import { ok, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; export type MarkAllNotificationsAsReadInput = Record; /** * Adds READ (and SEEN) to every caller-owned, deliverable IN_APP Notification, stamping readAt and writing one audit row per transition. Idempotent. */ export async function run( db: Transaction, _input: MarkAllNotificationsAsReadInput, ctx: CommandContext, ) { // Select caller-owned, deliverable IN_APP rows. The command is the bulk // counterpart of the inbox feed, so it scopes to the IN_APP channel the // same way listInboxNotifications / unreadCount do — rows dispatched on // other channels (e.g. EMAIL) must not be stamped READ. Engagement-set // predicates (READ ∉, ARCHIVED ∉) are filtered post-fetch in JS rather // than expressed in raw SQL — the dataset per call is bounded by the // user's own inbox so this is a single SELECT followed by per-row UPDATEs. 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("READ")) continue; if (row.engagementStatuses.includes("ARCHIVED")) continue; const seenAlreadyPresent = row.engagementStatuses.includes("SEEN"); const nextStatuses = [...row.engagementStatuses]; if (!seenAlreadyPresent) nextStatuses.push("SEEN"); nextStatuses.push("READ"); await db .updateTable("Notification") .set({ engagementStatuses: nextStatuses, seenAt: seenAlreadyPresent ? row.seenAt : now, readAt: now, updatedAt: now, }) .where("id", "=", row.id) .execute(); await db .insertInto("NotificationDeliveryAudit") .values({ id: crypto.randomUUID(), notificationId: row.id, eventType: "READ", occurredAt: now, occurredBy: ctx.actorId ?? null, errorClass: null, errorDetail: null, createdAt: now, updatedAt: now, }) .execute(); transitionedCount++; } return ok({ transitionedCount }); }