feat(pos): migrate CFDI timbrado from Horux to Facturapi

- Add Facturapi REST service (invoices, customers, orgs, cancel, downloads)
- Add JSON payload builder for ingreso/egreso/pago/global invoices
- Replace XML queue with Facturapi JSON queue (payload_unsigned, external_id)
- Update invoicing blueprint with Facturapi config and download endpoints
- Update global invoice service to use Facturapi payloads
- Add migration v4.3_facturapi.sql and tenant rollout script
- Update invoicing UI: payload preview, PDF/XML downloads, PAC status panel
- Add FACTURAPI_USER_KEY to .env.example
This commit is contained in:
2026-06-14 09:26:42 +00:00
parent 3378d26a31
commit 8796cadb56
11 changed files with 956 additions and 179 deletions

View File

@@ -62,6 +62,7 @@ const Invoicing = (() => {
if (name === 'notas') loadNotas();
if (name === 'complementos') loadComplementos();
if (name === 'cancelaciones') loadCancelaciones();
if (name === 'config') loadFacturapiStatus();
}
// ---- Badge helpers ----
@@ -259,6 +260,35 @@ const Invoicing = (() => {
}
}
// ---- Facturapi status (config tab) ----
async function loadFacturapiStatus() {
const container = document.getElementById('facturapi-status');
if (!container) return;
container.innerHTML = 'Cargando...';
try {
const status = await api('/facturapi/status');
if (!status.configured) {
container.innerHTML = `<p style="color:var(--color-error);">Facturapi no configurado. Configura la llave API en Configuración.</p>`;
return;
}
container.innerHTML = `
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-4);">
<div>
<div style="font-size:var(--text-caption);color:var(--color-text-muted);">Organización</div>
<div style="font-weight:var(--font-weight-semibold);">${status.legal_name || '-'}</div>
<div style="font-family:var(--font-mono);font-size:var(--text-body-sm);">${status.tax_id || ''}</div>
</div>
<div>
<div style="font-size:var(--text-caption);color:var(--color-text-muted);">CSD</div>
<div style="font-weight:var(--font-weight-semibold);">${status.has_csd ? '<span style="color:var(--color-success);">Activo</span>' : '<span style="color:var(--color-error);">Pendiente</span>'}</div>
</div>
</div>
`;
} catch (e) {
container.innerHTML = `<p style="color:var(--color-error);">Error: ${e.message}</p>`;
}
}
// ---- Detail modal (uses modalDetalleOverlay) ----
async function showDetail(cfdiId) {
const overlay = document.getElementById('modalDetalleOverlay');
@@ -300,10 +330,16 @@ const Invoicing = (() => {
</div>
</div>
${item.error_message ? `<p style="color:var(--color-error);margin-bottom:var(--space-3);"><strong>Error:</strong> ${escapeHtml(item.error_message)}</p>` : ''}
${(item.xml_signed || item.xml_unsigned) ? `
<div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-2);">Vista previa XML</div>
<pre style="background:var(--color-surface-3); border:1px solid var(--color-border); border-radius:var(--radius-md); padding:var(--space-4); font-family:var(--font-mono); font-size:11px; color:var(--color-text-secondary); overflow-x:auto; max-height:200px; line-height:1.6;">${escapeHtml(item.xml_signed || item.xml_unsigned)}</pre>
` : ''}`;
${(item.xml_signed || item.payload_unsigned) ? `
<div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-2);">${item.xml_signed ? 'Vista previa XML' : 'Payload Facturapi'}</div>
<pre style="background:var(--color-surface-3); border:1px solid var(--color-border); border-radius:var(--radius-md); padding:var(--space-4); font-family:var(--font-mono); font-size:11px; color:var(--color-text-secondary); overflow-x:auto; max-height:200px; line-height:1.6;">${escapeHtml(item.xml_signed || item.payload_unsigned)}</pre>
` : ''}
${item.status === 'stamped' && item.external_id ? `
<div style="display:flex;gap:var(--space-3);margin-top:var(--space-4);">
<a class="btn btn--ghost btn--sm" href="${API}/facturapi/download/${item.id}/xml" target="_blank">Descargar XML</a>
<a class="btn btn--ghost btn--sm" href="${API}/facturapi/download/${item.id}/pdf" target="_blank">Descargar PDF</a>
</div>
` : ''}
}
// Wire the cancel button inside modal footer
@@ -531,7 +567,7 @@ const Invoicing = (() => {
window.notaCreditoPlaceholder = notaCreditoPlaceholder;
return {
switchTab, loadFacturas, loadNotas, loadComplementos, loadCancelaciones,
switchTab, loadFacturas, loadNotas, loadComplementos, loadCancelaciones, loadFacturapiStatus,
showDetail, showCancelModal, confirmCancel, processQueue,
showNewInvoiceModal, closeNewInvoiceModal, submitNewInvoice, notaCreditoPlaceholder,
openGlobalInvoiceModal, previewGlobalInvoice, generateGlobalInvoice,