@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,131 @@
|
|
|
1
|
+
// Import all document type definitions
|
|
2
|
+
import { dodaPdfDefinition } from './document-types/doda-pdf.js';
|
|
3
|
+
import { dodaXmlDefinition } from './document-types/doda-xml.js';
|
|
4
|
+
import { facturaInterAgenciaDefinition } from './document-types/factura-inter-agencia.js';
|
|
5
|
+
import { facturasComerciales } from './document-types/facturas-comerciales.js';
|
|
6
|
+
import { pedimentoCompletoXmlDefinition } from './document-types/pedimento-completo-xml.js';
|
|
7
|
+
import { pedimentoCompletoDefinition } from './document-types/pedimento-completo.js';
|
|
8
|
+
import { pedimentoSimplificadoDefinition } from './document-types/pedimento-simplificado.js';
|
|
9
|
+
import { proformaDefinition } from './document-types/proforma.js';
|
|
10
|
+
import { supportDocumentDefinition } from './document-types/support-document.js';
|
|
11
|
+
|
|
12
|
+
// Document type definitions and extraction utilities
|
|
13
|
+
// Ported from TypeScript to JavaScript for Node.js
|
|
14
|
+
|
|
15
|
+
export class FieldResult {
|
|
16
|
+
constructor(name, found, value) {
|
|
17
|
+
this.name = name;
|
|
18
|
+
this.found = found;
|
|
19
|
+
this.value = value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class DocumentTypeDefinition {
|
|
24
|
+
constructor(
|
|
25
|
+
type,
|
|
26
|
+
extensions,
|
|
27
|
+
match,
|
|
28
|
+
extractors,
|
|
29
|
+
extractNumPedimento,
|
|
30
|
+
extractPedimentoYear,
|
|
31
|
+
) {
|
|
32
|
+
this.type = type;
|
|
33
|
+
this.extensions = extensions;
|
|
34
|
+
this.match = match;
|
|
35
|
+
this.extractors = extractors;
|
|
36
|
+
this.extractNumPedimento = extractNumPedimento;
|
|
37
|
+
this.extractPedimentoYear = extractPedimentoYear;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Registry of all document types
|
|
42
|
+
// pedimentoSimplificadoDefinition is the primary matcher for "FORMA SIMPLIFICADA" documents.
|
|
43
|
+
// After field extraction, resolveType() differentiates between pedimento_simplificado (paid) and proforma (unpaid).
|
|
44
|
+
// proformaDefinition is kept as reference but not used directly in the registry since resolution is handled post-extraction.
|
|
45
|
+
const documentTypes = [
|
|
46
|
+
pedimentoSimplificadoDefinition,
|
|
47
|
+
pedimentoCompletoDefinition,
|
|
48
|
+
pedimentoCompletoXmlDefinition,
|
|
49
|
+
supportDocumentDefinition,
|
|
50
|
+
dodaPdfDefinition,
|
|
51
|
+
dodaXmlDefinition,
|
|
52
|
+
// factura_inter_agencia MUST be evaluated BEFORE facturasComerciales
|
|
53
|
+
// because a NORCOM↔PALCO CFDI would also match the generic commercial
|
|
54
|
+
// invoice matcher. First match wins (see extractDocumentFields).
|
|
55
|
+
facturaInterAgenciaDefinition,
|
|
56
|
+
facturasComerciales,
|
|
57
|
+
// Add more document types here as needed
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Extract document fields from text content
|
|
62
|
+
* @param {string} source - The text content to analyze
|
|
63
|
+
* @param {string} fileExtension - File extension for context
|
|
64
|
+
* @param {string} filePath - File path for context
|
|
65
|
+
* @returns {[string|null, FieldResult[], string|null, number|null]} - [detectedType, fields, pedimento, year]
|
|
66
|
+
*/
|
|
67
|
+
export function extractDocumentFields(source, fileExtension, filePath) {
|
|
68
|
+
if (!source || typeof source !== 'string') {
|
|
69
|
+
return [null, [], null, null];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Try to match against each document type
|
|
73
|
+
for (const docType of documentTypes) {
|
|
74
|
+
// Check if file extension matches
|
|
75
|
+
if (
|
|
76
|
+
fileExtension &&
|
|
77
|
+
!docType.extensions.includes(fileExtension.toLowerCase())
|
|
78
|
+
) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Test if content matches this document type
|
|
83
|
+
if (docType.match(source)) {
|
|
84
|
+
console.log(`✅ Matched document type: ${docType.type}`);
|
|
85
|
+
|
|
86
|
+
// Extract all fields
|
|
87
|
+
const fields = [];
|
|
88
|
+
for (const extractor of docType.extractors) {
|
|
89
|
+
try {
|
|
90
|
+
const result = extractor.extract(source);
|
|
91
|
+
fields.push(result);
|
|
92
|
+
if (result.found) {
|
|
93
|
+
console.log(` - ${result.name}: ${result.value}`);
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error(`Error extracting field ${extractor.field}:`, error);
|
|
97
|
+
fields.push(new FieldResult(extractor.field, false, null));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Resolve final type if the definition supports it (e.g., pedimento_simplificado vs proforma)
|
|
102
|
+
const resolvedType = docType.resolveType
|
|
103
|
+
? docType.resolveType(fields, filePath)
|
|
104
|
+
: docType.type;
|
|
105
|
+
|
|
106
|
+
console.log(` → Resolved type: ${resolvedType}`);
|
|
107
|
+
|
|
108
|
+
// Extract pedimento number and year. `filePath` is forwarded so XML
|
|
109
|
+
// matchers (which compose numPedimento from the filename) can use it.
|
|
110
|
+
const pedimento = docType.extractNumPedimento
|
|
111
|
+
? docType.extractNumPedimento(source, fields, filePath)
|
|
112
|
+
: null;
|
|
113
|
+
const year = docType.extractPedimentoYear
|
|
114
|
+
? docType.extractPedimentoYear(source, fields, filePath)
|
|
115
|
+
: null;
|
|
116
|
+
|
|
117
|
+
// Ensure downstream code (composeArelaPath) sees `numPedimento` as a
|
|
118
|
+
// field. PDF matchers add it via an explicit extractor; XML matchers
|
|
119
|
+
// compose it externally via extractNumPedimento. Backfill so both paths
|
|
120
|
+
// expose the same shape.
|
|
121
|
+
if (pedimento && !fields.some((f) => f.name === 'numPedimento')) {
|
|
122
|
+
fields.push(new FieldResult('numPedimento', true, pedimento));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return [resolvedType, fields, pedimento, year];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log('❓ No document type matched');
|
|
130
|
+
return [null, [], null, null];
|
|
131
|
+
}
|
package/.claude/worktrees/agent-multi-profile/src/document-types/_pedimento-shared-extractors.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
// Shared PDF extractors used by both `pedimento_simplificado` and
|
|
2
|
+
// `pedimento_completo` matchers. Keeping the regexes in a single module
|
|
3
|
+
// prevents drift between the two pedimento variants.
|
|
4
|
+
import { FieldResult } from '../document-type-shared.js';
|
|
5
|
+
|
|
6
|
+
// 1) Número de Pedimento (15 digits, possibly separated by spaces)
|
|
7
|
+
export const numPedimentoExtractor = {
|
|
8
|
+
field: 'numPedimento',
|
|
9
|
+
extract: (source) => {
|
|
10
|
+
const match = source.match(/\d{2}\s?\d{2}\s?\d{4}\s?\d{7}/);
|
|
11
|
+
return new FieldResult(
|
|
12
|
+
'numPedimento',
|
|
13
|
+
!!match,
|
|
14
|
+
match ? match[0].replace(/\s/g, '') : null,
|
|
15
|
+
);
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// 2) Tipo de Operación: 3 chars after the pedimento number
|
|
20
|
+
export const tipoOperacionExtractor = {
|
|
21
|
+
field: 'tipoOperacion',
|
|
22
|
+
extract: (source) => {
|
|
23
|
+
const match = source.match(/\d{2}\s+\d{2}\s+\d{4}\s+\d{7}\s+([A-Z]{3})/);
|
|
24
|
+
return new FieldResult('tipoOperacion', !!match, match ? match[1] : null);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// 3) Clave de Pedimento: 2 chars after tipoOperacion (multiple layout patterns)
|
|
29
|
+
export const clavePedimentoExtractor = {
|
|
30
|
+
field: 'clavePedimento',
|
|
31
|
+
extract: (source) => {
|
|
32
|
+
const patterns = [
|
|
33
|
+
// Standard spaced layout: "22 07 3429 2002089 EXP RT"
|
|
34
|
+
/\d{2}\s+\d{2}\s+\d{4}\s+\d{7}\s+[A-Z]{3}\s+([A-Z0-9]{2})\b/,
|
|
35
|
+
// Concatenated 15-digit layout: "260734296013645 EXP RT"
|
|
36
|
+
/\d{15}\s+[A-Z]{3}\s+([A-Z0-9]{2})\b/,
|
|
37
|
+
// Fallback: T.OPER keyword followed by 2-char clave
|
|
38
|
+
/\b(?:EXP|IMP|TRA|TRN)\s+([A-Z][A-Z0-9])\b/,
|
|
39
|
+
// Explicit label
|
|
40
|
+
/CVE\.?\s*PED(?:IMENTO)?[^A-Z0-9]{0,60}?\b([A-Z][A-Z0-9])\b/i,
|
|
41
|
+
];
|
|
42
|
+
for (const re of patterns) {
|
|
43
|
+
const m = source.match(re);
|
|
44
|
+
if (m) return new FieldResult('clavePedimento', true, m[1]);
|
|
45
|
+
}
|
|
46
|
+
return new FieldResult('clavePedimento', false, null);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// 4) Aduana E/S: 3-digit code on the peso-bruto line
|
|
51
|
+
// Fallback A: allow 2-digit code (some SIMP layouts omit the leading zero).
|
|
52
|
+
// Fallback B: derive the 2-digit customs-office code from positions 2-3 of
|
|
53
|
+
// numPedimento (e.g. "260734296013645" → "07"), which is what the
|
|
54
|
+
// arela_path formula uses after padStart(2,'0').
|
|
55
|
+
export const aduanaEntradaSalidaExtractor = {
|
|
56
|
+
field: 'aduanaEntradaSalida',
|
|
57
|
+
extract: (source) => {
|
|
58
|
+
// Primary: 3-digit aduana code at end of peso-bruto line
|
|
59
|
+
const m3 = source.match(/^\s*\d+\s+[\d,.]+\s+(\d{3})\s*$/m);
|
|
60
|
+
if (m3) return new FieldResult('aduanaEntradaSalida', true, m3[1]);
|
|
61
|
+
|
|
62
|
+
// Fallback A: 2-digit aduana code at end of peso-bruto line
|
|
63
|
+
const m2 = source.match(/^\s*\d+\s+[\d,.]+\s+(\d{2})\s*$/m);
|
|
64
|
+
if (m2) return new FieldResult('aduanaEntradaSalida', true, m2[1]);
|
|
65
|
+
|
|
66
|
+
// Fallback B: derive 2-digit customs-office code from numPedimento
|
|
67
|
+
// Format: AA BB CCCC DDDDDDD → BB (positions 2-3) = aduana
|
|
68
|
+
const pedMatch = source.match(/\d{2}\s?\d{2}\s?\d{4}\s?\d{7}/);
|
|
69
|
+
if (pedMatch) {
|
|
70
|
+
const num = pedMatch[0].replace(/\s/g, '');
|
|
71
|
+
if (num.length === 15) {
|
|
72
|
+
return new FieldResult(
|
|
73
|
+
'aduanaEntradaSalida',
|
|
74
|
+
true,
|
|
75
|
+
num.substring(2, 4),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return new FieldResult('aduanaEntradaSalida', false, null);
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// 5) RFC: importer/exporter RFC on its own line.
|
|
85
|
+
// Strategy A: strict whole-line pattern (3-4 letters + 6 consecutive digits +
|
|
86
|
+
// 3 alphanum). COVE codes like COVE2681B1RX8 naturally fail this because
|
|
87
|
+
// their digit section is non-consecutive (2681B1 has a letter at pos 5).
|
|
88
|
+
// Strategy B: RFC as a word within a longer line (handles "RFC: IMS030409FZ0").
|
|
89
|
+
// Strategy C: loose 12-13 alphanum isolated on its own line — iterate ALL
|
|
90
|
+
// matches via matchAll() so that a leading COVE code is skipped and the
|
|
91
|
+
// actual RFC (which appears later in the document) is still found.
|
|
92
|
+
export const rfcExtractor = {
|
|
93
|
+
field: 'rfc',
|
|
94
|
+
extract: (source) => {
|
|
95
|
+
const RFC_STRICT = /^[A-Z]{3,4}\d{6}[A-Z0-9]{3}$/i;
|
|
96
|
+
const lines = source
|
|
97
|
+
.split(/\r?\n/)
|
|
98
|
+
.map((l) => l.trim())
|
|
99
|
+
.filter((l) => l);
|
|
100
|
+
|
|
101
|
+
// Primary: RFC occupies an entire trimmed line
|
|
102
|
+
const strictLine = lines.find((line) => RFC_STRICT.test(line));
|
|
103
|
+
if (strictLine) return new FieldResult('rfc', true, strictLine);
|
|
104
|
+
|
|
105
|
+
// Fallback A: RFC embedded in a longer line (word-boundary search)
|
|
106
|
+
for (const line of lines) {
|
|
107
|
+
const m = line.match(/\b([A-Z]{3,4}\d{6}[A-Z0-9]{3})\b/i);
|
|
108
|
+
if (m) return new FieldResult('rfc', true, m[1]);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Fallback B: loose 12-13 alphanum isolated on its own line.
|
|
112
|
+
// Use matchAll() to iterate ALL occurrences — a leading COVE code must not
|
|
113
|
+
// short-circuit the search; the RFC typically follows it in the document.
|
|
114
|
+
for (const m of source.matchAll(/\n\s*([A-Z0-9]{12,13})\s*\n/g)) {
|
|
115
|
+
if (!/^COVE/i.test(m[1])) return new FieldResult('rfc', true, m[1]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return new FieldResult('rfc', false, null);
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// 6) Código de Aceptación: 8 alphanumeric chars on the line right after the RFC.
|
|
123
|
+
// Uses the same RFC-line detection logic as rfcExtractor.
|
|
124
|
+
export const codigoAceptacionExtractor = {
|
|
125
|
+
field: 'codigoAceptacion',
|
|
126
|
+
extract: (source) => {
|
|
127
|
+
const RFC_STRICT = /^[A-Z]{3,4}\d{6}[A-Z0-9]{3}$/i;
|
|
128
|
+
const RFC_LOOSE = /^[A-Z0-9]{12,13}$/;
|
|
129
|
+
const lines = source
|
|
130
|
+
.split(/\r?\n/)
|
|
131
|
+
.map((l) => l.trim())
|
|
132
|
+
.filter((l) => l.length > 0);
|
|
133
|
+
|
|
134
|
+
// Find RFC line using strict pattern first, then loose (excluding COVE)
|
|
135
|
+
let rfcIndex = lines.findIndex((l) => RFC_STRICT.test(l));
|
|
136
|
+
if (rfcIndex < 0) {
|
|
137
|
+
rfcIndex = lines.findIndex((l) => RFC_LOOSE.test(l) && !/^COVE/i.test(l));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let code = null;
|
|
141
|
+
if (rfcIndex >= 0 && /^[A-Z0-9]{8}$/.test(lines[rfcIndex + 1] || '')) {
|
|
142
|
+
code = lines[rfcIndex + 1];
|
|
143
|
+
}
|
|
144
|
+
return new FieldResult('codigoAceptacion', code !== null, code);
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// 7) Num. E-Document: collects all 13-char codes following
|
|
149
|
+
// `NUM. E-DOCUMENT` / `NUMERO DE E-DOCUMENT` labels. Real eDocument
|
|
150
|
+
// numbers START WITH A DIGIT (e.g. "0438261FSSN86"); MVE acuse folios
|
|
151
|
+
// ("MNVA26002Q3V5") are also listed under the same label and kept —
|
|
152
|
+
// downstream consumers filter them. The old bare [A-Z0-9]{13} also
|
|
153
|
+
// captured 13-letter words (TRANSPORTISTA), truncated tokens
|
|
154
|
+
// (IDENTIFICACIO[N], CURP prefixes) and RFCs.
|
|
155
|
+
const EDOC_RE = /(?<![A-Z0-9])(?:\d[A-Z0-9]{12}|MNVA[A-Z0-9]{9})(?![A-Z0-9])/g;
|
|
156
|
+
|
|
157
|
+
export const numEDocumentoExtractor = {
|
|
158
|
+
field: 'numEDocumento',
|
|
159
|
+
extract: (source) => {
|
|
160
|
+
const lines = source.split(/\r?\n/);
|
|
161
|
+
const extractedCodes = [];
|
|
162
|
+
const titlePatterns = [/NUMERO\s+DE\s+E-DOCUMENT/i, /NUM\.?\s*E-DOCUMENT/i];
|
|
163
|
+
|
|
164
|
+
for (let i = 0; i < lines.length; i++) {
|
|
165
|
+
const line = lines[i];
|
|
166
|
+
const hasTitle = titlePatterns.some((p) => p.test(line));
|
|
167
|
+
if (!hasTitle) continue;
|
|
168
|
+
|
|
169
|
+
// Codes on the title line itself
|
|
170
|
+
const codesInLine = line.match(EDOC_RE) || [];
|
|
171
|
+
extractedCodes.push(...codesInLine);
|
|
172
|
+
|
|
173
|
+
// Codes on the next few lines (e.g. CLAVE/COMPL. table rows)
|
|
174
|
+
for (let j = 1; j <= 10 && i + j < lines.length; j++) {
|
|
175
|
+
const nextLine = lines[i + j];
|
|
176
|
+
if (/NUMERO|OBSERVACIONES/i.test(nextLine)) break;
|
|
177
|
+
const codesInNextLine = nextLine.match(EDOC_RE) || [];
|
|
178
|
+
extractedCodes.push(...codesInNextLine);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (extractedCodes.length === 0) {
|
|
183
|
+
return new FieldResult('numEDocumento', false, null);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const uniqueCodes = [...new Set(extractedCodes)];
|
|
187
|
+
return new FieldResult('numEDocumento', true, `[${uniqueCodes.join(',')}]`);
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// 8) Payment date — multiple known label variants
|
|
192
|
+
export const paymentDateExtractor = {
|
|
193
|
+
field: 'paymentDate',
|
|
194
|
+
extract: (source) => {
|
|
195
|
+
const patterns = [
|
|
196
|
+
/FECHA\s+DE\s+PAGO:?\s*(\d{2}\/\d{2}\/\d{4})/i, // 0: explicit label DD/MM/YYYY
|
|
197
|
+
/FECHA\s+DE\s+PAGO:?\s*(\d{4}\/\d{2}\/\d{2})/i, // 1: explicit label YYYY/MM/DD
|
|
198
|
+
/2\s+PAGO:\s*(\d{2}\/\d{2}\/\d{4})/, // 2: forma simplificada scheduled date ⚠️
|
|
199
|
+
/(?:^|\n)\s*PAGO\s+(\d{2}\/\d{2}\/\d{4})/i, // 3: PAGO at line start (original)
|
|
200
|
+
/(?<=\d)PAGO\s+(\d{2}\/\d{2}\/\d{4})/i, // 4: PAGO after digit (pdf-parse artifact)
|
|
201
|
+
/(\d{2}\/\d{2}\/\d{4})[ \t]+PAGO[ \t]*$/im, // 5: reversed layout — date before PAGO (FECHAS column)
|
|
202
|
+
// 6: forma simplificada — pdf-parse extracts table cells out of order, so the
|
|
203
|
+
// label "FECHA DE PAGO:" can appear on its own line and the value (along with
|
|
204
|
+
// other cells like línea de captura, pedimento, importe) follows several lines
|
|
205
|
+
// later. Take the FIRST dd/mm/yyyy after the label within a 400-char window.
|
|
206
|
+
// Safe because `isNoPagado` short-circuits documents without a real payment,
|
|
207
|
+
// so we won't grab the unrelated ENTRADA date from the "FECHAS:" block above.
|
|
208
|
+
/FECHA\s+DE\s+PAGO:[\s\S]{1,400}?(\d{2}\/\d{2}\/\d{4})/i,
|
|
209
|
+
/PRESENTACION:\s*(\d{2}\/\d{2}\/\d{4})/i, // 7: fallback
|
|
210
|
+
];
|
|
211
|
+
// "*** NO PAGADO" is the explicit SAT marker that no payment has been
|
|
212
|
+
// certified. When present, the bank-certification block is physically
|
|
213
|
+
// absent, so any date matched by the fallback patterns (e.g.
|
|
214
|
+
// "2 PAGO:" with a scheduled date, or "PRESENTACION:") would be a false
|
|
215
|
+
// positive. Return null outright — the document is classified as proforma.
|
|
216
|
+
const isNoPagado = /\*{3}\s*NO\s+PAGADO/i.test(source);
|
|
217
|
+
if (isNoPagado) {
|
|
218
|
+
return new FieldResult('paymentDate', false, null);
|
|
219
|
+
}
|
|
220
|
+
for (const pattern of patterns) {
|
|
221
|
+
const m = source.match(pattern);
|
|
222
|
+
if (!m) continue;
|
|
223
|
+
return new FieldResult('paymentDate', true, m[1]);
|
|
224
|
+
}
|
|
225
|
+
return new FieldResult('paymentDate', false, null);
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// 9) COVE — collect all `COVE<alphanum>` tokens from lines containing
|
|
230
|
+
// `COVE` or `NUMERO DE ACUSE DE VALOR`. CoveFact variant emits
|
|
231
|
+
// `COVE257W76NF2 / ID250230` → only the leading COVE token is kept
|
|
232
|
+
// because the COVE regex stops at the space before `/`.
|
|
233
|
+
export const coveExtractor = {
|
|
234
|
+
field: 'cove',
|
|
235
|
+
extract: (source) => {
|
|
236
|
+
const lines = source.split(/\r?\n/);
|
|
237
|
+
const coveLines = lines.filter(
|
|
238
|
+
(line) => /COVE/i.test(line) || /NUMERO DE ACUSE DE VALOR/i.test(line),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
if (coveLines.length === 0) {
|
|
242
|
+
return new FieldResult('cove', false, null);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const coveValues = [];
|
|
246
|
+
coveLines.forEach((line) => {
|
|
247
|
+
const coveMatches = line.match(/COVE[A-Z0-9]+/gi) || [];
|
|
248
|
+
coveValues.push(...coveMatches.map((m) => m.toUpperCase()));
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (coveValues.length === 0) {
|
|
252
|
+
return new FieldResult('cove', false, null);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const unique = [...new Set(coveValues)];
|
|
256
|
+
return new FieldResult('cove', true, `[${unique.join(',')}]`);
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// 10) Patente: from the PATENTE/PEDIMENTO/ADUANA header table
|
|
261
|
+
// Fallback A: pago electrónico line "3429 4024126 07" (pedimento_completo).
|
|
262
|
+
// Fallback B: positions 4-7 of numPedimento (always available when found).
|
|
263
|
+
export const patenteExtractor = {
|
|
264
|
+
field: 'patente',
|
|
265
|
+
extract: (source) => {
|
|
266
|
+
// Primary: PATENTE:/PEDIMENTO:/ADUANA: header followed by data line
|
|
267
|
+
const lines = source.split(/\r?\n/);
|
|
268
|
+
const patenteHeaderIndex = lines.findIndex((line) =>
|
|
269
|
+
/PATENTE:.*PEDIMENTO:.*ADUANA:/i.test(line),
|
|
270
|
+
);
|
|
271
|
+
if (patenteHeaderIndex >= 0) {
|
|
272
|
+
for (let i = patenteHeaderIndex + 1; i < lines.length; i++) {
|
|
273
|
+
const line = lines[i].trim();
|
|
274
|
+
if (/^\d+\s+\d+\s+\d+$/.test(line)) {
|
|
275
|
+
const parts = line.split(/\s+/);
|
|
276
|
+
return new FieldResult('patente', true, parts[0]);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Fallback A: pago electrónico line "3429 4024126 07"
|
|
282
|
+
const pagoMatch = source.match(/(\d{4})\s+\d{7}\s+\d{2}/);
|
|
283
|
+
if (pagoMatch) return new FieldResult('patente', true, pagoMatch[1]);
|
|
284
|
+
|
|
285
|
+
// Fallback B: positions 4-7 of numPedimento
|
|
286
|
+
const pedMatch = source.match(/\d{2}\s?\d{2}\s?\d{4}\s?\d{7}/);
|
|
287
|
+
if (pedMatch) {
|
|
288
|
+
const num = pedMatch[0].replace(/\s/g, '');
|
|
289
|
+
if (num.length === 15) {
|
|
290
|
+
return new FieldResult('patente', true, num.substring(4, 8));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return new FieldResult('patente', false, null);
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// 11) Fecha de Pago Rectificación — used when clavePedimento is a rectification
|
|
299
|
+
export const fechaPagoRectificacionExtractor = {
|
|
300
|
+
field: 'fechaPagoRectificacion',
|
|
301
|
+
extract: (source) => {
|
|
302
|
+
const rectSectionMatch = source.match(
|
|
303
|
+
/RECTIFICACION[\s\S]{0,500}?(\d{2}\/\d{2}\/\d{4})/i,
|
|
304
|
+
);
|
|
305
|
+
if (rectSectionMatch) {
|
|
306
|
+
return new FieldResult(
|
|
307
|
+
'fechaPagoRectificacion',
|
|
308
|
+
true,
|
|
309
|
+
rectSectionMatch[1],
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
const fechaMatch = source.match(
|
|
313
|
+
/FECHA PAGO RECT[\s\S]{0,500}?(\d{2}\/\d{2}\/\d{4})/i,
|
|
314
|
+
);
|
|
315
|
+
if (fechaMatch) {
|
|
316
|
+
return new FieldResult('fechaPagoRectificacion', true, fechaMatch[1]);
|
|
317
|
+
}
|
|
318
|
+
return new FieldResult('fechaPagoRectificacion', false, null);
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Canonical extractor list for both pedimento_simplificado and pedimento_completo PDFs.
|
|
324
|
+
* Order matters only for downstream tooling that inspects the result array.
|
|
325
|
+
*/
|
|
326
|
+
export const sharedPedimentoExtractors = [
|
|
327
|
+
numPedimentoExtractor,
|
|
328
|
+
tipoOperacionExtractor,
|
|
329
|
+
clavePedimentoExtractor,
|
|
330
|
+
aduanaEntradaSalidaExtractor,
|
|
331
|
+
rfcExtractor,
|
|
332
|
+
codigoAceptacionExtractor,
|
|
333
|
+
numEDocumentoExtractor,
|
|
334
|
+
paymentDateExtractor,
|
|
335
|
+
coveExtractor,
|
|
336
|
+
patenteExtractor,
|
|
337
|
+
fechaPagoRectificacionExtractor,
|
|
338
|
+
];
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Shared helper used by both PDF matchers' `extractPedimentoYear`.
|
|
342
|
+
*/
|
|
343
|
+
export function pedimentoYearFromFields(fields) {
|
|
344
|
+
const numPedimento = fields?.find((f) => f.name === 'numPedimento')?.value;
|
|
345
|
+
if (!numPedimento) return null;
|
|
346
|
+
const year = parseInt(numPedimento.substring(0, 2), 10);
|
|
347
|
+
return year < 50 ? year + 2000 : year + 1900;
|
|
348
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { FieldResult } from '../document-type-shared.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DODA PDF Document Type Definition
|
|
5
|
+
* Detects DODA (Documento de Operación para Despacho Aduanero) in PDF format.
|
|
6
|
+
* DODAs are validation documents generated by VUCEM for customs clearance.
|
|
7
|
+
* They reside in the SAME directory as the pedimento_simplificado,
|
|
8
|
+
* so within-table propagation handles arela_path assignment.
|
|
9
|
+
*/
|
|
10
|
+
export const dodaPdfDefinition = {
|
|
11
|
+
type: 'doda_pdf',
|
|
12
|
+
extensions: ['pdf'],
|
|
13
|
+
match: (source) => {
|
|
14
|
+
// DODA PDFs contain specific markers from VUCEM/customs systems
|
|
15
|
+
const markers = [
|
|
16
|
+
/DOCUMENTO DE OPERACI[OÓ]N PARA DESPACHO ADUANERO/i,
|
|
17
|
+
/DODA/i,
|
|
18
|
+
/VUCEM/i,
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// Require the primary DODA marker, or at least 2 of the secondary markers
|
|
22
|
+
const primaryMatch = markers[0].test(source);
|
|
23
|
+
if (primaryMatch) return true;
|
|
24
|
+
|
|
25
|
+
const secondaryMatches = markers
|
|
26
|
+
.slice(1)
|
|
27
|
+
.filter((m) => m.test(source)).length;
|
|
28
|
+
|
|
29
|
+
// Also check for pedimento number + DODA-specific context
|
|
30
|
+
const hasPedimentoNumber = /\d{2}\s?\d{2}\s?\d{4}\s?\d{7}/.test(source);
|
|
31
|
+
const hasDodaContext =
|
|
32
|
+
/despacho aduanero|operaci[oó]n aduanera|validaci[oó]n/i.test(source);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
(secondaryMatches >= 2 && hasPedimentoNumber) ||
|
|
36
|
+
(hasDodaContext && hasPedimentoNumber && secondaryMatches >= 1)
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
extractNumPedimento: (source, fields) => {
|
|
41
|
+
return fields?.find((f) => f.name === 'numPedimento')?.value ?? null;
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
extractPedimentoYear: (source, fields) => {
|
|
45
|
+
const numPedimento = fields?.find((f) => f.name === 'numPedimento')?.value;
|
|
46
|
+
if (!numPedimento) return null;
|
|
47
|
+
const year = parseInt(numPedimento.substring(0, 2), 10);
|
|
48
|
+
return year < 50 ? year + 2000 : year + 1900;
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
extractors: [
|
|
52
|
+
{
|
|
53
|
+
field: 'numPedimento',
|
|
54
|
+
extract: (source) => {
|
|
55
|
+
// Try cadena original format: ||aduana|patente|...|pedimentos|integracion|...
|
|
56
|
+
// e.g. ||070|3429|2|4009029,4008062|109335668|A231|
|
|
57
|
+
const cadenaMatch = source.match(
|
|
58
|
+
/\|\|(\d{2,3})\|(\d{4})\|\d\|([\d,]+)\|(\d+)\|/,
|
|
59
|
+
);
|
|
60
|
+
if (cadenaMatch) {
|
|
61
|
+
const rawAduana = cadenaMatch[1];
|
|
62
|
+
// 3-digit code = aduana(2) + section(1), e.g. 070 → aduana 07
|
|
63
|
+
const aduana =
|
|
64
|
+
rawAduana.length === 3
|
|
65
|
+
? rawAduana.slice(0, 2)
|
|
66
|
+
: rawAduana.padStart(2, '0');
|
|
67
|
+
const patente = cadenaMatch[2];
|
|
68
|
+
const pedNums = cadenaMatch[3].split(',');
|
|
69
|
+
// Use first pedimento number, pad to 7 digits
|
|
70
|
+
const pedNum = pedNums[0].padStart(7, '0');
|
|
71
|
+
// Extract year from date in cadena
|
|
72
|
+
const yearMatch = source.match(/(\d{4})-\d{2}-\d{2}/);
|
|
73
|
+
const year = yearMatch
|
|
74
|
+
? yearMatch[1].slice(-2)
|
|
75
|
+
: new Date().getFullYear().toString().slice(-2);
|
|
76
|
+
const full = `${year}${aduana}${patente}${pedNum}`;
|
|
77
|
+
if (full.length === 15) {
|
|
78
|
+
return new FieldResult('numPedimento', true, full);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Try dash-separated format: YY-AA-PPPP-NNNNNNN
|
|
83
|
+
const dashMatch = source.match(/(\d{2})-(\d{2})-(\d{4})-(\d{7})/);
|
|
84
|
+
if (dashMatch) {
|
|
85
|
+
const full =
|
|
86
|
+
dashMatch[1] + dashMatch[2] + dashMatch[3] + dashMatch[4];
|
|
87
|
+
return new FieldResult('numPedimento', true, full);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Try 15-digit near pedimento keyword (avoid matching sello digital)
|
|
91
|
+
const contextMatch = source.match(
|
|
92
|
+
/pedimento[^\d]{0,30}(\d{2}\s?\d{2}\s?\d{4}\s?\d{7})/i,
|
|
93
|
+
);
|
|
94
|
+
if (contextMatch) {
|
|
95
|
+
return new FieldResult(
|
|
96
|
+
'numPedimento',
|
|
97
|
+
true,
|
|
98
|
+
contextMatch[1].replace(/\s/g, ''),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return new FieldResult('numPedimento', false, null);
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
field: 'rfc',
|
|
107
|
+
extract: (source) => {
|
|
108
|
+
// Mexican RFC: 3-4 letters + 6 digits + 3 alphanumeric
|
|
109
|
+
const match = source.match(/\b([A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3})\b/);
|
|
110
|
+
return new FieldResult('rfc', !!match, match ? match[1] : null);
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
field: 'aduana',
|
|
115
|
+
extract: (source) => {
|
|
116
|
+
const match = source.match(/aduana[:\s]*(\d{2,4})/i);
|
|
117
|
+
return new FieldResult('aduana', !!match, match ? match[1] : null);
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
};
|