feat(facturacion): fecha de emisión personalizable para I, E, T
- 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
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<ConceptoForm[]>([{ ...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() {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{tipoComprobante !== 'P' && (
|
||||
<div className="space-y-2">
|
||||
<Label>Fecha de Emisión</Label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={fechaEmision}
|
||||
onChange={e => setFechaEmision(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">Máximo 72 horas en el pasado. No se permiten fechas a futuro.</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<Label>Serie (opcional)</Label>
|
||||
<Input value={serie} onChange={e => setSerie(e.target.value.toUpperCase())} placeholder="P" maxLength={10} />
|
||||
|
||||
@@ -69,6 +69,7 @@ export interface InvoiceData {
|
||||
series?: string;
|
||||
folioNumber?: number;
|
||||
conditions?: string;
|
||||
fechaEmision?: string;
|
||||
}
|
||||
|
||||
export interface InvoiceResult {
|
||||
|
||||
Reference in New Issue
Block a user