import type { Pool } from 'pg'; /** * Tipos de correos informativos cuyo envío puede desactivarse por * contribuyente. NO incluye correos transaccionales críticos * (welcome, password-reset, payment-*) — esos siempre se envían. * * Estado de implementación: * - documento_subido: ✅ implementado (notify-upload.service.ts) * - weekly_update: ⏳ pendiente (job es tenant-wide hoy) * - subscription_expiring: ⏳ pendiente (no es per-contribuyente hoy) * - recordatorio_fiscal: ⏳ placeholder para futuras alertas */ export const EMAIL_TYPES = [ 'documento_subido', 'weekly_update', 'subscription_expiring', 'recordatorio_fiscal', ] as const; export type EmailType = (typeof EMAIL_TYPES)[number]; export type EmailPreferences = Record; /** * Default: todo activado. Si el JSONB en BD viene vacío o falta una * key, asumimos `true` para preservar el comportamiento previo. */ function applyDefaults(raw: Partial>): EmailPreferences { const out = {} as EmailPreferences; for (const t of EMAIL_TYPES) { out[t] = raw[t] === false ? false : true; } return out; } function sanitizeUuid(id: string): string { return id.replace(/[^a-f0-9-]/gi, ''); } /** * Lee las preferencias de un contribuyente. Devuelve defaults (todo * activado) si no hay fila o la columna está vacía. */ export async function getContribuyenteEmailPreferences( pool: Pool, contribuyenteId: string, ): Promise { const safeId = sanitizeUuid(contribuyenteId); const { rows } = await pool.query<{ email_preferences: Record | null }>( `SELECT email_preferences FROM contribuyentes WHERE entidad_id = $1`, [safeId], ); const raw = rows[0]?.email_preferences ?? {}; return applyDefaults(raw); } /** * Actualiza las preferencias de un contribuyente. Solo persiste las * keys conocidas (filtra extras maliciosos). Merge sobre la columna * existente (no sobreescribe keys no enviadas). */ export async function setContribuyenteEmailPreferences( pool: Pool, contribuyenteId: string, partial: Partial, ): Promise { const safeId = sanitizeUuid(contribuyenteId); const merged: Record = {}; for (const t of EMAIL_TYPES) { if (t in partial) merged[t] = partial[t] === true; } await pool.query( `UPDATE contribuyentes SET email_preferences = COALESCE(email_preferences, '{}'::jsonb) || $2::jsonb WHERE entidad_id = $1`, [safeId, JSON.stringify(merged)], ); return getContribuyenteEmailPreferences(pool, contribuyenteId); } /** * Lee preferencias para múltiples contribuyentes en una sola query. * Útil para la UI de `/configuracion/notificaciones` que lista todos. */ export async function getEmailPreferencesPorContribuyente( pool: Pool, ): Promise> { const { rows } = await pool.query<{ entidad_id: string; rfc: string; nombre: string; email_preferences: Record | null; }>( `SELECT c.entidad_id, c.rfc, e.nombre, c.email_preferences FROM contribuyentes c JOIN entidades_gestionadas e ON e.id = c.entidad_id WHERE e.active = true ORDER BY e.nombre`, ); return rows.map(r => ({ contribuyenteId: r.entidad_id, rfc: r.rfc, nombre: r.nombre, preferences: applyDefaults(r.email_preferences ?? {}), })); }