111 lines
3.4 KiB
TypeScript
111 lines
3.4 KiB
TypeScript
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<EmailType, boolean>;
|
|
|
|
/**
|
|
* 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<Record<string, unknown>>): 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<EmailPreferences> {
|
|
const safeId = sanitizeUuid(contribuyenteId);
|
|
const { rows } = await pool.query<{ email_preferences: Record<string, unknown> | 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<EmailPreferences>,
|
|
): Promise<EmailPreferences> {
|
|
const safeId = sanitizeUuid(contribuyenteId);
|
|
const merged: Record<string, boolean> = {};
|
|
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<Array<{ contribuyenteId: string; rfc: string; nombre: string; preferences: EmailPreferences }>> {
|
|
const { rows } = await pool.query<{
|
|
entidad_id: string;
|
|
rfc: string;
|
|
nombre: string;
|
|
email_preferences: Record<string, unknown> | 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 ?? {}),
|
|
}));
|
|
}
|