// Shared core: the dispatch engine's Stage 2 — DESTINATION resolution loop. // Recipient-independent. One PENDING DestinationDeliveryLog per active // ChannelRoutingBinding whose (targetType, targetId) matches the event's // (sourceType, sourceId) and whose channel is an enabled DESTINATION-kind // channel. The rendered subject/body are captured on the log so the delivery // worker (internal/deliverDestination) can post from the row alone. import { type ReadonlyDB } from "@tailor-platform/erp-kit/core"; import type { DB, Transaction } from "../generated/kysely-tailordb"; import { DEFAULT_LOCALE, loadChannelByChannelId, loadTemplateWithFallback, } from "../lib/dispatchQueries"; import { interpolate } from "../lib/templateRendering"; export interface PlannedDestination { bindingId: string; channelId: string; externalChannelRef: string; logId: string; status: "PENDING" | "FAILED" | "SENT"; reason?: string; } interface ChannelRoutingBindingRow { id: string; targetType: string; targetId: string; channelId: string; externalChannelRef: string; displayLabel: string | null; isActive: boolean; } async function loadActiveBindings( db: ReadonlyDB, targetType: string, targetId: string, ): Promise { return (await db .selectFrom("ChannelRoutingBinding") .selectAll() .where("targetType", "=", targetType) .where("targetId", "=", targetId) .where("isActive", "=", true) .execute()) as ChannelRoutingBindingRow[]; } // Belt-and-suspenders idempotency, symmetric with the PERSONAL path's // loadExistingNotification: a DestinationDeliveryLog is keyed by the event's // logical identity `(idempotencyKey)` and the post target // `(channelId, externalChannelRef)`, so re-planning the same logical event // (e.g. a duplicate NotificationEvent row that slipped past the ingress dedup) // reuses the existing log instead of posting to the shared channel twice. The // idempotencyKey carries the payloadHash, so two *distinct* events for the same // (eventType, sourceType, sourceId) still get separate posts. async function loadExistingLog( db: Transaction, idempotencyKey: string, channelId: string, externalChannelRef: string, ): Promise<{ id: string; status: "PENDING" | "SENT" | "FAILED" } | undefined> { return (await db .selectFrom("DestinationDeliveryLog") .select(["id", "status"]) .where("idempotencyKey", "=", idempotencyKey) .where("channelId", "=", channelId) .where("externalChannelRef", "=", externalChannelRef) .executeTakeFirst()) as { id: string; status: "PENDING" | "SENT" | "FAILED" } | undefined; } async function insertLog( db: Transaction, row: { idempotencyKey: string; eventType: string; sourceType: string; sourceId: string; targetType: string; targetId: string; channelId: string; externalChannelRef: string; subject: string; body: string; status: "PENDING" | "FAILED"; failureReason: string | null; }, now: Date, ): Promise { const id = crypto.randomUUID(); await db .insertInto("DestinationDeliveryLog") .values({ id, ...row, providerMessageId: null, providerResponse: null, retryCount: 0, attemptedAt: now, completedAt: row.status === "FAILED" ? now : null, createdAt: now, updatedAt: now, }) .execute(); return id; } export interface PlanDestinationsArgs { eventType: string; sourceType: string; sourceId: string; /** Event logical-identity key (eventType:sourceType:sourceId:payloadHash) used to dedup logs across re-plans. */ idempotencyKey: string; locale?: string; payloadVars: Record; } export async function planDestinations( db: Transaction, args: PlanDestinationsArgs, now: Date, ): Promise { const out: PlannedDestination[] = []; const bindings = await loadActiveBindings(db, args.sourceType, args.sourceId); for (const b of bindings) { const channel = await loadChannelByChannelId(db, b.channelId); if (!channel || !channel.enabled || channel.kind !== "DESTINATION") continue; // Reuse an already-planned log for this (event identity, post target) rather // than inserting a duplicate that would post to the shared channel twice. const existing = await loadExistingLog( db, args.idempotencyKey, b.channelId, b.externalChannelRef, ); if (existing) { out.push({ bindingId: b.id, channelId: b.channelId, externalChannelRef: b.externalChannelRef, logId: existing.id, status: existing.status, reason: "DEDUPED", }); continue; } const locale = args.locale ?? DEFAULT_LOCALE; const template = await loadTemplateWithFallback(db, args.eventType, channel.id, locale); if (!template) { const logId = await insertLog( db, { idempotencyKey: args.idempotencyKey, eventType: args.eventType, sourceType: args.sourceType, sourceId: args.sourceId, targetType: b.targetType, targetId: b.targetId, channelId: b.channelId, externalChannelRef: b.externalChannelRef, subject: "", body: "", status: "FAILED", failureReason: "template_not_found", }, now, ); out.push({ bindingId: b.id, channelId: b.channelId, externalChannelRef: b.externalChannelRef, logId, status: "FAILED", reason: "TEMPLATE_NOT_FOUND", }); continue; } const subject = interpolate(template.subject, args.payloadVars, false); const body = interpolate(template.body, args.payloadVars, false); const logId = await insertLog( db, { idempotencyKey: args.idempotencyKey, eventType: args.eventType, sourceType: args.sourceType, sourceId: args.sourceId, targetType: b.targetType, targetId: b.targetId, channelId: b.channelId, externalChannelRef: b.externalChannelRef, subject, body, status: "PENDING", failureReason: null, }, now, ); out.push({ bindingId: b.id, channelId: b.channelId, externalChannelRef: b.externalChannelRef, logId, status: "PENDING", }); } return out; }