import { type CommonReservedFields, type NoReservedFields, defaultGqlPermission, defaultPermission, } from "@tailor-platform/erp-kit/core"; import { db, type TailorAnyDBField } from "@tailor-platform/sdk"; import { NOTIFICATION_REASONS } from "../lib/events"; import { notificationLifecycle } from "./notification.lifecycle.generated"; import { notificationChannel } from "./notificationChannel"; /** Delivery-axis states of a Notification (e.g. QUEUED, SENT, DELIVERED, FAILED, BOUNCED). */ export const NOTIFICATION_DELIVERY_STATUSES = notificationLifecycle.states; /** Engagement states a Notification can accumulate (SEEN, READ, ARCHIVED). */ export const NOTIFICATION_ENGAGEMENT_STATUSES = ["SEEN", "READ", "ARCHIVED"] as const; const builtins = { recipientUserId: db .string() .description("Recipient user id; replaced with TOMBSTONED when the row has been anonymized"), channelId: db .uuid() .relation({ type: "n-1", toward: { type: notificationChannel, as: "channel" }, backward: "notifications", }) .description("Foreign key to NotificationChannel this row was dispatched on"), eventType: db.string().description("Event type key resolved at dispatch time"), sourceType: db .string() .description("Polymorphic source-record discriminator owned by the emitting module"), sourceId: db.string().description("Polymorphic source-record id; no FK constraint"), reason: db .enum(NOTIFICATION_REASONS) .description( "Resolved inclusion reason (SUBSCRIBED/ASSIGNED/AUTHOR/MENTION). ASSIGNED/MENTION trigger the critical bypass over a preference mute", ), locale: db.string().description("Locale used to render this row's subject and body"), payloadVars: db .string({ optional: true }) .description( "JSON-encoded payload variables supplied by the emitter; cleared on anonymization", ), subject: db .string() .description( "Rendered subject captured at dispatch (immutable; replaced with TOMBSTONED on anonymization)", ), body: db .string() .description( "Rendered plain-text body captured at dispatch (immutable; replaced with TOMBSTONED on anonymization)", ), htmlBody: db .string({ optional: true }) .description("Rendered HTML body for channels that support it; cleared on anonymization"), idempotencyKey: db .string() .description("Per-recipient dedup key; uniqueness enforced with channelId + recipientUserId"), deliveryStatus: db .enum(NOTIFICATION_DELIVERY_STATUSES) .description("Linear delivery axis (QUEUED, SENT, DELIVERED, FAILED, BOUNCED)"), engagementStatuses: db .string({ array: true }) .description("Monotonic engagement set (SEEN, READ, ARCHIVED)"), seenAt: db .datetime({ optional: true }) .description("First time SEEN was added to engagementStatuses"), readAt: db .datetime({ optional: true }) .description("First time READ was added to engagementStatuses"), archivedAt: db .datetime({ optional: true }) .description("First time ARCHIVED was added to engagementStatuses"), adapterMessageId: db .string({ optional: true }) .description("Optional provider message id returned by the channel adapter on success"), }; type ReservedFields = keyof typeof builtins | CommonReservedFields; export interface CreateNotificationTypeParams> { fields?: NoReservedFields; } export function createNotificationType>( params: CreateNotificationTypeParams, ) { return db .table("Notification", { ...builtins, ...((params.fields ?? {}) as F), ...db.fields.timestamps(), }) .indexes({ fields: ["recipientUserId", "channelId", "idempotencyKey"], unique: true, name: "notification_idempotency_unique_idx", }) .permission(defaultPermission) .gqlPermission(defaultGqlPermission); } export const notification = createNotificationType({});