feat(sat): factura global + fecha_efectiva, fallback tenant-contribuyente, fix anio_global typo

Factura Global & fecha_efectiva:
- Migracion 045_factura_global.sql: periodicidad, meses_global, año_global, fecha_efectiva
- sat-parser.service.ts: extrae InformacionGlobal del XML
- sat.service.ts: calcFechaEfectiva con soporte bimestral (periodicidad 05)
- metricas-compute, dashboard, impuestos, cfdi, export, conciliacion, alertas:
  reemplaza fecha_emision-1h por COALESCE(fecha_efectiva, fecha_emision-1h)
- Script recalc-metricas.ts para recalculo manual

Fallback datos fiscales tenant → contribuyente:
- contribuyente.service.ts: fetchTenantFiscalData + mergeContribuyenteWithTenant
  rellena regimenFiscal, codigoPostal y domicilio cuando el contribuyente
  tiene el mismo RFC que el tenant y sus campos estan vacios
- contribuyente.controller.ts y contribuyente-config.controller.ts:
  pasan req.user!.tenantId al servicio

Fix critico SAT sync:
- sat.service.ts: anio_global → año_global en INSERT/UPDATE de CFDIs
  (la migracion creo 'año_global' con tilde; el codigo usaba 'anio_global',
   causando fallo en 100% de inserciones de CFDI)
- determineChunkMonths: salta sondeo si existe job previo con requestIds
- MAX_POLL_ATTEMPTS: 45 → 500 (~8h) para syncs iniciales grandes

Docs:
- docs/sessions/2026-05-22-factura-global-contribuyente-fallback.md
This commit is contained in:
Horux Dev
2026-05-22 15:52:10 +00:00
parent ba6004ebd6
commit 46846200da
33 changed files with 1128 additions and 171 deletions

View File

@@ -5,7 +5,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query';
import { DashboardShell } from '@/components/layouts/dashboard-shell';
import { Card, CardContent, CardHeader, CardTitle, Button, SortableHeader, Input } from '@horux/shared-ui';
import { apiClient } from '@/lib/api/client';
import { formatCurrency } from '@/lib/utils';
import { formatCurrency, toCfdiDate } from '@/lib/utils';
import { exportToExcel } from '@/lib/export-excel';
import { useTableSort } from '@horux/shared-ui';
import { CfdiViewerModal } from '@/components/cfdi/cfdi-viewer-modal';
@@ -29,7 +29,7 @@ const EXCEL_COLUMNS = [
function prepareRows(data: any[]) {
return data.map((c) => ({
...c,
_fecha: new Date(c.fechaEmision).toLocaleDateString('es-MX'),
_fecha: toCfdiDate(c.fechaEmision).toLocaleDateString('es-MX'),
_totalMxn: Number(c.totalMxn || 0),
regimenReceptor: c.regimenReceptor || c.regimenFiscalReceptor || '',
}));
@@ -91,10 +91,10 @@ export default function DiscrepanciaRegimenPage() {
let filtered = data;
if (fechaDesde) {
filtered = filtered.filter(c => c.fechaEmision >= fechaDesde);
filtered = filtered.filter(c => toCfdiDate(c.fechaEmision).toISOString() >= fechaDesde);
}
if (fechaHasta) {
filtered = filtered.filter(c => c.fechaEmision <= fechaHasta + 'T23:59:59');
filtered = filtered.filter(c => toCfdiDate(c.fechaEmision).toISOString() <= fechaHasta + 'T23:59:59');
}
if (regimenFilter) {
filtered = filtered.filter((c: any) => (c.regimenReceptor || c.regimenFiscalReceptor) === regimenFilter);
@@ -106,7 +106,7 @@ export default function DiscrepanciaRegimenPage() {
const { sortedData, toggleSort, getSortIndicator } = useTableSort<Cfdi, 'fecha' | 'total'>(
visibleData,
{
fecha: (c) => new Date(c.fechaEmision).getTime(),
fecha: (c) => toCfdiDate(c.fechaEmision).getTime(),
total: (c) => Number(c.totalMxn || 0),
},
'fecha',
@@ -311,7 +311,7 @@ export default function DiscrepanciaRegimenPage() {
</button>
</td>
<td className="py-3 font-mono text-xs">{cfdi.uuid?.substring(0, 8)}</td>
<td className="py-3">{new Date(cfdi.fechaEmision).toLocaleDateString('es-MX')}</td>
<td className="py-3">{toCfdiDate(cfdi.fechaEmision).toLocaleDateString('es-MX')}</td>
<td className="py-3 font-mono text-xs">{cfdi.rfcEmisor}</td>
<td className="py-3 truncate max-w-[200px]">{cfdi.nombreEmisor}</td>
<td className="py-3 font-mono font-bold text-destructive">{cfdi.regimenReceptor}</td>