@arela/uploader 1.1.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/.claude/worktrees/agent-multi-profile/.env.template +224 -0
  2. package/.claude/worktrees/agent-multi-profile/.prettierrc +13 -0
  3. package/.claude/worktrees/agent-multi-profile/README.md +405 -0
  4. package/.claude/worktrees/agent-multi-profile/package-lock.json +7096 -0
  5. package/.claude/worktrees/agent-multi-profile/package.json +78 -0
  6. package/.claude/worktrees/agent-multi-profile/scripts/cleanup-ds-store.js +109 -0
  7. package/.claude/worktrees/agent-multi-profile/scripts/cleanup-system-files.js +69 -0
  8. package/.claude/worktrees/agent-multi-profile/scripts/scoring-compare.js +243 -0
  9. package/.claude/worktrees/agent-multi-profile/scripts/scoring-phase4-check.js +96 -0
  10. package/.claude/worktrees/agent-multi-profile/scripts/tests/phase-7-features.test.js +415 -0
  11. package/.claude/worktrees/agent-multi-profile/scripts/tests/signal-handling.test.js +275 -0
  12. package/.claude/worktrees/agent-multi-profile/scripts/tests/smart-watch-integration.test.js +554 -0
  13. package/.claude/worktrees/agent-multi-profile/scripts/tests/watch-service-integration.test.js +584 -0
  14. package/.claude/worktrees/agent-multi-profile/src/commands/AgentCommand.js +229 -0
  15. package/.claude/worktrees/agent-multi-profile/src/commands/AgentInitCommand.js +316 -0
  16. package/.claude/worktrees/agent-multi-profile/src/commands/DatastageCommand.js +164 -0
  17. package/.claude/worktrees/agent-multi-profile/src/commands/GDriveSyncCommand.js +475 -0
  18. package/.claude/worktrees/agent-multi-profile/src/commands/IdentifyCommand.js +708 -0
  19. package/.claude/worktrees/agent-multi-profile/src/commands/PollWorkerCommand.js +169 -0
  20. package/.claude/worktrees/agent-multi-profile/src/commands/PropagateCommand.js +636 -0
  21. package/.claude/worktrees/agent-multi-profile/src/commands/PushCommand.js +743 -0
  22. package/.claude/worktrees/agent-multi-profile/src/commands/ScanCommand.js +722 -0
  23. package/.claude/worktrees/agent-multi-profile/src/commands/UploadCommand.js +587 -0
  24. package/.claude/worktrees/agent-multi-profile/src/commands/WatchCommand.js +1342 -0
  25. package/.claude/worktrees/agent-multi-profile/src/commands/WorkerCommand.js +337 -0
  26. package/.claude/worktrees/agent-multi-profile/src/config/config.js +862 -0
  27. package/.claude/worktrees/agent-multi-profile/src/document-type-shared.js +131 -0
  28. package/.claude/worktrees/agent-multi-profile/src/document-types/_pedimento-shared-extractors.js +348 -0
  29. package/.claude/worktrees/agent-multi-profile/src/document-types/doda-pdf.js +121 -0
  30. package/.claude/worktrees/agent-multi-profile/src/document-types/doda-xml.js +118 -0
  31. package/.claude/worktrees/agent-multi-profile/src/document-types/factura-inter-agencia.js +186 -0
  32. package/.claude/worktrees/agent-multi-profile/src/document-types/facturas-comerciales.js +233 -0
  33. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo-xml.js +372 -0
  34. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo.js +108 -0
  35. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-simplificado.js +76 -0
  36. package/.claude/worktrees/agent-multi-profile/src/document-types/proforma.js +29 -0
  37. package/.claude/worktrees/agent-multi-profile/src/document-types/support-document.js +200 -0
  38. package/.claude/worktrees/agent-multi-profile/src/errors/ErrorHandler.js +278 -0
  39. package/.claude/worktrees/agent-multi-profile/src/errors/ErrorTypes.js +104 -0
  40. package/.claude/worktrees/agent-multi-profile/src/file-detection.js +338 -0
  41. package/.claude/worktrees/agent-multi-profile/src/index.js +890 -0
  42. package/.claude/worktrees/agent-multi-profile/src/scoring/db-matcher-adapter.js +98 -0
  43. package/.claude/worktrees/agent-multi-profile/src/scoring/matchers-seed.js +386 -0
  44. package/.claude/worktrees/agent-multi-profile/src/scoring/scoring-engine.js +251 -0
  45. package/.claude/worktrees/agent-multi-profile/src/services/AdvancedFilterService.js +505 -0
  46. package/.claude/worktrees/agent-multi-profile/src/services/AutoProcessingService.js +749 -0
  47. package/.claude/worktrees/agent-multi-profile/src/services/BenchmarkingService.js +381 -0
  48. package/.claude/worktrees/agent-multi-profile/src/services/DatabaseService.js +2173 -0
  49. package/.claude/worktrees/agent-multi-profile/src/services/DatastageApiService.js +240 -0
  50. package/.claude/worktrees/agent-multi-profile/src/services/ErrorMonitor.js +275 -0
  51. package/.claude/worktrees/agent-multi-profile/src/services/GoogleDriveService.js +217 -0
  52. package/.claude/worktrees/agent-multi-profile/src/services/LoggingService.js +649 -0
  53. package/.claude/worktrees/agent-multi-profile/src/services/MonitoringService.js +401 -0
  54. package/.claude/worktrees/agent-multi-profile/src/services/PerformanceOptimizer.js +511 -0
  55. package/.claude/worktrees/agent-multi-profile/src/services/PipelineApiService.js +274 -0
  56. package/.claude/worktrees/agent-multi-profile/src/services/PipelineJobRunner.js +389 -0
  57. package/.claude/worktrees/agent-multi-profile/src/services/ProfileManager.js +164 -0
  58. package/.claude/worktrees/agent-multi-profile/src/services/ReportingService.js +511 -0
  59. package/.claude/worktrees/agent-multi-profile/src/services/ScanApiService.js +775 -0
  60. package/.claude/worktrees/agent-multi-profile/src/services/SignalHandler.js +255 -0
  61. package/.claude/worktrees/agent-multi-profile/src/services/SmartWatchDatabaseService.js +527 -0
  62. package/.claude/worktrees/agent-multi-profile/src/services/WatchService.js +783 -0
  63. package/.claude/worktrees/agent-multi-profile/src/services/upload/ApiUploadService.js +676 -0
  64. package/.claude/worktrees/agent-multi-profile/src/services/upload/BaseUploadService.js +36 -0
  65. package/.claude/worktrees/agent-multi-profile/src/services/upload/MultiApiUploadService.js +233 -0
  66. package/.claude/worktrees/agent-multi-profile/src/services/upload/SupabaseUploadService.js +148 -0
  67. package/.claude/worktrees/agent-multi-profile/src/services/upload/UploadServiceFactory.js +100 -0
  68. package/.claude/worktrees/agent-multi-profile/src/utils/CleanupManager.js +262 -0
  69. package/.claude/worktrees/agent-multi-profile/src/utils/FileOperations.js +192 -0
  70. package/.claude/worktrees/agent-multi-profile/src/utils/FileSanitizer.js +99 -0
  71. package/.claude/worktrees/agent-multi-profile/src/utils/PathDetector.js +198 -0
  72. package/.claude/worktrees/agent-multi-profile/src/utils/PathNormalizer.js +274 -0
  73. package/.claude/worktrees/agent-multi-profile/src/utils/WatchEventHandler.js +522 -0
  74. package/.claude/worktrees/agent-multi-profile/supabase/migrations/001_create_initial_schema.sql +366 -0
  75. package/.claude/worktrees/agent-multi-profile/supabase/migrations/002_align_with_arela_api_schema.sql +145 -0
  76. package/.claude/worktrees/agent-multi-profile/tests/commands/IdentifyCommand.test.js +570 -0
  77. package/.claude/worktrees/agent-multi-profile/tests/commands/PropagateCommand.test.js +568 -0
  78. package/.claude/worktrees/agent-multi-profile/tests/commands/PushCommand.test.js +754 -0
  79. package/.claude/worktrees/agent-multi-profile/tests/commands/ScanCommand.test.js +382 -0
  80. package/.claude/worktrees/agent-multi-profile/tests/unit/PathAndTableNameGeneration.test.js +1211 -0
  81. package/.claude/worktrees/agent-multi-profile/tests/unit/factura-inter-agencia.test.js +218 -0
  82. package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-completo-xml-matcher.test.js +271 -0
  83. package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-simplificado-matcher.test.js +185 -0
  84. package/.claude/worktrees/agent-multi-profile/tests/unit/scoring-engine.test.js +221 -0
  85. package/README.md +65 -0
  86. package/package.json +1 -1
  87. package/src/commands/AgentCommand.js +210 -0
  88. package/src/commands/AgentInitCommand.js +316 -0
  89. package/src/commands/PollWorkerCommand.js +11 -322
  90. package/src/config/config.js +44 -6
  91. package/src/document-type-shared.js +1 -1
  92. package/src/document-types/pedimento-completo.js +8 -1
  93. package/src/file-detection.js +9 -0
  94. package/src/index.js +55 -0
  95. package/src/scoring/scoring-engine.js +1 -1
  96. package/src/services/LoggingService.js +35 -0
  97. package/src/services/PipelineApiService.js +7 -1
  98. package/src/services/PipelineJobRunner.js +389 -0
  99. package/src/services/ProfileManager.js +164 -0
