import { ok, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; export interface MarkNotificationsAsSeenInput {} /** * Adds SEEN to every caller-owned IN_APP Notification whose deliveryStatus is * SENT/DELIVERED and that is not yet SEEN or ARCHIVED, stamps seenAt, and writes * one SEEN audit row per transition. SEEN is not READ and does not affect * unreadCount. Idempotent: a zero-row selection returns transitionedCount=0. */ export async function run( db: Transaction, input: MarkNotificationsAsSeenInput, ctx: CommandContext, ) { void input; // Select caller-owned, deliverable IN_APP rows whose engagementStatuses do // not yet contain SEEN. The command scopes to the IN_APP channel the same way // listInboxNotifications / markAllNotificationsAsRead do — rows dispatched on // other channels (e.g. EMAIL, terminal at SENT) must never be stamped SEEN. // Per-row containment of SEEN / ARCHIVED is filtered post-fetch to keep array // semantics out of the where clause; the database narrows on channel / owner / // status. const candidates = await db .selectFrom("Notification") .innerJoin("NotificationChannel", "NotificationChannel.id", "Notification.channelId") .where("NotificationChannel.channelId", "=", "IN_APP") .selectAll("Notification") .where("recipientUserId", "=", ctx.actorId) .where("deliveryStatus", "in", ["SENT", "DELIVERED"]) .forUpdate() .execute(); const now = new Date(); let transitionedCount = 0; for (const row of candidates) { if (row.engagementStatuses.includes("ARCHIVED")) continue; if (row.engagementStatuses.includes("SEEN")) continue; const nextStatuses = [...row.engagementStatuses, "SEEN"]; await db .updateTable("Notification") .set({ engagementStatuses: nextStatuses, seenAt: now, updatedAt: now, }) .where("id", "=", row.id) .execute(); await db .insertInto("NotificationDeliveryAudit") .values({ id: crypto.randomUUID(), notificationId: row.id, eventType: "SEEN", occurredAt: now, occurredBy: ctx.actorId ?? null, errorClass: null, errorDetail: null, createdAt: now, updatedAt: now, }) .execute(); transitionedCount++; } return ok({ transitionedCount }); }