Initial commit - Horux Despachos NL
This commit is contained in:
257
apps/api/src/services/conciliacion.service.ts
Normal file
257
apps/api/src/services/conciliacion.service.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
import type { Pool } from 'pg';
|
||||
|
||||
const VIGENTE = `status NOT IN ('Cancelado', '0')`;
|
||||
|
||||
export interface ConciliacionCfdi {
|
||||
id: number;
|
||||
uuid: string;
|
||||
type: string;
|
||||
fechaEmision: string;
|
||||
rfcEmisor: string;
|
||||
nombreEmisor: string;
|
||||
rfcReceptor: string;
|
||||
nombreReceptor: string;
|
||||
total: number;
|
||||
totalMxn: number;
|
||||
metodoPago: string | null;
|
||||
conciliado: string | null;
|
||||
idConciliacion: number | null;
|
||||
conciliacion: {
|
||||
id: number;
|
||||
fechaDePago: string;
|
||||
banco: string;
|
||||
terminacionCuenta: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export async function getCfdisConConciliacion(
|
||||
pool: Pool,
|
||||
filters: {
|
||||
tipo: string;
|
||||
fechaInicio?: string;
|
||||
fechaFin?: string;
|
||||
regimen?: string;
|
||||
estado?: string;
|
||||
contribuyenteId?: string;
|
||||
}
|
||||
): Promise<ConciliacionCfdi[]> {
|
||||
const params: any[] = [];
|
||||
let idx = 1;
|
||||
|
||||
let where = `WHERE c.type = $${idx++} AND c.${VIGENTE}`;
|
||||
params.push(filters.tipo);
|
||||
|
||||
// Excluir PPD en recibidos
|
||||
if (filters.tipo === 'RECIBIDO') {
|
||||
where += ` AND (c.metodo_pago IS NULL OR c.metodo_pago != 'PPD')`;
|
||||
}
|
||||
|
||||
// Excluir PPD en emitidos para todos los regimenes excepto 605 y 616
|
||||
if (filters.tipo === 'EMITIDO') {
|
||||
where += ` AND NOT (c.metodo_pago = 'PPD' AND (c.regimen_fiscal_emisor IS NULL OR c.regimen_fiscal_emisor NOT IN ('605','616')))`;
|
||||
}
|
||||
|
||||
if (filters.fechaInicio) {
|
||||
where += ` AND c.fecha_emision >= $${idx++}::date`;
|
||||
params.push(filters.fechaInicio);
|
||||
}
|
||||
if (filters.fechaFin) {
|
||||
where += ` AND c.fecha_emision <= ($${idx++}::date + interval '1 day')`;
|
||||
params.push(filters.fechaFin);
|
||||
}
|
||||
if (filters.regimen) {
|
||||
const regimenCol = filters.tipo === 'EMITIDO' ? 'regimen_fiscal_emisor' : 'regimen_fiscal_receptor';
|
||||
where += ` AND c.${regimenCol} = $${idx++}`;
|
||||
params.push(filters.regimen);
|
||||
}
|
||||
if (filters.estado === 'conciliado') {
|
||||
where += ` AND c.conciliado = 'true'`;
|
||||
} else if (filters.estado === 'pendiente') {
|
||||
where += ` AND (c.conciliado IS NULL OR c.conciliado != 'true')`;
|
||||
}
|
||||
|
||||
if (filters.contribuyenteId) {
|
||||
const safeId = filters.contribuyenteId.replace(/[^a-f0-9-]/gi, '');
|
||||
where += ` AND c.contribuyente_id = '${safeId}'`;
|
||||
}
|
||||
|
||||
const { rows } = await pool.query(`
|
||||
SELECT
|
||||
c.id, c.uuid, c.type,
|
||||
c.fecha_emision as "fechaEmision",
|
||||
c.rfc_emisor as "rfcEmisor", c.nombre_emisor as "nombreEmisor",
|
||||
c.rfc_receptor as "rfcReceptor", c.nombre_receptor as "nombreReceptor",
|
||||
c.total, c.total_mxn as "totalMxn",
|
||||
c.tipo_comprobante as "tipoComprobante",
|
||||
c.monto_pago_mxn as "montoPagoMxn",
|
||||
c.metodo_pago as "metodoPago",
|
||||
c.conciliado,
|
||||
c.id_conciliacion as "idConciliacion",
|
||||
con.id as "conId",
|
||||
con.fecha_de_pago as "conFechaDePago",
|
||||
b.banco as "conBanco",
|
||||
b.terminacion_cuenta as "conTerminacionCuenta"
|
||||
FROM cfdis c
|
||||
LEFT JOIN conciliaciones con ON con.id_cfdi = c.id
|
||||
LEFT JOIN bancos b ON b.id = con.id_banco
|
||||
${where}
|
||||
ORDER BY c.fecha_emision DESC
|
||||
`, params);
|
||||
|
||||
return rows.map((r: any) => ({
|
||||
id: r.id,
|
||||
uuid: r.uuid,
|
||||
type: r.type,
|
||||
fechaEmision: r.fechaEmision,
|
||||
rfcEmisor: r.rfcEmisor,
|
||||
nombreEmisor: r.nombreEmisor,
|
||||
rfcReceptor: r.rfcReceptor,
|
||||
nombreReceptor: r.nombreReceptor,
|
||||
total: Number(r.total),
|
||||
totalMxn: Number(r.totalMxn),
|
||||
tipoComprobante: r.tipoComprobante,
|
||||
montoPagoMxn: Number(r.montoPagoMxn || 0),
|
||||
// P usa monto_pago_mxn, PPD conciliada no suma (evitar duplicar con su P), resto usa total_mxn
|
||||
montoMxn: r.tipoComprobante === 'P'
|
||||
? Number(r.montoPagoMxn || 0)
|
||||
: (r.metodoPago === 'PPD' && r.conciliado === 'true') ? 0 : Number(r.totalMxn || 0),
|
||||
metodoPago: r.metodoPago,
|
||||
conciliado: r.conciliado,
|
||||
idConciliacion: r.idConciliacion,
|
||||
conciliacion: r.conId ? {
|
||||
id: r.conId,
|
||||
fechaDePago: r.conFechaDePago,
|
||||
banco: r.conBanco,
|
||||
terminacionCuenta: r.conTerminacionCuenta,
|
||||
} : null,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function conciliar(
|
||||
pool: Pool,
|
||||
data: { cfdiIds: number[]; fechaDePago: string; idBanco: number },
|
||||
tenantCreatedYear: number,
|
||||
): Promise<number> {
|
||||
const fechaPago = new Date(data.fechaDePago + 'T12:00:00');
|
||||
const anio = String(fechaPago.getFullYear());
|
||||
const mes = String(fechaPago.getMonth() + 1).padStart(2, '0');
|
||||
|
||||
if (fechaPago.getFullYear() < tenantCreatedYear) {
|
||||
throw new Error(`Solo se puede conciliar del año ${tenantCreatedYear} en adelante`);
|
||||
}
|
||||
|
||||
const { rows: bancoRows } = await pool.query(`SELECT id FROM bancos WHERE id = $1`, [data.idBanco]);
|
||||
if (bancoRows.length === 0) throw new Error('Banco no encontrado');
|
||||
|
||||
const { rows: cfdis } = await pool.query(`
|
||||
SELECT id, conciliado FROM cfdis
|
||||
WHERE id = ANY($1) AND ${VIGENTE}
|
||||
`, [data.cfdiIds]);
|
||||
|
||||
if (cfdis.length !== data.cfdiIds.length) {
|
||||
throw new Error('Algunos CFDIs no existen o estan cancelados');
|
||||
}
|
||||
|
||||
const yaConc = cfdis.filter((c: any) => c.conciliado === 'true');
|
||||
if (yaConc.length > 0) {
|
||||
throw new Error(`${yaConc.length} CFDIs ya estan conciliados`);
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for (const cfdiId of data.cfdiIds) {
|
||||
const { rows: inserted } = await pool.query(`
|
||||
INSERT INTO conciliaciones (anio, mes, id_cfdi, fecha_de_pago, id_banco)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id
|
||||
`, [anio, mes, cfdiId, data.fechaDePago, data.idBanco]);
|
||||
|
||||
await pool.query(`
|
||||
UPDATE cfdis SET conciliado = 'true', id_conciliacion = $1 WHERE id = $2
|
||||
`, [inserted[0].id, cfdiId]);
|
||||
|
||||
count++;
|
||||
|
||||
// Auto-conciliar PPD si esta factura tipo P lleva saldo pendiente a 0
|
||||
await autoConciliarPpd(pool, cfdiId, anio, mes, data.fechaDePago, data.idBanco);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cuando se concilia una factura tipo P con saldo_pendiente = 0,
|
||||
* auto-concilia la factura PPD original relacionada con los mismos datos.
|
||||
*/
|
||||
async function autoConciliarPpd(
|
||||
pool: Pool,
|
||||
cfdiId: number,
|
||||
anio: string,
|
||||
mes: string,
|
||||
fechaDePago: string,
|
||||
idBanco: number,
|
||||
): Promise<void> {
|
||||
// Verificar si es tipo P con saldo pendiente 0
|
||||
const { rows: pRows } = await pool.query(`
|
||||
SELECT tipo_comprobante, uuid_relacionado, saldo_pendiente
|
||||
FROM cfdis WHERE id = $1
|
||||
`, [cfdiId]);
|
||||
|
||||
const pCfdi = pRows[0];
|
||||
if (!pCfdi || pCfdi.tipo_comprobante !== 'P') return;
|
||||
if (!pCfdi.uuid_relacionado) return;
|
||||
|
||||
const saldoPendiente = Number(pCfdi.saldo_pendiente || 0);
|
||||
if (saldoPendiente !== 0) return;
|
||||
|
||||
// Buscar la factura PPD original por UUID
|
||||
const { rows: ppdRows } = await pool.query(`
|
||||
SELECT id, conciliado FROM cfdis
|
||||
WHERE uuid = $1 AND metodo_pago = 'PPD' AND ${VIGENTE}
|
||||
`, [pCfdi.uuid_relacionado]);
|
||||
|
||||
if (ppdRows.length === 0) return;
|
||||
const ppd = ppdRows[0];
|
||||
if (ppd.conciliado === 'true') return; // ya conciliada
|
||||
|
||||
// Auto-conciliar la PPD con los mismos datos
|
||||
const { rows: inserted } = await pool.query(`
|
||||
INSERT INTO conciliaciones (anio, mes, id_cfdi, fecha_de_pago, id_banco)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id
|
||||
`, [anio, mes, ppd.id, fechaDePago, idBanco]);
|
||||
|
||||
await pool.query(`
|
||||
UPDATE cfdis SET conciliado = 'true', id_conciliacion = $1 WHERE id = $2
|
||||
`, [inserted[0].id, ppd.id]);
|
||||
}
|
||||
|
||||
export async function desconciliar(pool: Pool, conciliacionId: number): Promise<void> {
|
||||
// Buscar la conciliacion y el CFDI asociado
|
||||
const { rows } = await pool.query(`
|
||||
SELECT con.id_cfdi, c.tipo_comprobante, c.uuid_relacionado
|
||||
FROM conciliaciones con
|
||||
JOIN cfdis c ON c.id = con.id_cfdi
|
||||
WHERE con.id = $1
|
||||
`, [conciliacionId]);
|
||||
if (rows.length === 0) throw new Error('Conciliacion no encontrada');
|
||||
|
||||
const cfdi = rows[0];
|
||||
|
||||
// Desconciliar el CFDI principal
|
||||
await pool.query(`UPDATE cfdis SET conciliado = NULL, id_conciliacion = NULL WHERE id_conciliacion = $1`, [conciliacionId]);
|
||||
await pool.query(`DELETE FROM conciliaciones WHERE id = $1`, [conciliacionId]);
|
||||
|
||||
// Si es tipo P, también desconciliar la PPD auto-conciliada
|
||||
if (cfdi.tipo_comprobante === 'P' && cfdi.uuid_relacionado) {
|
||||
const { rows: ppdRows } = await pool.query(`
|
||||
SELECT c.id_conciliacion FROM cfdis c
|
||||
WHERE c.uuid = $1 AND c.conciliado = 'true' AND c.metodo_pago = 'PPD'
|
||||
`, [cfdi.uuid_relacionado]);
|
||||
|
||||
if (ppdRows.length > 0 && ppdRows[0].id_conciliacion) {
|
||||
const ppdConcId = ppdRows[0].id_conciliacion;
|
||||
await pool.query(`UPDATE cfdis SET conciliado = NULL, id_conciliacion = NULL WHERE id_conciliacion = $1`, [ppdConcId]);
|
||||
await pool.query(`DELETE FROM conciliaciones WHERE id = $1`, [ppdConcId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user