@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,372 @@
|
|
|
1
|
+
// VUCEM "consultarPedimentoCompleto" XML matcher.
|
|
2
|
+
//
|
|
3
|
+
// Registered in `document-type-shared.js`. Downstream code
|
|
4
|
+
// (composeArelaPath, arela-api propagation SQL, IdentifyCommand counters)
|
|
5
|
+
// also includes `pedimento_completo_xml`.
|
|
6
|
+
//
|
|
7
|
+
// Filename patterns recognized (try in order — patente extraction):
|
|
8
|
+
// 1) VU_PATENTE_ADUANA_PEDIMENTO.xml → e.g. VU_3429_070_5016101.xml
|
|
9
|
+
// 2) ADUANA-PATENTE-PEDIMENTO.xml → e.g. 670-3806-2002487.xml
|
|
10
|
+
// 3) {15-digit}[_{15-digit}].xml → e.g. 260734296016642_260734296016642.xml
|
|
11
|
+
// If none match, arela_path is left null and propagation fills it from a
|
|
12
|
+
// sibling PDF in the same directory.
|
|
13
|
+
//
|
|
14
|
+
// numPedimento is composed from XML body + filename because the body never
|
|
15
|
+
// carries the 15-digit form: YY|AA|PPPP|NNNNNNN.
|
|
16
|
+
import { FieldResult } from '../document-type-shared.js';
|
|
17
|
+
|
|
18
|
+
// --------------------------- helpers ---------------------------------------
|
|
19
|
+
|
|
20
|
+
function firstTag(source, tag) {
|
|
21
|
+
// Match <ns2:tag>value</ns2:tag> or unprefixed <tag>value</tag>
|
|
22
|
+
const re = new RegExp(
|
|
23
|
+
`<(?:[a-z0-9]+:)?${tag}>([^<]*)</(?:[a-z0-9]+:)?${tag}>`,
|
|
24
|
+
'i',
|
|
25
|
+
);
|
|
26
|
+
const m = source.match(re);
|
|
27
|
+
return m ? m[1].trim() : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function allTagBlocks(source, tag) {
|
|
31
|
+
const re = new RegExp(
|
|
32
|
+
`<(?:[a-z0-9]+:)?${tag}>([\\s\\S]*?)</(?:[a-z0-9]+:)?${tag}>`,
|
|
33
|
+
'gi',
|
|
34
|
+
);
|
|
35
|
+
const out = [];
|
|
36
|
+
let m;
|
|
37
|
+
while ((m = re.exec(source)) !== null) {
|
|
38
|
+
out.push(m[1]);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function pad(value, length) {
|
|
44
|
+
if (value == null) return null;
|
|
45
|
+
return String(value).padStart(length, '0');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Convert a VUCEM `aduanaEntradaSalida.clave` (e.g. "70", "750", "40") to the
|
|
50
|
+
* 2-digit "sección aduanera" prefix used inside the 15-digit pedimento number.
|
|
51
|
+
*
|
|
52
|
+
* VUCEM strips leading zeros from the canonical 3-digit SAT aduana code,
|
|
53
|
+
* so `070` (Ciudad Juárez) arrives as `70`. The pedimento prefix is the
|
|
54
|
+
* first 2 digits of the 3-digit code:
|
|
55
|
+
* `70` → `070` → `07` (Cd. Juárez)
|
|
56
|
+
* `750` → `750` → `75` (Puebla)
|
|
57
|
+
* `40` → `040` → `04` (Lázaro Cárdenas)
|
|
58
|
+
*/
|
|
59
|
+
function aduanaToSeccion(claveValue) {
|
|
60
|
+
if (claveValue == null) return null;
|
|
61
|
+
return pad(claveValue, 3).substring(0, 2);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Try the three known filename patterns and return {patente, aduana, pedimento}
|
|
66
|
+
* with any subset of the fields populated. Returns null if no pattern matches.
|
|
67
|
+
*/
|
|
68
|
+
function parseFilenameParts(filePath) {
|
|
69
|
+
if (!filePath) return null;
|
|
70
|
+
const fileName = filePath.split(/[\\/]/).pop();
|
|
71
|
+
if (!fileName) return null;
|
|
72
|
+
|
|
73
|
+
let m;
|
|
74
|
+
|
|
75
|
+
// 1) VU_PATENTE_ADUANA_PEDIMENTO.xml
|
|
76
|
+
m = fileName.match(/^VU_(\d{4})_(\d{3})_(\d{7})\.xml$/i);
|
|
77
|
+
if (m) {
|
|
78
|
+
return {
|
|
79
|
+
patente: m[1],
|
|
80
|
+
aduana: m[2].substring(0, 2),
|
|
81
|
+
pedimento: m[3],
|
|
82
|
+
year: null,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 2) ADUANA-PATENTE-PEDIMENTO.xml
|
|
87
|
+
m = fileName.match(/^(\d{3})-(\d{4})-(\d{7})\.xml$/i);
|
|
88
|
+
if (m) {
|
|
89
|
+
return {
|
|
90
|
+
patente: m[2],
|
|
91
|
+
aduana: m[1].substring(0, 2),
|
|
92
|
+
pedimento: m[3],
|
|
93
|
+
year: null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 3) 15-digit form YY|AA|PPPP|NNNNNNN
|
|
98
|
+
m = fileName.match(/^(\d{15})(?:_\d{15})?\.xml$/i);
|
|
99
|
+
if (m) {
|
|
100
|
+
const fifteen = m[1];
|
|
101
|
+
return {
|
|
102
|
+
year: fifteen.substring(0, 2),
|
|
103
|
+
aduana: fifteen.substring(2, 4),
|
|
104
|
+
patente: fifteen.substring(4, 8),
|
|
105
|
+
pedimento: fifteen.substring(8, 15),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Capture the YY year from an ISO date string like "2026-05-08-06:00".
|
|
113
|
+
function yyFromIsoDate(iso) {
|
|
114
|
+
if (!iso) return null;
|
|
115
|
+
const m = iso.match(/^(\d{4})-/);
|
|
116
|
+
return m ? m[1].substring(2, 4) : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Find <ns2:fechas> block whose nested <clave> matches `claveValue` and
|
|
120
|
+
// return its <ns2:fecha>. Works for both shapes:
|
|
121
|
+
// <fechas><clave>N</clave><fecha>...</fecha></fechas>
|
|
122
|
+
// <fechas><fecha>...</fecha><tipo><clave>N</clave></tipo></fechas>
|
|
123
|
+
// (firstTag finds the FIRST <clave> in the block — both layouts expose only
|
|
124
|
+
// one clave per fechas entry.)
|
|
125
|
+
function findFechaByClave(source, claveValue) {
|
|
126
|
+
const fechasBlocks = allTagBlocks(source, 'fechas');
|
|
127
|
+
for (const block of fechasBlocks) {
|
|
128
|
+
const clave = firstTag(block, 'clave');
|
|
129
|
+
if (clave === claveValue) {
|
|
130
|
+
const fecha = firstTag(block, 'fecha');
|
|
131
|
+
if (fecha) return fecha;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Fecha de pago de las contribuciones (tipo.clave == 2).
|
|
138
|
+
function findPaymentDate(source) {
|
|
139
|
+
return findFechaByClave(source, '2');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Fecha de presentacion (tipo.clave == 5). This is the authoritative source
|
|
143
|
+
// for the pedimento's YY prefix — a pedimento opened in Dec-2025 but paid in
|
|
144
|
+
// Jan-2026 keeps the `25` prefix, matching what VUCEM stamps in the filename.
|
|
145
|
+
function findPresentationDate(source) {
|
|
146
|
+
return findFechaByClave(source, '5');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// --------------------------- extractors ------------------------------------
|
|
150
|
+
|
|
151
|
+
const rfcExtractor = {
|
|
152
|
+
field: 'rfc',
|
|
153
|
+
extract: (source) => {
|
|
154
|
+
const value = firstTag(source, 'rfc');
|
|
155
|
+
return new FieldResult('rfc', !!value, value);
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const clavePedimentoExtractor = {
|
|
160
|
+
field: 'clavePedimento',
|
|
161
|
+
extract: (source) => {
|
|
162
|
+
// <ns2:claveDocumento><ns2:clave>R1</ns2:clave></ns2:claveDocumento>
|
|
163
|
+
const blocks = allTagBlocks(source, 'claveDocumento');
|
|
164
|
+
const clave = blocks.length > 0 ? firstTag(blocks[0], 'clave') : null;
|
|
165
|
+
return new FieldResult('clavePedimento', !!clave, clave);
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const tipoOperacionExtractor = {
|
|
170
|
+
field: 'tipoOperacion',
|
|
171
|
+
extract: (source) => {
|
|
172
|
+
const blocks = allTagBlocks(source, 'tipoOperacion');
|
|
173
|
+
const desc = blocks.length > 0 ? firstTag(blocks[0], 'descripcion') : null;
|
|
174
|
+
return new FieldResult('tipoOperacion', !!desc, desc);
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const aduanaEntradaSalidaExtractor = {
|
|
179
|
+
field: 'aduanaEntradaSalida',
|
|
180
|
+
extract: (source) => {
|
|
181
|
+
const blocks = allTagBlocks(source, 'aduanaEntradaSalida');
|
|
182
|
+
const clave = blocks.length > 0 ? firstTag(blocks[0], 'clave') : null;
|
|
183
|
+
return new FieldResult(
|
|
184
|
+
'aduanaEntradaSalida',
|
|
185
|
+
!!clave,
|
|
186
|
+
aduanaToSeccion(clave),
|
|
187
|
+
);
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const paymentDateExtractor = {
|
|
192
|
+
field: 'paymentDate',
|
|
193
|
+
extract: (source) => {
|
|
194
|
+
const fecha = findPaymentDate(source);
|
|
195
|
+
return new FieldResult('paymentDate', !!fecha, fecha);
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const presentationDateExtractor = {
|
|
200
|
+
field: 'presentationDate',
|
|
201
|
+
extract: (source) => {
|
|
202
|
+
const fecha = findPresentationDate(source);
|
|
203
|
+
return new FieldResult('presentationDate', !!fecha, fecha);
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const fechaPagoRectificacionExtractor = {
|
|
208
|
+
field: 'fechaPagoRectificacion',
|
|
209
|
+
extract: (source) => {
|
|
210
|
+
const rectBlocks = allTagBlocks(source, 'rectificacion');
|
|
211
|
+
if (rectBlocks.length === 0) {
|
|
212
|
+
return new FieldResult('fechaPagoRectificacion', false, null);
|
|
213
|
+
}
|
|
214
|
+
const fechaPago = firstTag(rectBlocks[0], 'fechaPago');
|
|
215
|
+
return new FieldResult('fechaPagoRectificacion', !!fechaPago, fechaPago);
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const coveExtractor = {
|
|
220
|
+
field: 'cove',
|
|
221
|
+
extract: (source) => {
|
|
222
|
+
// Collect <ns2:numero> values that live inside <ns2:facturas> blocks.
|
|
223
|
+
const facturas = allTagBlocks(source, 'facturas');
|
|
224
|
+
const numeros = [];
|
|
225
|
+
facturas.forEach((block) => {
|
|
226
|
+
const numero = firstTag(block, 'numero');
|
|
227
|
+
if (numero) numeros.push(numero);
|
|
228
|
+
});
|
|
229
|
+
if (numeros.length === 0) {
|
|
230
|
+
return new FieldResult('cove', false, null);
|
|
231
|
+
}
|
|
232
|
+
const unique = [...new Set(numeros)];
|
|
233
|
+
return new FieldResult('cove', true, `[${unique.join(',')}]`);
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const numEDocumentoExtractor = {
|
|
238
|
+
field: 'numEDocumento',
|
|
239
|
+
extract: (source) => {
|
|
240
|
+
// For each <ns2:identificadores> block, check the nested
|
|
241
|
+
// <ns2:claveIdentificador><ns2:clave> value. If it equals 'ED',
|
|
242
|
+
// collect the sibling <ns2:complemento1> value.
|
|
243
|
+
const blocks = allTagBlocks(source, 'identificadores');
|
|
244
|
+
const codes = [];
|
|
245
|
+
blocks.forEach((block) => {
|
|
246
|
+
const claveBlocks = allTagBlocks(block, 'claveIdentificador');
|
|
247
|
+
const clave =
|
|
248
|
+
claveBlocks.length > 0 ? firstTag(claveBlocks[0], 'clave') : null;
|
|
249
|
+
if (clave === 'ED') {
|
|
250
|
+
const complemento = firstTag(block, 'complemento1');
|
|
251
|
+
if (complemento) codes.push(complemento);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
if (codes.length === 0) {
|
|
255
|
+
return new FieldResult('numEDocumento', false, null);
|
|
256
|
+
}
|
|
257
|
+
const unique = [...new Set(codes)];
|
|
258
|
+
return new FieldResult('numEDocumento', true, `[${unique.join(',')}]`);
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// Composed numPedimento — needs the filename + already-extracted fields.
|
|
263
|
+
// We expose it as the LAST extractor so `aduanaEntradaSalida` is available
|
|
264
|
+
// via the `fields` array if the runner exposes it. To stay compatible with
|
|
265
|
+
// the existing extractor signature (which only receives `source`), we
|
|
266
|
+
// re-derive aduanaEntradaSalida inside this extractor and read the filename
|
|
267
|
+
// from a closure populated by `match()` via `extractNumPedimento` below.
|
|
268
|
+
// (See `extractNumPedimento` — that is the canonical place numPedimento is
|
|
269
|
+
// composed for XML.)
|
|
270
|
+
|
|
271
|
+
// --------------------------- definition ------------------------------------
|
|
272
|
+
|
|
273
|
+
export const pedimentoCompletoXmlDefinition = {
|
|
274
|
+
type: 'pedimento_completo_xml',
|
|
275
|
+
extensions: ['xml'],
|
|
276
|
+
|
|
277
|
+
match: (source) => {
|
|
278
|
+
return /consultarPedimentoCompletoRespuesta/i.test(source);
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Resolve to `pedimento_completo_xml` only if payment evidence exists:
|
|
283
|
+
* - When a <rectificacion> block is present, require fechaPagoRectificacion.
|
|
284
|
+
* - Otherwise require paymentDate.
|
|
285
|
+
*/
|
|
286
|
+
resolveType: (fields) => {
|
|
287
|
+
const hasRect = !!fields?.find(
|
|
288
|
+
(f) => f.name === 'fechaPagoRectificacion' && f.found,
|
|
289
|
+
);
|
|
290
|
+
if (hasRect) return 'pedimento_completo_xml';
|
|
291
|
+
|
|
292
|
+
const paymentDate =
|
|
293
|
+
fields?.find((f) => f.name === 'paymentDate' && f.found)?.value ?? null;
|
|
294
|
+
return paymentDate ? 'pedimento_completo_xml' : 'proforma_completo_xml';
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Compose the 15-digit pedimento number from XML body + filename.
|
|
299
|
+
* YY: priority order (most authoritative first):
|
|
300
|
+
* 1) Filename pattern 3 (`{15-digit}.xml`) — VUCEM stamps the correct
|
|
301
|
+
* prefix at export time.
|
|
302
|
+
* 2) Fecha de presentacion (<fechas><clave>5) — the year the pedimento
|
|
303
|
+
* was opened. Authoritative for the YY prefix even when payment
|
|
304
|
+
* crosses calendar year (e.g. opened Dec-2025, paid Jan-2026 → YY=25).
|
|
305
|
+
* 3) Rectification fechaPago (only when no presentation date exists).
|
|
306
|
+
* 4) Payment date (last-resort fallback).
|
|
307
|
+
* AA: from <aduanaEntradaSalida><clave> padded to 2.
|
|
308
|
+
* PPPP: from the filename (any of the three patterns).
|
|
309
|
+
* NNNNNNN: from <pedimento> padded to 7.
|
|
310
|
+
* Returns null if any component cannot be resolved.
|
|
311
|
+
*/
|
|
312
|
+
extractNumPedimento: (source, fields, filePath) => {
|
|
313
|
+
const parts = parseFilenameParts(filePath);
|
|
314
|
+
|
|
315
|
+
const presentation = fields?.find(
|
|
316
|
+
(f) => f.name === 'presentationDate' && f.found,
|
|
317
|
+
)?.value;
|
|
318
|
+
const rect = fields?.find(
|
|
319
|
+
(f) => f.name === 'fechaPagoRectificacion' && f.found,
|
|
320
|
+
)?.value;
|
|
321
|
+
const pay = fields?.find((f) => f.name === 'paymentDate' && f.found)?.value;
|
|
322
|
+
|
|
323
|
+
let yy =
|
|
324
|
+
(parts && parts.year) ||
|
|
325
|
+
yyFromIsoDate(presentation) ||
|
|
326
|
+
yyFromIsoDate(rect) ||
|
|
327
|
+
yyFromIsoDate(pay) ||
|
|
328
|
+
null;
|
|
329
|
+
|
|
330
|
+
const aduanaField = fields?.find(
|
|
331
|
+
(f) => f.name === 'aduanaEntradaSalida',
|
|
332
|
+
)?.value;
|
|
333
|
+
let aduana = aduanaField || (parts && parts.aduana) || null;
|
|
334
|
+
|
|
335
|
+
let patente = parts && parts.patente ? parts.patente : null;
|
|
336
|
+
|
|
337
|
+
const pedimentoBody = firstTag(source, 'pedimento');
|
|
338
|
+
let pedimento = pedimentoBody
|
|
339
|
+
? pad(pedimentoBody, 7)
|
|
340
|
+
: parts && parts.pedimento
|
|
341
|
+
? parts.pedimento
|
|
342
|
+
: null;
|
|
343
|
+
|
|
344
|
+
if (!yy || !aduana || !patente || !pedimento) return null;
|
|
345
|
+
|
|
346
|
+
return `${pad(yy, 2)}${pad(aduana, 2)}${pad(patente, 4)}${pedimento}`;
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
extractPedimentoYear: (source, fields, filePath) => {
|
|
350
|
+
// Reuse extractNumPedimento; the year is its leading 2 digits.
|
|
351
|
+
const num = pedimentoCompletoXmlDefinition.extractNumPedimento(
|
|
352
|
+
source,
|
|
353
|
+
fields,
|
|
354
|
+
filePath,
|
|
355
|
+
);
|
|
356
|
+
if (!num) return null;
|
|
357
|
+
const year = parseInt(num.substring(0, 2), 10);
|
|
358
|
+
return year < 50 ? year + 2000 : year + 1900;
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
extractors: [
|
|
362
|
+
rfcExtractor,
|
|
363
|
+
clavePedimentoExtractor,
|
|
364
|
+
tipoOperacionExtractor,
|
|
365
|
+
aduanaEntradaSalidaExtractor,
|
|
366
|
+
paymentDateExtractor,
|
|
367
|
+
presentationDateExtractor,
|
|
368
|
+
fechaPagoRectificacionExtractor,
|
|
369
|
+
coveExtractor,
|
|
370
|
+
numEDocumentoExtractor,
|
|
371
|
+
],
|
|
372
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
pedimentoYearFromFields,
|
|
3
|
+
sharedPedimentoExtractors,
|
|
4
|
+
} from './_pedimento-shared-extractors.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* "Pedimento Completo" matcher — the 7-page DEF / SEGUNDA / TERCERA copy
|
|
8
|
+
* (and the "CoveFact" variant). This is distinct from `pedimento_simplificado`
|
|
9
|
+
* which uses the "FORMA SIMPLIFICADA DE PEDIMENTO" header.
|
|
10
|
+
*
|
|
11
|
+
* Match strategy: require the structural fields that the long-form pedimento
|
|
12
|
+
* always carries (`NUM. PEDIMENTO:`, `CVE.PEDIMENTO:`, `T.OPER:`) plus at
|
|
13
|
+
* least one of the printed copy markers, while explicitly excluding any
|
|
14
|
+
* document that already declares itself as a "FORMA SIMPLIFICADA".
|
|
15
|
+
*/
|
|
16
|
+
export const pedimentoCompletoDefinition = {
|
|
17
|
+
type: 'pedimento_completo',
|
|
18
|
+
extensions: ['pdf'],
|
|
19
|
+
match: (source) => {
|
|
20
|
+
// Hard exclude: "FORMA SIMPLIFICADA [DE|DEL] PEDIMENTO" is handled by
|
|
21
|
+
// pedimento_simplificado.
|
|
22
|
+
if (/FORMA\s+SIMPLIFICADA\s+DEL?\s+PEDIMENTO/i.test(source)) return false;
|
|
23
|
+
|
|
24
|
+
// Hard exclude: "AVISO CONSOLIDADO" shares the header trio but is a
|
|
25
|
+
// different document type handled by aviso_consolidado.
|
|
26
|
+
if (/AVISO\s+CONSOLIDADO/i.test(source)) return false;
|
|
27
|
+
|
|
28
|
+
// The colon after "T. OPER" is optional — see note in pedimento-simplificado.js.
|
|
29
|
+
const hasHeaderFields =
|
|
30
|
+
/NUM\.?\s*PEDIMENTO:/i.test(source) &&
|
|
31
|
+
/CVE\.?\s*PEDIMENTO:/i.test(source) &&
|
|
32
|
+
/T\.?\s*OPER:?/i.test(source);
|
|
33
|
+
if (hasHeaderFields) {
|
|
34
|
+
const hasCopyMarker =
|
|
35
|
+
/ORIGINAL:\s*ADMINISTRACION GENERAL DE ADUANAS/i.test(source) ||
|
|
36
|
+
/SEGUNDA\s+COPIA/i.test(source) ||
|
|
37
|
+
/TERCERA\s+COPIA/i.test(source) ||
|
|
38
|
+
/COPIA\s+(SIMPLIFICAD[AO])?\s*TRANSPORTISTA/i.test(source) ||
|
|
39
|
+
/DEFINITIVO/i.test(source) ||
|
|
40
|
+
/ANEXO\s+DEL\s+PEDIMENTO/i.test(source) ||
|
|
41
|
+
/\*+FIN\s+DE\s+PEDIMENTO\s*\*+/i.test(source);
|
|
42
|
+
if (hasCopyMarker) return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Fallback clue-counting heuristic for exotic layouts.
|
|
46
|
+
const clues = [
|
|
47
|
+
/\bPEDIMENTO\s*\n.*NUM\.\s*PEDIMENTO:/i,
|
|
48
|
+
/NUM\.\s*PEDIMENTO:\s*T\.OPER:\s*CVE\.PEDIMENTO:\s*REGIMEN:/i,
|
|
49
|
+
/\d{2}\s+\d{2}\s+\d{4}\s+\d{7}\s+[A-Z]{3}\s+[A-Z]{3}/i,
|
|
50
|
+
/CERTIFICACIONES/i,
|
|
51
|
+
/DESTINO\/ORIGEN\s+TIPO\s+CAMBIO\s+PESO\s+BRUTO\s+ADUANA\s+E\/S/i,
|
|
52
|
+
/MEDIOS\s+DE\s+TRANSPORTE/i,
|
|
53
|
+
/DATOS\s+DEL\s+IMPORTADOR\/EXPORTADOR/i,
|
|
54
|
+
/RFC:\s+NOMBRE,\s+DENOMINACION\s+O\s+RAZON\s+SOCIAL:/i,
|
|
55
|
+
/CUADRO\s+DE\s+LIQUIDACION/i,
|
|
56
|
+
/\*\*\*\s+PAGO\s+ELECTRONICO\s+\*\*\*/i,
|
|
57
|
+
/PATENTE:\s+PEDIMENTO:\s+ADUANA:/i,
|
|
58
|
+
/LINEA\s+DE\s+CAPTURA:/i,
|
|
59
|
+
/DATOS\s+DEL\s+PROVEEDOR\s+O\s+COMPRADOR/i,
|
|
60
|
+
/CLAVE\/COMPL\.\s+IDENTIFICADOR/i,
|
|
61
|
+
/ANEXO\s+DEL\s+PEDIMENTO/i,
|
|
62
|
+
/\*+FIN\s+DE\s+PEDIMENTO\s+\*+NUM\.\s+TOTAL\s+DE\s+PARTIDAS:/i,
|
|
63
|
+
/DECLARO\s+BAJO\s+PROTESTA\s+DE\s+DECIR\s+VERDAD/i,
|
|
64
|
+
/PEDIMENTO\s+ELABORADO\s+DE\s+CONFORMIDAD/i,
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const found = clues.filter((clue) => clue.test(source));
|
|
68
|
+
return found.length > clues.length * 0.25;
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resolve the final document type after fields have been extracted.
|
|
73
|
+
* Mirrors the simplificado logic:
|
|
74
|
+
* - R1 rectifications require fechaPagoRectificacion
|
|
75
|
+
* - Everything else requires paymentDate
|
|
76
|
+
* No payment evidence ⇒ proforma_completo.
|
|
77
|
+
*
|
|
78
|
+
* Exception: the "CoveFact" annex (…-CoveFact.pdf) doesn't always print the
|
|
79
|
+
* payment section, so the payment-based demotion misfires and the file gets
|
|
80
|
+
* stuck as proforma_completo (NON_PUSHABLE) forever — the CLI never
|
|
81
|
+
* re-identifies rows that already have a detected_type. The filename is the
|
|
82
|
+
* payment-independent signal for this variant.
|
|
83
|
+
*/
|
|
84
|
+
resolveType: (fields, filePath) => {
|
|
85
|
+
if (/covefact/i.test(filePath ?? '')) return 'pedimento_completo';
|
|
86
|
+
const clavePedimento =
|
|
87
|
+
fields?.find((f) => f.name === 'clavePedimento')?.value ?? null;
|
|
88
|
+
const paymentDate =
|
|
89
|
+
fields?.find((f) => f.name === 'paymentDate' && f.found)?.value ?? null;
|
|
90
|
+
const fechaPagoRectificacion =
|
|
91
|
+
fields?.find((f) => f.name === 'fechaPagoRectificacion' && f.found)
|
|
92
|
+
?.value ?? null;
|
|
93
|
+
|
|
94
|
+
if (clavePedimento === 'R1') {
|
|
95
|
+
return fechaPagoRectificacion
|
|
96
|
+
? 'pedimento_completo'
|
|
97
|
+
: 'proforma_completo';
|
|
98
|
+
}
|
|
99
|
+
return paymentDate ? 'pedimento_completo' : 'proforma_completo';
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
extractNumPedimento: (source, fields) => {
|
|
103
|
+
return fields?.find((f) => f.name === 'numPedimento')?.value ?? null;
|
|
104
|
+
},
|
|
105
|
+
extractPedimentoYear: (source, fields) => pedimentoYearFromFields(fields),
|
|
106
|
+
|
|
107
|
+
extractors: sharedPedimentoExtractors,
|
|
108
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import {
|
|
2
|
+
pedimentoYearFromFields,
|
|
3
|
+
sharedPedimentoExtractors,
|
|
4
|
+
} from './_pedimento-shared-extractors.js';
|
|
5
|
+
|
|
6
|
+
export const pedimentoSimplificadoDefinition = {
|
|
7
|
+
type: 'pedimento_simplificado',
|
|
8
|
+
extensions: ['pdf'],
|
|
9
|
+
match: (source) => {
|
|
10
|
+
// Hard exclude: "AVISO CONSOLIDADO" shares the header trio but is a
|
|
11
|
+
// different document type handled by aviso_consolidado.
|
|
12
|
+
if (/AVISO\s+CONSOLIDADO/i.test(source)) return false;
|
|
13
|
+
|
|
14
|
+
// Fast path: the literal title appears on standard SIMP layouts.
|
|
15
|
+
// Some prevalidators print "FORMA SIMPLIFICADA DEL PEDIMENTO" (with DEL).
|
|
16
|
+
if (/FORMA\s+SIMPLIFICADA\s+DEL?\s+PEDIMENTO/i.test(source)) return true;
|
|
17
|
+
|
|
18
|
+
// Some PDFs (single-page anchors) lack that title but still carry the
|
|
19
|
+
// three pedimento header fields. Treat them as simplificado UNLESS they
|
|
20
|
+
// have the multi-page copy markers that uniquely identify a completo.
|
|
21
|
+
// NOTE: the colon after "T. OPER" is optional — many printable layouts
|
|
22
|
+
// render OPER as a table-header label with the value in the next cell.
|
|
23
|
+
const hasHeaderFields =
|
|
24
|
+
/NUM\.?\s*PEDIMENTO:/i.test(source) &&
|
|
25
|
+
/CVE\.?\s*PEDIMENTO:/i.test(source) &&
|
|
26
|
+
/T\.?\s*OPER:?/i.test(source);
|
|
27
|
+
if (!hasHeaderFields) return false;
|
|
28
|
+
|
|
29
|
+
const hasCompletoCopyMarker =
|
|
30
|
+
/ORIGINAL:\s*ADMINISTRACION GENERAL DE ADUANAS/i.test(source) ||
|
|
31
|
+
/SEGUNDA\s+COPIA/i.test(source) ||
|
|
32
|
+
/TERCERA\s+COPIA/i.test(source) ||
|
|
33
|
+
/COPIA\s+(SIMPLIFICAD[AO])?\s*TRANSPORTISTA/i.test(source) ||
|
|
34
|
+
/ANEXO\s+DEL\s+PEDIMENTO/i.test(source) ||
|
|
35
|
+
/\*+FIN\s+DE\s+PEDIMENTO\s*\*+/i.test(source);
|
|
36
|
+
if (hasCompletoCopyMarker) return false;
|
|
37
|
+
|
|
38
|
+
// Exclude COVE/eDocument forms that may reference a pedimento in their body.
|
|
39
|
+
if (/COMPROBANTE\s+DE\s+VALOR\s+ELECTR[ÓO]NICO/i.test(source)) return false;
|
|
40
|
+
if (/\bCOVE\b\s*:/i.test(source) && !/PAGO/i.test(source)) return false;
|
|
41
|
+
|
|
42
|
+
return true;
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the final document type after fields have been extracted.
|
|
47
|
+
* A document is a "pedimento_simplificado" only if it has been paid:
|
|
48
|
+
* - For clavePedimento = 'R1': must have fechaPagoRectificacion
|
|
49
|
+
* - Otherwise: must have paymentDate
|
|
50
|
+
* If no payment evidence is found, it's a "proforma".
|
|
51
|
+
*
|
|
52
|
+
* @param {import('../document-type-shared.js').FieldResult[]} fields
|
|
53
|
+
* @returns {string} - 'pedimento_simplificado' or 'proforma'
|
|
54
|
+
*/
|
|
55
|
+
resolveType: (fields) => {
|
|
56
|
+
const clavePedimento =
|
|
57
|
+
fields?.find((f) => f.name === 'clavePedimento')?.value ?? null;
|
|
58
|
+
const paymentDate =
|
|
59
|
+
fields?.find((f) => f.name === 'paymentDate' && f.found)?.value ?? null;
|
|
60
|
+
const fechaPagoRectificacion =
|
|
61
|
+
fields?.find((f) => f.name === 'fechaPagoRectificacion' && f.found)
|
|
62
|
+
?.value ?? null;
|
|
63
|
+
|
|
64
|
+
if (clavePedimento === 'R1') {
|
|
65
|
+
return fechaPagoRectificacion ? 'pedimento_simplificado' : 'proforma';
|
|
66
|
+
}
|
|
67
|
+
return paymentDate ? 'pedimento_simplificado' : 'proforma';
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
extractNumPedimento: (source, fields) => {
|
|
71
|
+
return fields?.find((f) => f.name === 'numPedimento')?.value ?? null;
|
|
72
|
+
},
|
|
73
|
+
extractPedimentoYear: (source, fields) => pedimentoYearFromFields(fields),
|
|
74
|
+
|
|
75
|
+
extractors: sharedPedimentoExtractors,
|
|
76
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { FieldResult } from '../document-type-shared.js';
|
|
2
|
+
import { pedimentoSimplificadoDefinition } from './pedimento-simplificado.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Proforma Document Type Definition
|
|
6
|
+
*
|
|
7
|
+
* A proforma is essentially the same document as a pedimento simplificado,
|
|
8
|
+
* but it has NOT been paid yet. It shares the same extractors and structure.
|
|
9
|
+
*
|
|
10
|
+
* Classification logic:
|
|
11
|
+
* - If clavePedimento = 'R1': needs fechaPagoRectificacion to be pedimento_simplificado
|
|
12
|
+
* - Otherwise: needs paymentDate to be pedimento_simplificado
|
|
13
|
+
* - If neither payment field is found → it's a proforma
|
|
14
|
+
*/
|
|
15
|
+
export const proformaDefinition = {
|
|
16
|
+
type: 'proforma',
|
|
17
|
+
extensions: ['pdf'],
|
|
18
|
+
|
|
19
|
+
// Same content marker as pedimento simplificado (accepts "DE" or "DEL").
|
|
20
|
+
match: (source) => {
|
|
21
|
+
return /FORMA\s+SIMPLIFICADA\s+DEL?\s+PEDIMENTO/i.test(source);
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
extractNumPedimento: pedimentoSimplificadoDefinition.extractNumPedimento,
|
|
25
|
+
extractPedimentoYear: pedimentoSimplificadoDefinition.extractPedimentoYear,
|
|
26
|
+
|
|
27
|
+
// Reuse all extractors from pedimento simplificado
|
|
28
|
+
extractors: pedimentoSimplificadoDefinition.extractors,
|
|
29
|
+
};
|