import { ok, err, type ReadonlyDB, UnauthenticatedError, type CallerContext, } from "@tailor-platform/erp-kit/core"; import type { DB } from "../generated/kysely-tailordb"; import { loadChannelByChannelId } from "../lib/dispatchQueries"; import { CategoryNotFoundError, ChannelNotFoundError, ForbiddenError, } from "../lib/errors.generated"; export interface GetEffectiveNotificationPreferenceInput { userId: string; eventType: string; channelKey: string; } export type EffectiveSource = "TRANSACTIONAL_OVERRIDE" | "USER_PREFERENCE" | "DEFAULT"; export interface EffectivePreference { allowed: boolean; source: EffectiveSource; } /** * Resolves the effective dispatch decision for a (userId, eventType, channelKey) * triple, composing the dispatcher's gate stack in order: transactional override, * the user's NotificationPreference row, then the default opt-in. Returns * `{ allowed, source }` disclosing which layer decided. The reason-driven critical * bypass is intentionally out of scope. */ export async function run( db: ReadonlyDB, input: GetEffectiveNotificationPreferenceInput, ctx: CallerContext, ) { // Every result here is scoped to the caller, so there is nothing to return when // there is no caller. Queries carry no permission gate of their own. const { actorId } = ctx; if (actorId === null) return err(new UnauthenticatedError()); const { userId, eventType, channelKey } = input; if (actorId !== userId) { return err(new ForbiddenError(userId)); } const binding = await db .selectFrom("EventCategoryBinding") .selectAll() .where("eventType", "=", eventType) .executeTakeFirst(); if (!binding) { return err(new CategoryNotFoundError(eventType)); } const category = await db .selectFrom("NotificationCategory") .select(["optOutAllowed"]) .where("id", "=", binding.categoryId) .executeTakeFirst(); const transactional = Boolean(binding.transactional) || category?.optOutAllowed === false; if (transactional) { return ok({ effective: { allowed: true, source: "TRANSACTIONAL_OVERRIDE" as const, } satisfies EffectivePreference, }); } const channel = await loadChannelByChannelId(db, channelKey); if (!channel) { return err(new ChannelNotFoundError(channelKey)); } const preference = await db .selectFrom("NotificationPreference") .selectAll() .where("userId", "=", userId) .where("categoryId", "=", binding.categoryId) .where("channelId", "=", channel.id) .executeTakeFirst(); if (preference) { return ok({ effective: { allowed: preference.allowed, source: "USER_PREFERENCE" as const, } satisfies EffectivePreference, }); } return ok({ effective: { allowed: true, source: "DEFAULT" as const, } satisfies EffectivePreference, }); }