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.