Files
HoruxDespachosNuevo/apps/api/src/services/notify-upload.service.ts

91 lines
3.2 KiB
TypeScript

import type { Pool } from 'pg';
import { prisma } from '../config/database.js';
import { emailService } from './email/email.service.js';
import { getTenantOwnerEmails, getUserEmailById } from '../utils/memberships.js';
import { env } from '../config/env.js';
import { getContribuyenteEmailPreferences } from './notification-preferences.service.js';
import type { DocumentoSubidoData } from './email/templates/documento-subido.js';
/**
* Notifica a los destinatarios relevantes cuando se sube una declaración
* o un documento extra. Destinatarios:
* - Owners activos del despacho (getTenantOwnerEmails)
* - Supervisor del contribuyente (entidades_gestionadas.supervisor_user_id),
* si existe y no coincide con un owner ya incluido
*
* El uploader mismo SE EXCLUYE (no tiene sentido notificarle su propia acción).
*
* Fire-and-forget: el caller hace `.catch()` y esta función no re-lanza.
* Fail-soft: si SMTP no está configurado, los envíos se loguean a consola
* vía el transport de @horux/core.
*/
export async function notifyDocumentoSubido(params: {
pool: Pool;
tenantId: string;
contribuyenteId: string | null;
subidoPor: string;
kind: DocumentoSubidoData['kind'];
declaracion?: DocumentoSubidoData['declaracion'];
extra?: DocumentoSubidoData['extra'];
}): Promise<void> {
const { pool, tenantId, contribuyenteId, subidoPor } = params;
// 1. Datos del contribuyente (desde BD tenant). Sin contribuyenteId no hay
// subject informativo ni supervisor — skip.
if (!contribuyenteId) return;
// Respeta preferencias de notificación del contribuyente. Si el user
// desactivó `documento_subido` para este contribuyente, no enviar.
const prefs = await getContribuyenteEmailPreferences(pool, contribuyenteId);
if (!prefs.documento_subido) return;
const { rows } = await pool.query<{
rfc: string;
nombre: string;
supervisor_user_id: string | null;
}>(
`SELECT c.rfc, eg.nombre, eg.supervisor_user_id
FROM contribuyentes c
JOIN entidades_gestionadas eg ON eg.id = c.entidad_id
WHERE c.entidad_id = $1`,
[contribuyenteId.replace(/[^a-f0-9-]/gi, '')],
);
if (rows.length === 0) return;
const contrib = rows[0];
// 2. Recipients. Owners primero; luego supervisor si aplica.
const owners = await getTenantOwnerEmails(tenantId);
const recipients = new Set<string>(owners);
if (contrib.supervisor_user_id) {
const supervisorEmail = await getUserEmailById(contrib.supervisor_user_id);
if (supervisorEmail) recipients.add(supervisorEmail);
}
// Excluir al uploader: no notificarle su propia acción.
recipients.delete(subidoPor.toLowerCase());
recipients.delete(subidoPor);
if (recipients.size === 0) return;
// 3. Datos del despacho (para mostrar en el body).
const tenant = await prisma.tenant.findUnique({
where: { id: tenantId },
select: { nombre: true },
});
// 4. Link al sistema. Usa FRONTEND_URL del env.
const link = `${env.FRONTEND_URL}/documentos`;
await emailService.sendDocumentoSubido(Array.from(recipients), {
kind: params.kind,
subidoPor,
contribuyenteRfc: contrib.rfc,
contribuyenteNombre: contrib.nombre,
despachoNombre: tenant?.nombre,
declaracion: params.declaracion,
extra: params.extra,
link,
});
}