import type { Pool } from 'pg'; const VIGENTE = `status NOT IN ('Cancelado', '0')`; export interface ConciliacionCfdi { id: number; uuid: string; type: string; serie: string | null; folio: string | null; fechaEmision: string; rfcEmisor: string; nombreEmisor: string; rfcReceptor: string; nombreReceptor: string; total: number; totalMxn: number; subtotal: number; descuento: number; moneda: string; tipoCambio: number; tipoComprobante: string | null; metodoPago: string | null; formaPago: string | null; usoCfdi: string | null; status: string | null; fechaCertSat: string | null; ivaTraslado: number; ivaRetencion: number; isrRetencion: number; 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 { 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.serie, c.folio, 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.subtotal, c.descuento, c.moneda, c.tipo_cambio as "tipoCambio", c.tipo_comprobante as "tipoComprobante", c.monto_pago_mxn as "montoPagoMxn", c.metodo_pago as "metodoPago", c.forma_pago as "formaPago", c.uso_cfdi as "usoCfdi", c.status, c.fecha_cert_sat as "fechaCertSat", c.iva_traslado as "ivaTraslado", c.iva_retencion as "ivaRetencion", c.isr_retencion as "isrRetencion", 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, serie: r.serie, folio: r.folio, fechaEmision: r.fechaEmision, rfcEmisor: r.rfcEmisor, nombreEmisor: r.nombreEmisor, rfcReceptor: r.rfcReceptor, nombreReceptor: r.nombreReceptor, total: Number(r.total), totalMxn: Number(r.totalMxn), subtotal: Number(r.subtotal || 0), descuento: Number(r.descuento || 0), moneda: r.moneda || 'MXN', tipoCambio: Number(r.tipoCambio || 1), 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, formaPago: r.formaPago, usoCfdi: r.usoCfdi, status: r.status, fechaCertSat: r.fechaCertSat, ivaTraslado: Number(r.ivaTraslado || 0), ivaRetencion: Number(r.ivaRetencion || 0), isrRetencion: Number(r.isrRetencion || 0), 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 { 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 { // 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 { // 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]); } } }