/** * Per-category email send-from routing. * * Each outbound email category (account activation, EOI signing request, * brochure send, etc.) resolves to a `SenderAccount` — `noreply` for * automation-shaped traffic, `sales` for human-touch traffic. The * mapping is admin-configurable per port via the `email_routing` * system_setting (JSONB blob). * * When a category routes to `sales` but the port has no configured * sales account (no SMTP creds in `sales_email_config` settings), * the resolver transparently falls back to `noreply` so a half- * configured port still sends rather than throwing. Admin UI * surfaces this by disabling the `sales` option when creds are * absent. * * Defaults follow CLAUDE.md: auth/notifications/EOI-invite → noreply; * brochure/berth-PDF/signed-doc completion/sales send-out → sales. */ import { getSetting } from '@/lib/services/settings.service'; import { getSalesEmailConfig, type SenderAccount } from '@/lib/services/sales-email-config.service'; export const EMAIL_CATEGORIES = [ 'account_activation', 'password_reset', 'notification_digest', 'eoi_signing_request', 'reservation_signing_request', 'contract_signing_request', 'brochure_send', 'berth_pdf_send', 'signed_doc_completion', 'sales_sendout', 'manual_compose', 'inquiry_confirmation', 'inquiry_sales_notification', 'crm_invite', 'admin_email_change', 'gdpr_export', 'system_other', ] as const; export type EmailCategory = (typeof EMAIL_CATEGORIES)[number]; /** * Default sender per category. Categories not listed here fall back to * `noreply`. The admin UI seeds the routing matrix from this map on * first open. */ export const DEFAULT_CATEGORY_ROUTING: Record = { account_activation: 'noreply', password_reset: 'noreply', notification_digest: 'noreply', eoi_signing_request: 'noreply', reservation_signing_request: 'noreply', contract_signing_request: 'noreply', brochure_send: 'sales', berth_pdf_send: 'sales', signed_doc_completion: 'sales', sales_sendout: 'sales', manual_compose: 'sales', inquiry_confirmation: 'noreply', inquiry_sales_notification: 'noreply', crm_invite: 'noreply', admin_email_change: 'noreply', gdpr_export: 'noreply', system_other: 'noreply', }; export const EMAIL_ROUTING_KEY = 'email_routing'; /** * Resolve which sender account a given category should use for this * port. Reads the per-port `email_routing` JSONB blob; falls back to * the default mapping when unset, malformed, or missing the key. * Further falls back to `noreply` if the resolved sender is `sales` * but the port has no sales SMTP credentials configured (so a half- * configured port still sends through noreply rather than throwing). */ export async function resolveSenderForCategory( portId: string, category: EmailCategory, ): Promise { const setting = await getSetting(EMAIL_ROUTING_KEY, portId); let configured: SenderAccount = DEFAULT_CATEGORY_ROUTING[category]; if (setting && typeof setting.value === 'object' && setting.value !== null) { const routing = setting.value as Record; const v = routing[category]; if (v === 'noreply' || v === 'sales') { configured = v; } } if (configured === 'sales') { const sales = await getSalesEmailConfig(portId); if (!sales.isUsable) { return 'noreply'; } } return configured; } /** * Bulk-resolve the whole routing matrix for the admin UI. Returns the * effective mapping (defaults overlaid with whatever the port has * configured) plus an `isSalesAvailable` flag the UI uses to disable * the `sales` option when no creds are set. */ export async function getEmailRoutingMatrix(portId: string): Promise<{ routing: Record; isSalesAvailable: boolean; }> { const [setting, sales] = await Promise.all([ getSetting(EMAIL_ROUTING_KEY, portId), getSalesEmailConfig(portId), ]); const stored = setting && typeof setting.value === 'object' && setting.value !== null ? (setting.value as Record) : {}; const routing = { ...DEFAULT_CATEGORY_ROUTING }; for (const cat of EMAIL_CATEGORIES) { const v = stored[cat]; if (v === 'noreply' || v === 'sales') { routing[cat] = v; } } return { routing, isSalesAvailable: sales.isUsable }; }