@@ -0,0 +1,118 @@
1
+ import { FieldResult } from '../document-type-shared.js';
2
+
3
+ /**
4
+ * DODA XML Document Type Definition
5
+ * Detects DODA (Documento de Operación para Despacho Aduanero) in XML format.
6
+ * XML DODAs contain structured data from VUCEM/customs systems.
7
+ * They reside in the SAME directory as the pedimento_simplificado,
8
+ * so within-table propagation handles arela_path assignment.
9
+ */
10
+ export const dodaXmlDefinition = {
11
+ type: 'doda_xml',
12
+ extensions: ['xml'],
13
+ match: (source) => {
14
+ // DODA XML files contain specific XML tags/namespaces
15
+ const xmlMarkers = [
16
+ /documentoOperacion/i,
17
+ /despachoAduanero/i,
18
+ /<doda\b/i,
19
+ /xmlns[^"]*doda/i,
20
+ /VUCEM/i,
21
+ ];
22
+
23
+ // Also check for pedimento-related XML structure
24
+ const pedimentoXmlMarkers = [
25
+ /numPedimento/i,
26
+ /patenteAduanal/i,
27
+ /aduanaDespacho/i,
28
+ /tipoOperacion/i,
29
+ ];
30
+
31
+ const dodaMatches = xmlMarkers.filter((m) => m.test(source)).length;
32
+ const pedimentoMatches = pedimentoXmlMarkers.filter((m) =>
33
+ m.test(source),
34
+ ).length;
35
+
36
+ // Match if: has DODA-specific markers, or combination of pedimento markers with XML structure
37
+ return (
38
+ dodaMatches >= 1 || (pedimentoMatches >= 3 && /<\?xml/i.test(source))
39
+ );
40
+ },
41
+
42
+ extractNumPedimento: (source, fields) => {
43
+ return fields?.find((f) => f.name === 'numPedimento')?.value ?? null;
44
+ },
45
+
46
+ extractPedimentoYear: (source, fields) => {
47
+ const numPedimento = fields?.find((f) => f.name === 'numPedimento')?.value;
48
+ if (!numPedimento) {
49
+ // Try to extract year from date in XML
50
+ const dateMatch = source.match(/(\d{4})-\d{2}-\d{2}/);
51
+ if (dateMatch) {
52
+ return parseInt(dateMatch[1], 10);
53
+ }
54
+ return null;
55
+ }
56
+ const year = parseInt(numPedimento.substring(0, 2), 10);
57
+ return year < 50 ? year + 2000 : year + 1900;
58
+ },
59
+
60
+ extractors: [
61
+ {
62
+ field: 'numPedimento',
63
+ extract: (source) => {
64
+ // Try XML tag format first
65
+ const xmlMatch = source.match(/numPedimento[^>]*>(\d{15})<\/[^>]+>/i);
66
+ if (xmlMatch) {
67
+ return new FieldResult('numPedimento', true, xmlMatch[1]);
68
+ }
69
+
70
+ // Try attribute format
71
+ const attrMatch = source.match(
72
+ /numPedimento[=:"]\s*["']?(\d{15})["']?/i,
73
+ );
74
+ if (attrMatch) {
75
+ return new FieldResult('numPedimento', true, attrMatch[1]);
76
+ }
77
+
78
+ // Fallback: 15-digit pattern
79
+ const fallback = source.match(/\d{2}\s?\d{2}\s?\d{4}\s?\d{7}/);
80
+ return new FieldResult(
81
+ 'numPedimento',
82
+ !!fallback,
83
+ fallback ? fallback[0].replace(/\s/g, '') : null,
84
+ );
85
+ },
86
+ },
87
+ {
88
+ field: 'rfc',
89
+ extract: (source) => {
90
+ // Try XML tag format
91
+ const xmlMatch = source.match(
92
+ /rfc[^>]*>([A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3})<\/[^>]+>/i,
93
+ );
94
+ if (xmlMatch) {
95
+ return new FieldResult('rfc', true, xmlMatch[1]);
96
+ }
97
+
98
+ // Fallback: generic RFC pattern
99
+ const match = source.match(/\b([A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3})\b/);
100
+ return new FieldResult('rfc', !!match, match ? match[1] : null);
101
+ },
102
+ },
103
+ {
104
+ field: 'patente',
105
+ extract: (source) => {
106
+ const match = source.match(/patenteAduanal[^>]*>(\d{4})<\/[^>]+>/i);
107
+ return new FieldResult('patente', !!match, match ? match[1] : null);
108
+ },
109
+ },
110
+ {
111
+ field: 'aduana',
112
+ extract: (source) => {
113
+ const match = source.match(/aduanaDespacho[^>]*>(\d{2,4})<\/[^>]+>/i);
114
+ return new FieldResult('aduana', !!match, match ? match[1] : null);
115
+ },
116
+ },
117
+ ],
118
+ };
@@ -0,0 +1,186 @@
1
+ // NOTE: We intentionally do NOT import `FieldResult` from
2
+ // '../document-type-shared.js' to avoid a circular-import TDZ when this
3
+ // module is imported directly (e.g. from unit tests). `FieldResult` is a
4
+ // plain data-class with shape `{ name, found, value }`, so we construct
5
+ // equivalent plain objects locally.
6
+ const fieldResult = (name, found, value) => ({ name, found, value });
7
+
8
+ /**
9
+ * Factura Inter-Agencia Document Type Definition
10
+ *
11
+ * Detects CFDIs (XML or PDF) issued between customs broker agencies (e.g.,
12
+ * NORCOM ↔ PALCO). These files are dropped into a pedimento folder by the
13
+ * broker but they are NOT part of the customs electronic file (expediente
14
+ * aduanal) — they are inter-agency billing for broker services.
15
+ *
16
+ * Detection rules (ALL required):
17
+ * 1) CFDI markers present (either xml structure or PDF text representation)
18
+ * 2) Both emisor and receptor RFCs belong to the configured agency pair
19
+ * (NAA120215F20 = NORCOM, PCC1008161WA = PALCO) in any direction.
20
+ * 3) At least one concepto with ClaveProdServ 78141502 (Servicios de
21
+ * agentes aduaneros) — confirms the billing is for broker services.
22
+ *
23
+ * IMPORTANT: This matcher MUST be registered BEFORE `facturasComerciales`
24
+ * in document-type-shared.js — both would match a CFDI in a pedimento
25
+ * folder, but inter-agency invoices must take precedence so they are
26
+ * filtered out of the Arela push pipeline (see arela-api
27
+ * NON_PUSHABLE_TYPES_SQL).
28
+ *
29
+ * Currently scope-limited to NORCOM↔PALCO. To widen, move INTER_AGENCIA_RFCS
30
+ * to env config and require ≥2 distinct RFCs from the configured list.
31
+ */
32
+
33
+ /**
34
+ * RFCs of agencies whose mutual invoices should be excluded from the Arela
35
+ * push pipeline. Order is irrelevant — a match is any pair of distinct RFCs
36
+ * from this set appearing as emisor and receptor.
37
+ */
38
+ export const INTER_AGENCIA_RFCS = ['NAA120215F20', 'PCC1008161WA'];
39
+
40
+ const BROKER_SERVICE_CLAVE_PROD_SERV = '78141502';
41
+
42
+ const CFDI_XML_MARKERS = [
43
+ /cfdi:Comprobante/i,
44
+ /xmlns:cfdi/i,
45
+ /TipoDeComprobante/i,
46
+ ];
47
+
48
+ /**
49
+ * Detect that the source represents a CFDI — either as the original XML
50
+ * structure or as text extracted from a printed CFDI (PDF representation).
51
+ *
52
+ * PDF text loses XML tags, so we look for the human-readable equivalents
53
+ * commonly rendered by SAT-style invoice templates ("Folio Fiscal", "Sello
54
+ * Digital del CFDI", "Cadena Original ... Certificacion Digital del SAT").
55
+ */
56
+ function isCfdiContent(source) {
57
+ const xmlHits = CFDI_XML_MARKERS.filter((re) => re.test(source)).length;
58
+ if (xmlHits >= 2) return true;
59
+
60
+ const pdfMarkers = [
61
+ /folio\s*fiscal/i,
62
+ /sello\s*digital\s*del\s*cfdi/i,
63
+ /cadena\s*original.*certificaci[oó]n\s*digital\s*del\s*sat/i,
64
+ /representaci[oó]n\s*impresa\s*de\s*un\s*cfdi/i,
65
+ ];
66
+ return pdfMarkers.filter((re) => re.test(source)).length >= 2;
67
+ }
68
+
69
+ /**
70
+ * Return the subset of INTER_AGENCIA_RFCS that appear in `source`. Matching is
71
+ * case-insensitive and uses word boundaries so substrings inside larger tokens
72
+ * (cert/sello base64) don't produce false positives.
73
+ */
74
+ function findInterAgenciaRfcs(source) {
75
+ const found = new Set();
76
+ for (const rfc of INTER_AGENCIA_RFCS) {
77
+ const re = new RegExp(`\\b${rfc}\\b`, 'i');
78
+ if (re.test(source)) found.add(rfc.toUpperCase());
79
+ }
80
+ return [...found];
81
+ }
82
+
83
+ export const facturaInterAgenciaDefinition = {
84
+ type: 'factura_inter_agencia',
85
+ extensions: ['xml', 'pdf'],
86
+
87
+ match: (source) => {
88
+ if (!isCfdiContent(source)) return false;
89
+
90
+ // Need ≥2 distinct configured RFCs present (one as emisor, one as receptor)
91
+ const rfcsFound = findInterAgenciaRfcs(source);
92
+ if (rfcsFound.length < 2) return false;
93
+
94
+ // Confirm the invoice is for broker services (customs agent services)
95
+ if (!source.includes(BROKER_SERVICE_CLAVE_PROD_SERV)) return false;
96
+
97
+ return true;
98
+ },
99
+
100
+ // Pedimento extraction is optional / informational — these files are
101
+ // excluded from push, so arela_path is never composed. We still extract
102
+ // a pedimento number when present (from the "Referencias" / "Pedimento:"
103
+ // section of the printable CFDI) for auditability.
104
+ extractNumPedimento: (source, fields) => {
105
+ return fields?.find((f) => f.name === 'numPedimento')?.value ?? null;
106
+ },
107
+
108
+ extractPedimentoYear: (source, fields) => {
109
+ const numPedimento = fields?.find((f) => f.name === 'numPedimento')?.value;
110
+ if (numPedimento && numPedimento.length >= 2) {
111
+ const yy = parseInt(numPedimento.substring(0, 2), 10);
112
+ if (!isNaN(yy)) return yy < 50 ? yy + 2000 : yy + 1900;
113
+ }
114
+ return null;
115
+ },
116
+
117
+ extractors: [
118
+ {
119
+ field: 'rfcEmisor',
120
+ extract: (source) => {
121
+ // XML form: <cfdi:Emisor Rfc="..." />
122
+ const xmlMatch = source.match(
123
+ /<[^>]*Emisor[^>]*Rfc\s*=\s*["']([A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3})["']/i,
124
+ );
125
+ if (xmlMatch) return fieldResult('rfcEmisor', true, xmlMatch[1]);
126
+
127
+ // PDF form: "Emisor" section followed by RFC label/value on later lines.
128
+ // We pick the first INTER_AGENCIA RFC that appears in the document.
129
+ const rfcs = findInterAgenciaRfcs(source);
130
+ if (rfcs.length > 0) return fieldResult('rfcEmisor', true, rfcs[0]);
131
+
132
+ return fieldResult('rfcEmisor', false, null);
133
+ },
134
+ },
135
+ {
136
+ field: 'rfcReceptor',
137
+ extract: (source) => {
138
+ const xmlMatch = source.match(
139
+ /<[^>]*Receptor[^>]*Rfc\s*=\s*["']([A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3})["']/i,
140
+ );
141
+ if (xmlMatch) return fieldResult('rfcReceptor', true, xmlMatch[1]);
142
+
143
+ const rfcs = findInterAgenciaRfcs(source);
144
+ if (rfcs.length >= 2) {
145
+ return fieldResult('rfcReceptor', true, rfcs[1]);
146
+ }
147
+ return fieldResult('rfcReceptor', false, null);
148
+ },
149
+ },
150
+ {
151
+ field: 'folio',
152
+ extract: (source) => {
153
+ // CFDI Folio attribute
154
+ const xmlMatch = source.match(/\bFolio\s*=\s*["']([A-Z0-9-]+)["']/i);
155
+ if (xmlMatch) return fieldResult('folio', true, xmlMatch[1]);
156
+
157
+ // PDF: "Numero Folio 012749"
158
+ const pdfMatch = source.match(/Numero\s+Folio\s+([A-Z0-9-]+)/i);
159
+ if (pdfMatch) return fieldResult('folio', true, pdfMatch[1]);
160
+
161
+ return fieldResult('folio', false, null);
162
+ },
163
+ },
164
+ {
165
+ field: 'uuid',
166
+ extract: (source) => {
167
+ const uuidRe =
168
+ /[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/i;
169
+ const m = source.match(uuidRe);
170
+ return fieldResult('uuid', !!m, m ? m[0].toUpperCase() : null);
171
+ },
172
+ },
173
+ {
174
+ field: 'numPedimento',
175
+ extract: (source) => {
176
+ // Printable CFDI "Pedimento: 3458 6000046 Fecha: ..." — recovers an
177
+ // 11-digit pedimento (no YY prefix). Useful for auditability only.
178
+ const m = source.match(/Pedimento:?\s*(\d{4})\s*(\d{7})/i);
179
+ if (m) {
180
+ return fieldResult('numPedimento', true, `${m[1]}${m[2]}`);
181
+ }
182
+ return fieldResult('numPedimento', false, null);
183
+ },
184
+ },
185
+ ],
186
+ };
@@ -0,0 +1,233 @@
1
+ import { FieldResult } from '../document-type-shared.js';
2
+
3
+ /**
4
+ * Facturas Comerciales Document Type Definition
5
+ * Detects commercial invoices (facturas) related to customs operations.
6
+ *
7
+ * CRITICAL: Facturas reside in a DIFFERENT directory than the pedimento_simplificado,
8
+ * creating a different CLI scan table. Cross-table propagation uses `detected_pedimento`
9
+ * to link facturas to their corresponding pedimento and assign arela_path.
10
+ *
11
+ * Supported formats: PDF (scanned invoices), XML (CFDI/electronic invoices)
12
+ */
13
+ export const facturasComerciales = {
14
+ type: 'factura_comercial',
15
+ extensions: ['pdf', 'xml'],
16
+ match: (source) => {
17
+ // CFDI / electronic invoice markers (XML)
18
+ const cfdiMarkers = [
19
+ /cfdi:Comprobante/i,
20
+ /xmlns:cfdi/i,
21
+ /TipoDeComprobante/i,
22
+ /timbreFiscalDigital/i,
23
+ /SelloSAT/i,
24
+ ];
25
+
26
+ // PDF invoice markers
27
+ const invoiceMarkers = [
28
+ /factura\s*(comercial|de\s*venta|de\s*exportaci[oó]n)?/i,
29
+ /commercial\s*invoice/i,
30
+ /invoice\s*number/i,
31
+ /n[uú]mero\s*de\s*factura/i,
32
+ ];
33
+
34
+ // Customs-related invoice context
35
+ const customsContext = [
36
+ /pedimento/i,
37
+ /aduana/i,
38
+ /importaci[oó]n|exportaci[oó]n/i,
39
+ /despacho\s*aduanero/i,
40
+ /fracci[oó]n\s*arancelaria/i,
41
+ ];
42
+
43
+ const cfdiMatches = cfdiMarkers.filter((m) => m.test(source)).length;
44
+ const invoiceMatches = invoiceMarkers.filter((m) => m.test(source)).length;
45
+ const customsMatches = customsContext.filter((m) => m.test(source)).length;
46
+
47
+ // Match if: CFDI structure (>=2 markers), or invoice + customs context
48
+ return cfdiMatches >= 2 || (invoiceMatches >= 1 && customsMatches >= 1);
49
+ },
50
+
51
+ extractNumPedimento: (source, fields) => {
52
+ return fields?.find((f) => f.name === 'numPedimento')?.value ?? null;
53
+ },
54
+
55
+ extractPedimentoYear: (source, fields) => {
56
+ const numPedimento = fields?.find((f) => f.name === 'numPedimento')?.value;
57
+ if (numPedimento) {
58
+ const year = parseInt(numPedimento.substring(0, 2), 10);
59
+ return year < 50 ? year + 2000 : year + 1900;
60
+ }
61
+ // Try invoice date field
62
+ const invoiceDate = fields?.find((f) => f.name === 'invoiceDate')?.value;
63
+ if (invoiceDate) {
64
+ const yearMatch = invoiceDate.match(/^(\d{4})/);
65
+ if (yearMatch) return parseInt(yearMatch[1], 10);
66
+ }
67
+ // Try to extract year from any date in source
68
+ const dateMatch = source.match(/(\d{4})-\d{2}-\d{2}/);
69
+ if (dateMatch) return parseInt(dateMatch[1], 10);
70
+ const mmmMatch = source.match(
71
+ /\d{1,2}\/(?:ene|feb|mar|abr|may|jun|jul|ago|sep|oct|nov|dic)\/(\d{4})/i,
72
+ );
73
+ if (mmmMatch) return parseInt(mmmMatch[1], 10);
74
+ return null;
75
+ },
76
+
77
+ extractors: [
78
+ {
79
+ field: 'numPedimento',
80
+ extract: (source) => {
81
+ // Try dash-separated format: YY-AA-PPPP-NNNNNNN (most common in Mexican import invoices)
82
+ // e.g. "26-07-3429-6016477" — may be wrapped across lines in PDF text
83
+ const dashMatch = source.match(
84
+ /(\d{2})-(\d{2})-(\d{4})-(\d{1,7})\s*(\d*)/,
85
+ );
86
+ if (dashMatch) {
87
+ const lastPart = (dashMatch[4] + dashMatch[5]).substring(0, 7);
88
+ if (lastPart.length === 7) {
89
+ const full = dashMatch[1] + dashMatch[2] + dashMatch[3] + lastPart;
90
+ return new FieldResult('numPedimento', true, full);
91
+ }
92
+ }
93
+
94
+ // Try CFDI XML: NumPedimento attribute or InformacionAduanera
95
+ const cfdiMatch = source.match(
96
+ /(?:NumPedimento|NumeroPedimento)[=:"]\s*["']?(\d{15})["']?/i,
97
+ );
98
+ if (cfdiMatch) {
99
+ return new FieldResult('numPedimento', true, cfdiMatch[1]);
100
+ }
101
+
102
+ // Try InformacionAduanera tag
103
+ const aduaneraMatch = source.match(
104
+ /InformacionAduanera[^>]*NumeroPedimento[=:"]\s*["']?(\d{15})["']?/i,
105
+ );
106
+ if (aduaneraMatch) {
107
+ return new FieldResult('numPedimento', true, aduaneraMatch[1]);
108
+ }
109
+
110
+ // Try generic XML tag
111
+ const xmlMatch = source.match(/pedimento[^>]*>(\d{15})<\/[^>]+>/i);
112
+ if (xmlMatch) {
113
+ return new FieldResult('numPedimento', true, xmlMatch[1]);
114
+ }
115
+
116
+ // Try space-separated near pedimento keyword
117
+ const textMatch = source.match(
118
+ /pedimento[^\d]{0,30}(\d{2}\s?\d{2}\s?\d{4}\s?\d{7})/i,
119
+ );
120
+ if (textMatch) {
121
+ return new FieldResult(
122
+ 'numPedimento',
123
+ true,
124
+ textMatch[1].replace(/\s/g, ''),
125
+ );
126
+ }
127
+
128
+ return new FieldResult('numPedimento', false, null);
129
+ },
130
+ },
131
+ {
132
+ field: 'rfc',
133
+ extract: (source) => {
134
+ // Try CFDI Rfc attribute (emisor)
135
+ const cfdiMatch = source.match(
136
+ /Emisor[^>]*Rfc[=:"]\s*["']?([A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3})["']?/i,
137
+ );
138
+ if (cfdiMatch) {
139
+ return new FieldResult('rfc', true, cfdiMatch[1]);
140
+ }
141
+
142
+ // Try Receptor Rfc (for import invoices, receptor is the importer)
143
+ const receptorMatch = source.match(
144
+ /Receptor[^>]*Rfc[=:"]\s*["']?([A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3})["']?/i,
145
+ );
146
+ if (receptorMatch) {
147
+ return new FieldResult('rfc', true, receptorMatch[1]);
148
+ }
149
+
150
+ // Fallback: generic RFC pattern
151
+ const match = source.match(/\b([A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3})\b/);
152
+ return new FieldResult('rfc', !!match, match ? match[1] : null);
153
+ },
154
+ },
155
+ {
156
+ field: 'invoiceNumber',
157
+ extract: (source) => {
158
+ // CFDI Folio
159
+ const folioMatch = source.match(/Folio[=:"]\s*["']?([A-Z0-9-]+)["']?/i);
160
+ if (folioMatch) {
161
+ return new FieldResult('invoiceNumber', true, folioMatch[1]);
162
+ }
163
+
164
+ // Header line followed by invoice number on next line
165
+ // e.g. "FACTURA FECHA ADUANA...\nMIB260064 02/mar/2026..."
166
+ const headerMatch = source.match(
167
+ /FACTURA\s+FECHA[^\n]*\n([A-Z]{2,5}\d{4,10})/i,
168
+ );
169
+ if (headerMatch) {
170
+ return new FieldResult('invoiceNumber', true, headerMatch[1]);
171
+ }
172
+
173
+ // Text-based invoice number
174
+ const textMatch = source.match(
175
+ /(?:factura|invoice)\s*(?:no\.?|number|#|num\.?)?[:\s]*([A-Z]{2,5}\d{4,10})/i,
176
+ );
177
+ return new FieldResult(
178
+ 'invoiceNumber',
179
+ !!textMatch,
180
+ textMatch ? textMatch[1] : null,
181
+ );
182
+ },
183
+ },
184
+ {
185
+ field: 'invoiceDate',
186
+ extract: (source) => {
187
+ // CFDI Fecha attribute
188
+ const cfdiMatch = source.match(
189
+ /Fecha[=:"]\s*["']?(\d{4}-\d{2}-\d{2})/i,
190
+ );
191
+ if (cfdiMatch) {
192
+ return new FieldResult('invoiceDate', true, cfdiMatch[1]);
193
+ }
194
+
195
+ // DD/MMM/YYYY format (e.g. "02/mar/2026")
196
+ const mmmMonths = {
197
+ ene: '01',
198
+ feb: '02',
199
+ mar: '03',
200
+ abr: '04',
201
+ may: '05',
202
+ jun: '06',
203
+ jul: '07',
204
+ ago: '08',
205
+ sep: '09',
206
+ oct: '10',
207
+ nov: '11',
208
+ dic: '12',
209
+ };
210
+ const mmmMatch = source.match(
211
+ /(\d{1,2})\/(ene|feb|mar|abr|may|jun|jul|ago|sep|oct|nov|dic)\/(\d{4})/i,
212
+ );
213
+ if (mmmMatch) {
214
+ const day = mmmMatch[1].padStart(2, '0');
215
+ const month = mmmMonths[mmmMatch[2].toLowerCase()];
216
+ return new FieldResult(
217
+ 'invoiceDate',
218
+ true,
219
+ `${mmmMatch[3]}-${month}-${day}`,
220
+ );
221
+ }
222
+
223
+ // ISO date
224
+ const isoMatch = source.match(/(\d{4}-\d{2}-\d{2})/);
225
+ return new FieldResult(
226
+ 'invoiceDate',
227
+ !!isoMatch,
228
+ isoMatch ? isoMatch[1] : null,
229
+ );
230
+ },
231
+ },
232
+ ],
233
+ };