@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.
- package/.claude/worktrees/agent-multi-profile/.env.template +224 -0
- package/.claude/worktrees/agent-multi-profile/.prettierrc +13 -0
- package/.claude/worktrees/agent-multi-profile/README.md +405 -0
- package/.claude/worktrees/agent-multi-profile/package-lock.json +7096 -0
- package/.claude/worktrees/agent-multi-profile/package.json +78 -0
- package/.claude/worktrees/agent-multi-profile/scripts/cleanup-ds-store.js +109 -0
- package/.claude/worktrees/agent-multi-profile/scripts/cleanup-system-files.js +69 -0
- package/.claude/worktrees/agent-multi-profile/scripts/scoring-compare.js +243 -0
- package/.claude/worktrees/agent-multi-profile/scripts/scoring-phase4-check.js +96 -0
- package/.claude/worktrees/agent-multi-profile/scripts/tests/phase-7-features.test.js +415 -0
- package/.claude/worktrees/agent-multi-profile/scripts/tests/signal-handling.test.js +275 -0
- package/.claude/worktrees/agent-multi-profile/scripts/tests/smart-watch-integration.test.js +554 -0
- package/.claude/worktrees/agent-multi-profile/scripts/tests/watch-service-integration.test.js +584 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/AgentCommand.js +229 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/AgentInitCommand.js +316 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/DatastageCommand.js +164 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/GDriveSyncCommand.js +475 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/IdentifyCommand.js +708 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/PollWorkerCommand.js +169 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/PropagateCommand.js +636 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/PushCommand.js +743 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/ScanCommand.js +722 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/UploadCommand.js +587 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/WatchCommand.js +1342 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/WorkerCommand.js +337 -0
- package/.claude/worktrees/agent-multi-profile/src/config/config.js +862 -0
- package/.claude/worktrees/agent-multi-profile/src/document-type-shared.js +131 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/_pedimento-shared-extractors.js +348 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/doda-pdf.js +121 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/doda-xml.js +118 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/factura-inter-agencia.js +186 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/facturas-comerciales.js +233 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo-xml.js +372 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo.js +108 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-simplificado.js +76 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/proforma.js +29 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/support-document.js +200 -0
- package/.claude/worktrees/agent-multi-profile/src/errors/ErrorHandler.js +278 -0
- package/.claude/worktrees/agent-multi-profile/src/errors/ErrorTypes.js +104 -0
- package/.claude/worktrees/agent-multi-profile/src/file-detection.js +338 -0
- package/.claude/worktrees/agent-multi-profile/src/index.js +890 -0
- package/.claude/worktrees/agent-multi-profile/src/scoring/db-matcher-adapter.js +98 -0
- package/.claude/worktrees/agent-multi-profile/src/scoring/matchers-seed.js +386 -0
- package/.claude/worktrees/agent-multi-profile/src/scoring/scoring-engine.js +251 -0
- package/.claude/worktrees/agent-multi-profile/src/services/AdvancedFilterService.js +505 -0
- package/.claude/worktrees/agent-multi-profile/src/services/AutoProcessingService.js +749 -0
- package/.claude/worktrees/agent-multi-profile/src/services/BenchmarkingService.js +381 -0
- package/.claude/worktrees/agent-multi-profile/src/services/DatabaseService.js +2173 -0
- package/.claude/worktrees/agent-multi-profile/src/services/DatastageApiService.js +240 -0
- package/.claude/worktrees/agent-multi-profile/src/services/ErrorMonitor.js +275 -0
- package/.claude/worktrees/agent-multi-profile/src/services/GoogleDriveService.js +217 -0
- package/.claude/worktrees/agent-multi-profile/src/services/LoggingService.js +649 -0
- package/.claude/worktrees/agent-multi-profile/src/services/MonitoringService.js +401 -0
- package/.claude/worktrees/agent-multi-profile/src/services/PerformanceOptimizer.js +511 -0
- package/.claude/worktrees/agent-multi-profile/src/services/PipelineApiService.js +274 -0
- package/.claude/worktrees/agent-multi-profile/src/services/PipelineJobRunner.js +389 -0
- package/.claude/worktrees/agent-multi-profile/src/services/ProfileManager.js +164 -0
- package/.claude/worktrees/agent-multi-profile/src/services/ReportingService.js +511 -0
- package/.claude/worktrees/agent-multi-profile/src/services/ScanApiService.js +775 -0
- package/.claude/worktrees/agent-multi-profile/src/services/SignalHandler.js +255 -0
- package/.claude/worktrees/agent-multi-profile/src/services/SmartWatchDatabaseService.js +527 -0
- package/.claude/worktrees/agent-multi-profile/src/services/WatchService.js +783 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/ApiUploadService.js +676 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/BaseUploadService.js +36 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/MultiApiUploadService.js +233 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/SupabaseUploadService.js +148 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/UploadServiceFactory.js +100 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/CleanupManager.js +262 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/FileOperations.js +192 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/FileSanitizer.js +99 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/PathDetector.js +198 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/PathNormalizer.js +274 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/WatchEventHandler.js +522 -0
- package/.claude/worktrees/agent-multi-profile/supabase/migrations/001_create_initial_schema.sql +366 -0
- package/.claude/worktrees/agent-multi-profile/supabase/migrations/002_align_with_arela_api_schema.sql +145 -0
- package/.claude/worktrees/agent-multi-profile/tests/commands/IdentifyCommand.test.js +570 -0
- package/.claude/worktrees/agent-multi-profile/tests/commands/PropagateCommand.test.js +568 -0
- package/.claude/worktrees/agent-multi-profile/tests/commands/PushCommand.test.js +754 -0
- package/.claude/worktrees/agent-multi-profile/tests/commands/ScanCommand.test.js +382 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/PathAndTableNameGeneration.test.js +1211 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/factura-inter-agencia.test.js +218 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-completo-xml-matcher.test.js +271 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-simplificado-matcher.test.js +185 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/scoring-engine.test.js +221 -0
- package/README.md +65 -0
- package/package.json +1 -1
- package/src/commands/AgentCommand.js +210 -0
- package/src/commands/AgentInitCommand.js +316 -0
- package/src/commands/PollWorkerCommand.js +11 -322
- package/src/config/config.js +44 -6
- package/src/document-type-shared.js +1 -1
- package/src/document-types/pedimento-completo.js +8 -1
- package/src/file-detection.js +9 -0
- package/src/index.js +55 -0
- package/src/scoring/scoring-engine.js +1 -1
- package/src/services/LoggingService.js +35 -0
- package/src/services/PipelineApiService.js +7 -1
- package/src/services/PipelineJobRunner.js +389 -0
- 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
|
+
};
|