Files
cfw-autumn/server/src/internal/migrations/v2/lazy/checkPendingMigrationsForCustomer.ts
2026-05-13 17:24:08 +08:00

84 lines
2.7 KiB
TypeScript

import type { FullCustomer, MigrationItemRunData } from "@autumn/shared";
import { customerFilterMatchesFullCustomer } from "@autumn/shared/api/customers/utils/match/index.js";
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
import { runMigrationCustomerTask } from "@/trigger/migrations/runMigrationCustomerTask.js";
/**
* For each pending lazy migration on `ctx.org`, decide whether this customer
* needs migrating and enqueue a per-customer Trigger.dev task if so.
*
* Reads `migration_item_runs` from the already-loaded `fullCustomer` (embedded
* by the FullSubject / FullCustomer query) — no extra DB roundtrip.
*
* Fire-and-forget: the helper doesn't wait for the migration to complete.
* `executeMigrateCustomerPlan` inside the task busts the customer cache,
* so subsequent requests read post-migration state.
*/
export const checkPendingMigrationsForCustomer = async ({
ctx,
fullCustomer,
}: {
ctx: AutumnContext;
fullCustomer: Pick<
FullCustomer,
"id" | "internal_id" | "customer_products" | "migration_item_runs"
>;
}): Promise<void> => {
// Short-circuit when we're already inside a Trigger.dev task. A migration
// worker loading the customer via `CusService.getFull` would otherwise
// re-enter this helper and enqueue another task. Flag is set by
// `createTriggerContext`.
if (ctx.insideTriggerTask) return;
const pending = ctx.org.pendingMigrations ?? [];
if (pending.length === 0) return;
const itemRunsByMigrationInternalId = new Map<string, MigrationItemRunData>();
for (const itemRun of fullCustomer.migration_item_runs ?? []) {
const existing = itemRunsByMigrationInternalId.get(
itemRun.migration_internal_id,
);
if (
!existing ||
(itemRun.updated_at ?? 0) > (existing.updated_at ?? 0) ||
(itemRun.updated_at === existing.updated_at &&
itemRun.created_at > existing.created_at)
) {
itemRunsByMigrationInternalId.set(itemRun.migration_internal_id, itemRun);
}
}
for (const pendingMigration of pending) {
const { internal_id: migrationRunId, migration } = pendingMigration;
const matches = customerFilterMatchesFullCustomer({
filter: migration.filter?.customer ?? {},
fullCustomer,
});
if (!matches) continue;
const itemRun = itemRunsByMigrationInternalId.get(migration.internal_id);
if (
itemRun?.status === "succeeded" ||
itemRun?.status === "skipped" ||
itemRun?.status === "running"
) {
continue;
}
await runMigrationCustomerTask.trigger(
{
orgId: ctx.org.id,
env: ctx.env,
migrationInternalId: migration.internal_id,
migrationRunId,
customerInternalId: fullCustomer.internal_id,
customerId: fullCustomer.id ?? null,
},
{
concurrencyKey: `${migration.internal_id}:${fullCustomer.internal_id}`,
},
);
}
};