Initial commit - Horux Despachos NL

This commit is contained in:
2026-05-03 16:47:53 -06:00
commit b00b677c54
647 changed files with 133843 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
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 ?? {}),
}));
}