From 1bde5700359c9c3472f8c453077015f1805ef4a2 Mon Sep 17 00:00:00 2001 From: Horux Dev Date: Fri, 22 May 2026 20:11:03 +0000 Subject: [PATCH] =?UTF-8?q?feat(facturacion):=20fecha=20de=20emisi=C3=B3n?= =?UTF-8?q?=20personalizable=20para=20I,=20E,=20T?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Frontend: input datetime-local visible solo para tipos I, E, T (no P). Default al día actual a las 12:00. Se resetea al cambiar tipo. - Frontend: validación en handleSubmit: fecha ≤ ahora y ≥ ahora-72h - Backend controller: validación idéntica antes de consumir timbre - Backend servicios: pasa campo 'date' al payload de Facturapi cuando viene 'fechaEmision' en el body - Build y deploy exitosos --- .../src/controllers/facturacion.controller.ts | 11 ++++++ .../contribuyente-facturapi.service.ts | 1 + apps/api/src/services/facturapi.service.ts | 1 + apps/web/app/(dashboard)/facturacion/page.tsx | 34 +++++++++++++++++++ apps/web/lib/api/facturacion.ts | 1 + 5 files changed, 48 insertions(+) diff --git a/apps/api/src/controllers/facturacion.controller.ts b/apps/api/src/controllers/facturacion.controller.ts index 3d57e23..1fa9632 100644 --- a/apps/api/src/controllers/facturacion.controller.ts +++ b/apps/api/src/controllers/facturacion.controller.ts @@ -138,6 +138,17 @@ export async function emitir(req: Request, res: Response, next: NextFunction) { } } + // ── Validar fecha de emisión (solo I, E, T) ── + const tipo = req.body.type || 'I'; + if (tipo !== 'P' && req.body.fechaEmision) { + const fecha = new Date(req.body.fechaEmision); + const now = new Date(); + const minDate = new Date(now.getTime() - 72 * 60 * 60 * 1000); + if (isNaN(fecha.getTime()) || fecha > now || fecha < minDate) { + throw new AppError(400, 'La fecha de emisión debe estar entre 72 horas en el pasado y el momento actual'); + } + } + // Reservar timbre — si falla emisión en Facturapi, revertimos abajo const consumedTimbre = await facturapiService.consumeTimbre(tenantId); diff --git a/apps/api/src/services/contribuyente-facturapi.service.ts b/apps/api/src/services/contribuyente-facturapi.service.ts index 9c9803d..dd3e89a 100644 --- a/apps/api/src/services/contribuyente-facturapi.service.ts +++ b/apps/api/src/services/contribuyente-facturapi.service.ts @@ -457,6 +457,7 @@ export async function createInvoiceContribuyente( if (data.series) invoicePayload.series = data.series; if (data.folioNumber) invoicePayload.folio_number = data.folioNumber; + if (data.fechaEmision) invoicePayload.date = data.fechaEmision; if (data.relatedDocuments?.length) { // Estructura SAT 4.0: agrupa N uuids por tipo de relación. Acepta tanto diff --git a/apps/api/src/services/facturapi.service.ts b/apps/api/src/services/facturapi.service.ts index a729d52..1b48e71 100644 --- a/apps/api/src/services/facturapi.service.ts +++ b/apps/api/src/services/facturapi.service.ts @@ -340,6 +340,7 @@ export async function createInvoice( if (data.series) invoiceData.series = data.series; if (data.folioNumber) invoiceData.folio_number = data.folioNumber; + if ((data as any).fechaEmision) invoiceData.date = (data as any).fechaEmision; // Documentos relacionados (Ingreso / Egreso / Pago / Traslado). if (data.relatedDocuments?.length) { diff --git a/apps/web/app/(dashboard)/facturacion/page.tsx b/apps/web/app/(dashboard)/facturacion/page.tsx index e3ae1d7..b8da3e2 100644 --- a/apps/web/app/(dashboard)/facturacion/page.tsx +++ b/apps/web/app/(dashboard)/facturacion/page.tsx @@ -303,6 +303,11 @@ export default function FacturacionPage() { const [serie, setSerie] = useState(''); const [folio, setFolio] = useState(''); const [condiciones, setCondiciones] = useState(''); + const [fechaEmision, setFechaEmision] = useState(() => { + const d = new Date(); + d.setHours(12, 0, 0, 0); + return d.toISOString().slice(0, 16); + }); // Conceptos const [conceptos, setConceptos] = useState([{ ...emptyConcepto }]); @@ -535,6 +540,10 @@ export default function FacturacionPage() { // Resetear conceptos con unidad default según tipo const defaultUnit = tipo === 'T' ? 'H87' : 'E48'; setConceptos([{ ...emptyConcepto, unitKey: defaultUnit }]); + // Resetear fecha de emisión al día actual (12:00) + const d = new Date(); + d.setHours(12, 0, 0, 0); + setFechaEmision(d.toISOString().slice(0, 16)); }; // Unidades de servicio que no aplican para Traslado @@ -651,6 +660,20 @@ export default function FacturacionPage() { if (folio) data.folioNumber = parseInt(folio) || undefined; if (condiciones) data.conditions = condiciones; + // Validar fecha de emisión para I, E, T + if (tipoComprobante !== 'P' && fechaEmision) { + const now = new Date(); + const selected = new Date(fechaEmision); + const minDate = new Date(now.getTime() - 72 * 60 * 60 * 1000); + if (selected > now) { + alert('La fecha de emisión no puede ser a futuro'); return; + } + if (selected < minDate) { + alert('La fecha de emisión no puede ser mayor a 72 horas en el pasado'); return; + } + data.fechaEmision = selected.toISOString(); + } + if (config.needsConceptos) { if (conceptos.some(c => !c.description || !c.productKey)) { alert('Completa todos los conceptos'); return; @@ -1077,6 +1100,17 @@ export default function FacturacionPage() {

)} + {tipoComprobante !== 'P' && ( +
+ + setFechaEmision(e.target.value)} + /> +

Máximo 72 horas en el pasado. No se permiten fechas a futuro.

+
+ )}
setSerie(e.target.value.toUpperCase())} placeholder="P" maxLength={10} /> diff --git a/apps/web/lib/api/facturacion.ts b/apps/web/lib/api/facturacion.ts index 9ba5b2a..a623544 100644 --- a/apps/web/lib/api/facturacion.ts +++ b/apps/web/lib/api/facturacion.ts @@ -69,6 +69,7 @@ export interface InvoiceData { series?: string; folioNumber?: number; conditions?: string; + fechaEmision?: string; } export interface InvoiceResult {