import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; import { DuplicateChannelIdError, MissingRequiredFieldError } from "../lib/errors.generated"; export type ChannelId = "IN_APP" | "EMAIL" | "SMS" | "PUSH" | "SLACK" | "TEAMS"; export type ChannelKind = "PERSONAL" | "DESTINATION"; // Channels that post to a shared surface (one post per ChannelRoutingBinding) // rather than fanning out per recipient. const DESTINATION_CHANNEL_IDS: readonly ChannelId[] = ["SLACK", "TEAMS"]; export function defaultKindForChannel(channelId: ChannelId): ChannelKind { return DESTINATION_CHANNEL_IDS.includes(channelId) ? "DESTINATION" : "PERSONAL"; } export interface ChannelCapabilities { supportsHtmlBody: boolean; supportsAttachments: boolean; supportsRichActions: boolean; } export interface CreateNotificationChannelInput { channelId: ChannelId; // `kind` is intentionally not a caller input — it is derived from `channelId` // at insert time via the fixed mapping in defaultKindForChannel(). displayName: string; capabilities: ChannelCapabilities; enabled?: boolean; } function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object"; } function hasAllCapabilityKeys(value: unknown): value is ChannelCapabilities { if (!isRecord(value)) return false; return ( typeof value.supportsHtmlBody === "boolean" && typeof value.supportsAttachments === "boolean" && typeof value.supportsRichActions === "boolean" ); } /** * Inserts a row into the NotificationChannel registry so the dispatcher can * target a new delivery medium. The `kind` topology is derived from `channelId` * (never caller-supplied); `enabled` defaults to `true`. Rejects a duplicate * `channelId` and missing/blank required fields. */ export async function run( db: Transaction, input: CreateNotificationChannelInput, _ctx: CommandContext, ) { const { channelId, displayName, capabilities, enabled } = input; if (!channelId) { return err(new MissingRequiredFieldError("channelId")); } const trimmedDisplayName = displayName?.trim() ?? ""; if (trimmedDisplayName === "") { return err(new MissingRequiredFieldError("displayName")); } if (!hasAllCapabilityKeys(capabilities)) { return err(new MissingRequiredFieldError("capabilities")); } const existing = await db .selectFrom("NotificationChannel") .select("id") .where("channelId", "=", channelId) .executeTakeFirst(); if (existing) { return err(new DuplicateChannelIdError(channelId)); } const now = new Date(); const channel = await db .insertInto("NotificationChannel") .values({ id: crypto.randomUUID(), channelId, kind: defaultKindForChannel(channelId), displayName: trimmedDisplayName, capabilities, enabled: enabled ?? true, createdAt: now, updatedAt: now, }) .returningAll() .executeTakeFirstOrThrow(); return ok({ channel }); }