feat: facturación primer pago, fixes SAT/MP, autocompletado RFCs/conceptos
Backend: - Notificación email al admin cuando llega primer pago aprobado (sin factura auto) - Endpoints GET /pagos-sin-factura y POST /emitir-factura-pago para admin global - Fix vinculación org Facturapi Horux 360 (69f23a5a242e0af47a41fa0d) - Fix webhook MP: validación defensiva de x-signature header - Fix autocompleto RFCs: eliminado filtro por contribuyenteId - Fix autocompleto conceptos: eliminado filtro por contribuyenteId - SAT fixes: anti-bot CSF scraper, request reuse, date range fix, stale job thresholds - SAT sync request reuse across jobs para evitar agotar cuota diaria - Typo fix MP_ACCESS_TOKEN en .env - Trial invitations system backend Frontend: - Nueva página /admin/facturas-pendientes con tabla y emisión manual - Métrica 'Facturas pendientes' en /clientes (clickable) - Navegación onboarding FIEL/CSD corregida - Sidebar themes sincronizados - Fix SAT portal migration scraper (NetIQ) - Trial invitation acceptance pages
This commit is contained in:
@@ -111,7 +111,12 @@ export function extractXmlsFromZip(zipBase64: string): ExtractedXml[] {
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.entryName.toLowerCase().endsWith('.xml')) {
|
||||
const content = entry.getData().toString('utf-8');
|
||||
let content = entry.getData().toString('utf-8');
|
||||
// Remover UTF-8 BOM si existe — fast-xml-parser no lo maneja y devuelve
|
||||
// result.Comprobante = undefined, dejando el CFDI sin parsear.
|
||||
if (content.charCodeAt(0) === 0xFEFF) {
|
||||
content = content.slice(1);
|
||||
}
|
||||
xmlFiles.push({
|
||||
filename: entry.entryName,
|
||||
content,
|
||||
@@ -140,8 +145,13 @@ export function extractXmlsFromZip(zipBase64: string): ExtractedXml[] {
|
||||
*/
|
||||
function parseCfdiDate(str: string | null | undefined): Date {
|
||||
if (!str) return new Date(0);
|
||||
const s = String(str).trim();
|
||||
let s = String(str).trim();
|
||||
if (!s) return new Date(0);
|
||||
// Defensa: el SAT a veces concatena múltiples fechas con '|' (ej. en
|
||||
// FechaTimbrado duplicado). Tomamos solo la primera fecha válida.
|
||||
if (s.includes('|')) {
|
||||
s = s.split('|')[0].trim();
|
||||
}
|
||||
const hasTz = /[Zz]|[+-]\d{2}:?\d{2}$/.test(s);
|
||||
return new Date(hasTz ? s : s + 'Z');
|
||||
}
|
||||
@@ -155,18 +165,28 @@ function pf(val: any): number {
|
||||
return parseFloat(val || '0') || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrae el UUID del TimbreFiscalDigital
|
||||
*/
|
||||
function getFirstTimbre(comprobante: any): any {
|
||||
const timbre = comprobante.Complemento?.TimbreFiscalDigital;
|
||||
if (!timbre) return null;
|
||||
return Array.isArray(timbre) ? timbre[0] : timbre;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrae el UUID del TimbreFiscalDigital
|
||||
*/
|
||||
function extractUuid(comprobante: any): string {
|
||||
return comprobante.Complemento?.TimbreFiscalDigital?.['@_UUID'] || '';
|
||||
const timbre = getFirstTimbre(comprobante);
|
||||
return timbre?.['@_UUID'] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrae datos del timbre: fecha cert SAT y PAC
|
||||
*/
|
||||
function extractTimbreData(comprobante: any): { fechaCertSat: Date | null; pac: string | null } {
|
||||
const timbre = comprobante.Complemento?.TimbreFiscalDigital;
|
||||
const timbre = getFirstTimbre(comprobante);
|
||||
if (!timbre) return { fechaCertSat: null, pac: null };
|
||||
|
||||
return {
|
||||
@@ -322,7 +342,7 @@ function extractPagos(comprobante: any): {
|
||||
}
|
||||
}
|
||||
|
||||
result.fechaPagoP = fechas.length > 0 ? fechas.join('|') : null;
|
||||
result.fechaPagoP = fechas.length > 0 ? parseCfdiDate(fechas[0]).toISOString() : null;
|
||||
result.numParcialidad = parcialidades.length > 0 ? parcialidades.join('|') : null;
|
||||
result.uuidRelacionado = uuids.length > 0 ? uuids.join('|') : null;
|
||||
result.saldoInsoluto = saldos.length > 0 ? saldos.join('|') : null;
|
||||
@@ -370,9 +390,9 @@ function extractNomina(comprobante: any): {
|
||||
const nomina = complemento.Nomina;
|
||||
if (!nomina) return result;
|
||||
|
||||
result.fechaPago = nomina['@_FechaPago'] || null;
|
||||
result.fechaInicialPago = nomina['@_FechaInicialPago'] || null;
|
||||
result.fechaFinalPago = nomina['@_FechaFinalPago'] || null;
|
||||
result.fechaPago = nomina['@_FechaPago'] ? parseCfdiDate(nomina['@_FechaPago']).toISOString() : null;
|
||||
result.fechaInicialPago = nomina['@_FechaInicialPago'] ? parseCfdiDate(nomina['@_FechaInicialPago']).toISOString() : null;
|
||||
result.fechaFinalPago = nomina['@_FechaFinalPago'] ? parseCfdiDate(nomina['@_FechaFinalPago']).toISOString() : null;
|
||||
result.numDiasPagados = pf(nomina['@_NumDiasPagados']);
|
||||
result.totalPercepciones = pf(nomina['@_TotalPercepciones']);
|
||||
result.totalDeducciones = pf(nomina['@_TotalDeducciones']);
|
||||
|
||||
Reference in New Issue
Block a user