// Shared core: given a persisted PENDING NotificationEvent row, map it to the // plan engine input, run `planNotificationInternal` (which only writes the // outbox — Notification QUEUED + DestinationDeliveryLog PENDING rows), stamp the // event's terminal status, and return the ids of the outbox rows that still need // delivery. Used by both the CDC `dispatch-notification-events` executor // (primary trigger) and the insurance re-drain cron, so the two share one // mapping + status rule. import { type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; import type { DispatchAdapters } from "../lib/dispatchAdapters"; import { parseNotificationEventPayload } from "../lib/events"; import { planNotificationInternal, type PlanNotificationInput } from "./planNotification"; export interface PersistedEventRow { id: string; eventType: string; sourceType: string; sourceId: string; actorUserId: string | null; payload: string; payloadHash: string; status: string; } export type PlanEventStatus = "DISPATCHED" | "NO_DELIVERY"; export interface PlanPersistedEventOutcome { status: PlanEventStatus; ok: boolean; /** Error code when planning failed (e.g. CATEGORY_NOT_FOUND). */ reason?: string; /** QUEUED Notification ids to hand to the delivery worker. */ notificationIds: string[]; /** PENDING DestinationDeliveryLog ids to hand to the delivery worker. */ destinationLogIds: string[]; } /** * Plan one persisted event into outbox rows and stamp its terminal status. * * `idempotencyKey` is derived from the event's logical identity * `(eventType, sourceType, sourceId, payloadHash)` — not the event row id — so * plan dedup is stable across CDC re-fires and insurance re-drains AND converges * at the notification level even if duplicate event rows slip past the ingress * dedup (the same logical event maps to the same Notification rows). A planning * error (e.g. an `eventType` absent from the Event Catalog — an emitter contract * bug) is treated as `NO_DELIVERY` so the insurance cron does not re-plan it * forever; the error code is surfaced for diagnosis rather than silently * swallowed. */ export async function planPersistedEvent( db: Transaction, event: PersistedEventRow, adapters: DispatchAdapters, ctx: CommandContext, now: Date, ): Promise { const payload = parseNotificationEventPayload(event.payload) ?? {}; const input: PlanNotificationInput = { eventType: event.eventType, sourceType: event.sourceType, sourceId: event.sourceId, recipients: payload.recipients, actorUserId: event.actorUserId, notifySelf: payload.notifySelf, payloadVars: payload, idempotencyKey: `${event.eventType}:${event.sourceType}:${event.sourceId}:${event.payloadHash}`, locale: typeof payload.locale === "string" ? payload.locale : undefined, }; const result = await planNotificationInternal(db, input, ctx, adapters); let status: PlanEventStatus; let reason: string | undefined; let notificationIds: string[] = []; let destinationLogIds: string[] = []; if (!result.ok) { status = "NO_DELIVERY"; reason = result.error.code ?? "DISPATCH_FAILED"; } else { const r = result.value; const produced = r.notifications.length + r.deduped.length + r.destinations.length > 0; status = produced ? "DISPATCHED" : "NO_DELIVERY"; notificationIds = r.notifications.filter((n) => n.deliveryStatus === "QUEUED").map((n) => n.id); destinationLogIds = r.destinations.filter((d) => d.status === "PENDING").map((d) => d.logId); } await db .updateTable("NotificationEvent") .set({ status, dispatchedAt: now, updatedAt: now }) .where("id", "=", event.id) .execute(); return { status, ok: result.ok, reason, notificationIds, destinationLogIds }; }