@arela/uploader 1.1.4 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/.claude/worktrees/agent-multi-profile/.env.template +224 -0
  2. package/.claude/worktrees/agent-multi-profile/.prettierrc +13 -0
  3. package/.claude/worktrees/agent-multi-profile/README.md +405 -0
  4. package/.claude/worktrees/agent-multi-profile/package-lock.json +7096 -0
  5. package/.claude/worktrees/agent-multi-profile/package.json +78 -0
  6. package/.claude/worktrees/agent-multi-profile/scripts/cleanup-ds-store.js +109 -0
  7. package/.claude/worktrees/agent-multi-profile/scripts/cleanup-system-files.js +69 -0
  8. package/.claude/worktrees/agent-multi-profile/scripts/scoring-compare.js +243 -0
  9. package/.claude/worktrees/agent-multi-profile/scripts/scoring-phase4-check.js +96 -0
  10. package/.claude/worktrees/agent-multi-profile/scripts/tests/phase-7-features.test.js +415 -0
  11. package/.claude/worktrees/agent-multi-profile/scripts/tests/signal-handling.test.js +275 -0
  12. package/.claude/worktrees/agent-multi-profile/scripts/tests/smart-watch-integration.test.js +554 -0
  13. package/.claude/worktrees/agent-multi-profile/scripts/tests/watch-service-integration.test.js +584 -0
  14. package/.claude/worktrees/agent-multi-profile/src/commands/AgentCommand.js +229 -0
  15. package/.claude/worktrees/agent-multi-profile/src/commands/AgentInitCommand.js +316 -0
  16. package/.claude/worktrees/agent-multi-profile/src/commands/DatastageCommand.js +164 -0
  17. package/.claude/worktrees/agent-multi-profile/src/commands/GDriveSyncCommand.js +475 -0
  18. package/.claude/worktrees/agent-multi-profile/src/commands/IdentifyCommand.js +708 -0
  19. package/.claude/worktrees/agent-multi-profile/src/commands/PollWorkerCommand.js +169 -0
  20. package/.claude/worktrees/agent-multi-profile/src/commands/PropagateCommand.js +636 -0
  21. package/.claude/worktrees/agent-multi-profile/src/commands/PushCommand.js +743 -0
  22. package/.claude/worktrees/agent-multi-profile/src/commands/ScanCommand.js +722 -0
  23. package/.claude/worktrees/agent-multi-profile/src/commands/UploadCommand.js +587 -0
  24. package/.claude/worktrees/agent-multi-profile/src/commands/WatchCommand.js +1342 -0
  25. package/.claude/worktrees/agent-multi-profile/src/commands/WorkerCommand.js +337 -0
  26. package/.claude/worktrees/agent-multi-profile/src/config/config.js +862 -0
  27. package/.claude/worktrees/agent-multi-profile/src/document-type-shared.js +131 -0
  28. package/.claude/worktrees/agent-multi-profile/src/document-types/_pedimento-shared-extractors.js +348 -0
  29. package/.claude/worktrees/agent-multi-profile/src/document-types/doda-pdf.js +121 -0
  30. package/.claude/worktrees/agent-multi-profile/src/document-types/doda-xml.js +118 -0
  31. package/.claude/worktrees/agent-multi-profile/src/document-types/factura-inter-agencia.js +186 -0
  32. package/.claude/worktrees/agent-multi-profile/src/document-types/facturas-comerciales.js +233 -0
  33. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo-xml.js +372 -0
  34. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo.js +108 -0
  35. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-simplificado.js +76 -0
  36. package/.claude/worktrees/agent-multi-profile/src/document-types/proforma.js +29 -0
  37. package/.claude/worktrees/agent-multi-profile/src/document-types/support-document.js +200 -0
  38. package/.claude/worktrees/agent-multi-profile/src/errors/ErrorHandler.js +278 -0
  39. package/.claude/worktrees/agent-multi-profile/src/errors/ErrorTypes.js +104 -0
  40. package/.claude/worktrees/agent-multi-profile/src/file-detection.js +338 -0
  41. package/.claude/worktrees/agent-multi-profile/src/index.js +890 -0
  42. package/.claude/worktrees/agent-multi-profile/src/scoring/db-matcher-adapter.js +98 -0
  43. package/.claude/worktrees/agent-multi-profile/src/scoring/matchers-seed.js +386 -0
  44. package/.claude/worktrees/agent-multi-profile/src/scoring/scoring-engine.js +251 -0
  45. package/.claude/worktrees/agent-multi-profile/src/services/AdvancedFilterService.js +505 -0
  46. package/.claude/worktrees/agent-multi-profile/src/services/AutoProcessingService.js +749 -0
  47. package/.claude/worktrees/agent-multi-profile/src/services/BenchmarkingService.js +381 -0
  48. package/.claude/worktrees/agent-multi-profile/src/services/DatabaseService.js +2173 -0
  49. package/.claude/worktrees/agent-multi-profile/src/services/DatastageApiService.js +240 -0
  50. package/.claude/worktrees/agent-multi-profile/src/services/ErrorMonitor.js +275 -0
  51. package/.claude/worktrees/agent-multi-profile/src/services/GoogleDriveService.js +217 -0
  52. package/.claude/worktrees/agent-multi-profile/src/services/LoggingService.js +649 -0
  53. package/.claude/worktrees/agent-multi-profile/src/services/MonitoringService.js +401 -0
  54. package/.claude/worktrees/agent-multi-profile/src/services/PerformanceOptimizer.js +511 -0
  55. package/.claude/worktrees/agent-multi-profile/src/services/PipelineApiService.js +274 -0
  56. package/.claude/worktrees/agent-multi-profile/src/services/PipelineJobRunner.js +389 -0
  57. package/.claude/worktrees/agent-multi-profile/src/services/ProfileManager.js +164 -0
  58. package/.claude/worktrees/agent-multi-profile/src/services/ReportingService.js +511 -0
  59. package/.claude/worktrees/agent-multi-profile/src/services/ScanApiService.js +775 -0
  60. package/.claude/worktrees/agent-multi-profile/src/services/SignalHandler.js +255 -0
  61. package/.claude/worktrees/agent-multi-profile/src/services/SmartWatchDatabaseService.js +527 -0
  62. package/.claude/worktrees/agent-multi-profile/src/services/WatchService.js +783 -0
  63. package/.claude/worktrees/agent-multi-profile/src/services/upload/ApiUploadService.js +676 -0
  64. package/.claude/worktrees/agent-multi-profile/src/services/upload/BaseUploadService.js +36 -0
  65. package/.claude/worktrees/agent-multi-profile/src/services/upload/MultiApiUploadService.js +233 -0
  66. package/.claude/worktrees/agent-multi-profile/src/services/upload/SupabaseUploadService.js +148 -0
  67. package/.claude/worktrees/agent-multi-profile/src/services/upload/UploadServiceFactory.js +100 -0
  68. package/.claude/worktrees/agent-multi-profile/src/utils/CleanupManager.js +262 -0
  69. package/.claude/worktrees/agent-multi-profile/src/utils/FileOperations.js +192 -0
  70. package/.claude/worktrees/agent-multi-profile/src/utils/FileSanitizer.js +99 -0
  71. package/.claude/worktrees/agent-multi-profile/src/utils/PathDetector.js +198 -0
  72. package/.claude/worktrees/agent-multi-profile/src/utils/PathNormalizer.js +274 -0
  73. package/.claude/worktrees/agent-multi-profile/src/utils/WatchEventHandler.js +522 -0
  74. package/.claude/worktrees/agent-multi-profile/supabase/migrations/001_create_initial_schema.sql +366 -0
  75. package/.claude/worktrees/agent-multi-profile/supabase/migrations/002_align_with_arela_api_schema.sql +145 -0
  76. package/.claude/worktrees/agent-multi-profile/tests/commands/IdentifyCommand.test.js +570 -0
  77. package/.claude/worktrees/agent-multi-profile/tests/commands/PropagateCommand.test.js +568 -0
  78. package/.claude/worktrees/agent-multi-profile/tests/commands/PushCommand.test.js +754 -0
  79. package/.claude/worktrees/agent-multi-profile/tests/commands/ScanCommand.test.js +382 -0
  80. package/.claude/worktrees/agent-multi-profile/tests/unit/PathAndTableNameGeneration.test.js +1211 -0
  81. package/.claude/worktrees/agent-multi-profile/tests/unit/factura-inter-agencia.test.js +218 -0
  82. package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-completo-xml-matcher.test.js +271 -0
  83. package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-simplificado-matcher.test.js +185 -0
  84. package/.claude/worktrees/agent-multi-profile/tests/unit/scoring-engine.test.js +221 -0
  85. package/README.md +65 -0
  86. package/package.json +1 -1
  87. package/src/commands/AgentCommand.js +210 -0
  88. package/src/commands/AgentInitCommand.js +316 -0
  89. package/src/commands/PollWorkerCommand.js +11 -322
  90. package/src/config/config.js +44 -6
  91. package/src/index.js +55 -0
  92. package/src/services/LoggingService.js +35 -0
  93. package/src/services/PipelineApiService.js +7 -1
  94. package/src/services/PipelineJobRunner.js +389 -0
  95. package/src/services/ProfileManager.js +164 -0
@@ -0,0 +1,636 @@
1
+ import cliProgress from 'cli-progress';
2
+
3
+ import logger from '../services/LoggingService.js';
4
+ import ScanApiService from '../services/ScanApiService.js';
5
+
6
+ import appConfig from '../config/config.js';
7
+
8
+ /**
9
+ * Propagate Command
10
+ * Propagates arela_path from detected pedimentos to related files in the same directory
11
+ * Optimized for large datasets with batch processing and progress tracking
12
+ */
13
+ export class PropagateCommand {
14
+ constructor(options = {}) {
15
+ this.options = {
16
+ batchSize: parseInt(options.batchSize) || 50, // Process 50 pedimentos at a time
17
+ showStats: options.showStats || false,
18
+ api: options.api || 'default',
19
+ onProgress: options.onProgress || null, // Progress callback for worker mode
20
+ };
21
+
22
+ this.scanApiService = null;
23
+ this.tableName = null;
24
+ this.stats = {
25
+ startTime: Date.now(),
26
+ pedimentosProcessed: 0,
27
+ filesUpdated: 0,
28
+ filesFailed: 0,
29
+ directoriesProcessed: 0,
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Main execution method
35
+ */
36
+ async execute() {
37
+ try {
38
+ console.log('šŸ”„ Starting arela propagate command\n');
39
+
40
+ // Step 1: Validate configuration
41
+ await this.#validateConfiguration();
42
+
43
+ // Step 2: Initialize API service with configured target
44
+ const apiTarget = this.options.api || 'default';
45
+ this.scanApiService = new ScanApiService(apiTarget);
46
+ console.log(`šŸŽÆ API Target: ${apiTarget}`);
47
+
48
+ // Step 3: Fetch all tables for this instance
49
+ const scanConfig = appConfig.getScanConfig();
50
+ const tables = await this.scanApiService.getInstanceTables(
51
+ scanConfig.companySlug,
52
+ scanConfig.serverId,
53
+ scanConfig.basePathFull,
54
+ );
55
+
56
+ if (tables.length === 0) {
57
+ console.error(
58
+ '\nāŒ No tables found for this instance. Run "arela scan" first.\n',
59
+ );
60
+ process.exit(1);
61
+ }
62
+
63
+ console.log(
64
+ `šŸ“‹ Found ${tables.length} table${tables.length === 1 ? '' : 's'} to process:`,
65
+ );
66
+ for (const table of tables) {
67
+ console.log(` - ${table.tableName}`);
68
+ }
69
+ console.log();
70
+
71
+ // Step 4: Process each table
72
+ let totalStats = {
73
+ pedimentosProcessed: 0,
74
+ filesUpdated: 0,
75
+ filesFailed: 0,
76
+ directoriesProcessed: 0,
77
+ };
78
+
79
+ // Report initial progress
80
+ this.#reportProgress(
81
+ 0,
82
+ `Starting propagation on ${tables.length} tables`,
83
+ );
84
+
85
+ for (let i = 0; i < tables.length; i++) {
86
+ const table = tables[i];
87
+ console.log(`\nšŸ”„ Processing table: ${table.tableName}\n`);
88
+ this.#reportProgress(
89
+ Math.round((i / tables.length) * 100),
90
+ `Processing table ${i + 1}/${tables.length}: ${table.tableName}`,
91
+ );
92
+ this.tableName = table.tableName;
93
+
94
+ // Process this table
95
+ const stats = await this.#processTable();
96
+
97
+ totalStats.pedimentosProcessed += stats.pedimentosProcessed;
98
+ totalStats.filesUpdated += stats.filesUpdated;
99
+ totalStats.filesFailed += stats.filesFailed;
100
+ totalStats.directoriesProcessed += stats.directoriesProcessed;
101
+ }
102
+
103
+ // Step 5: Cross-table propagation
104
+ // Match files with detected_pedimento in one table to pedimento sources in other tables
105
+ const crossTableStats = await this.#processCrossTablePropagation(
106
+ scanConfig,
107
+ tables,
108
+ );
109
+ totalStats.filesUpdated += crossTableStats.filesUpdated;
110
+ totalStats.filesFailed += crossTableStats.filesFailed;
111
+
112
+ // Show combined results
113
+ const duration = ((Date.now() - this.stats.startTime) / 1000).toFixed(2);
114
+ const filesPerSec =
115
+ totalStats.filesUpdated > 0
116
+ ? (totalStats.filesUpdated / parseFloat(duration)).toFixed(1)
117
+ : 0;
118
+
119
+ // Report completion
120
+ this.#reportProgress(
121
+ 100,
122
+ `Propagation completed: ${totalStats.filesUpdated} files updated`,
123
+ );
124
+
125
+ console.log('\\nāœ… Propagation Complete!\\n');
126
+ console.log(`šŸ“Š Total Results:`);
127
+ console.log(` Tables Processed: ${tables.length}`);
128
+ console.log(
129
+ ` Pedimentos Processed: ${totalStats.pedimentosProcessed.toLocaleString()}`,
130
+ );
131
+ console.log(
132
+ ` Files Updated: ${totalStats.filesUpdated.toLocaleString()}`,
133
+ );
134
+ console.log(
135
+ ` Files Failed: ${totalStats.filesFailed.toLocaleString()}`,
136
+ );
137
+ console.log(
138
+ ` Directories Processed: ${totalStats.directoriesProcessed.toLocaleString()}`,
139
+ );
140
+ console.log(` Duration: ${duration}s`);
141
+ console.log(` Speed: ${filesPerSec} files/sec\\n`);
142
+ } catch (error) {
143
+ logger.error('Propagation command failed:', error);
144
+ console.error(`\\nāŒ Error: ${error.message}\\n`);
145
+ if (process.env.VERBOSE || process.env.DEBUG) {
146
+ console.error('Stack trace:', error.stack);
147
+ }
148
+ process.exit(1);
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Process a single table
154
+ * @private
155
+ * @returns {Promise<Object>} Statistics for this table
156
+ */
157
+ async #processTable() {
158
+ const tableStats = {
159
+ pedimentosProcessed: 0,
160
+ filesUpdated: 0,
161
+ filesFailed: 0,
162
+ directoriesProcessed: 0,
163
+ };
164
+
165
+ // Show initial statistics
166
+ const initialStats = await this.#showInitialStats();
167
+
168
+ // Mark files needing propagation (if we have pedimento sources)
169
+ if (initialStats.pedimentoSources > 0) {
170
+ await this.#markFilesForPropagation();
171
+ } else {
172
+ console.log(' ā„¹ļø No pedimento sources found. Skipping.\\n');
173
+ return tableStats;
174
+ }
175
+
176
+ // Check if there are files to propagate
177
+ const statsAfterMarking = await this.scanApiService.getPropagationStats(
178
+ this.tableName,
179
+ );
180
+ if (statsAfterMarking.pending === 0) {
181
+ console.log(
182
+ ' ā„¹ļø All files already have arela_path. Nothing to propagate.\\n',
183
+ );
184
+ return tableStats;
185
+ }
186
+
187
+ console.log(
188
+ ` šŸš€ Found ${statsAfterMarking.pending.toLocaleString()} files ready for propagation.\\n`,
189
+ );
190
+
191
+ // Process pedimentos and propagate arela_path
192
+ const stats = await this.#processPropagation();
193
+ Object.assign(tableStats, stats);
194
+
195
+ // Show final statistics for this table
196
+ await this.#showFinalStats();
197
+
198
+ return tableStats;
199
+ }
200
+
201
+ /**
202
+ * Validate scan configuration
203
+ * @private
204
+ */
205
+ async #validateConfiguration() {
206
+ logger.debug('Validating scan configuration...');
207
+
208
+ // Set API target
209
+ appConfig.setApiTarget(this.options.api);
210
+
211
+ // Validate scan config (same as scan/identify commands)
212
+ // Note: validateScanConfig() throws on error, doesn't return errors array
213
+ try {
214
+ appConfig.validateScanConfig();
215
+ } catch (error) {
216
+ console.error(`\\nāŒ ${error.message}\\n`);
217
+ throw new Error('Invalid scan configuration');
218
+ }
219
+
220
+ console.log(`šŸŽÆ API Target: ${this.options.api}`);
221
+ console.log(`šŸ“¦ Batch Size: ${this.options.batchSize}\\n`);
222
+
223
+ logger.debug('Configuration validated');
224
+ }
225
+
226
+ /**
227
+ * Show initial propagation statistics
228
+ * @private
229
+ */
230
+ async #showInitialStats() {
231
+ try {
232
+ const stats = await this.scanApiService.getPropagationStats(
233
+ this.tableName,
234
+ );
235
+
236
+ console.log('šŸ“ˆ Initial Status:');
237
+ console.log(` Total Files: ${stats.totalFiles.toLocaleString()}`);
238
+ console.log(
239
+ ` With arela_path: ${stats.withArelaPath.toLocaleString()}`,
240
+ );
241
+ console.log(
242
+ ` Pedimento Sources: ${stats.pedimentoSources.toLocaleString()}`,
243
+ );
244
+ console.log(` Errors: ${stats.errors.toLocaleString()}\n`);
245
+
246
+ return stats;
247
+ } catch (error) {
248
+ logger.error('Failed to fetch initial stats:', error);
249
+ throw new Error(`Failed to fetch propagation stats: ${error.message}`);
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Mark files that need propagation
255
+ * This is a preparation step that flags files for efficient processing
256
+ * @private
257
+ */
258
+ async #markFilesForPropagation() {
259
+ try {
260
+ console.log('šŸ·ļø Marking files needing propagation...');
261
+ const result = await this.scanApiService.markFilesNeedingPropagation(
262
+ this.tableName,
263
+ );
264
+ console.log(`āœ“ Marked ${result.markedCount.toLocaleString()} files\n`);
265
+ } catch (error) {
266
+ logger.error('Failed to mark files:', error);
267
+ throw new Error(`Failed to mark files: ${error.message}`);
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Process propagation in batches
273
+ * Fetches pedimentos and propagates their arela_path to files in the same directory
274
+ * @private
275
+ */
276
+ async #processPropagation() {
277
+ console.log('šŸš€ Processing propagation...\n');
278
+
279
+ // First, get the total count of pedimento sources
280
+ const initialStats = await this.scanApiService.getPropagationStats(
281
+ this.tableName,
282
+ );
283
+ const totalPedimentos = initialStats.pedimentoSources;
284
+
285
+ if (totalPedimentos === 0) {
286
+ console.log('ā„¹ļø No pedimento sources found.\n');
287
+ return;
288
+ }
289
+
290
+ let offset = 0;
291
+ let hasMore = true;
292
+ let processedCount = 0;
293
+
294
+ // Create progress bar with actual total
295
+ const progressBar = new cliProgress.SingleBar(
296
+ {
297
+ format:
298
+ 'šŸ“„ Propagating |{bar}| {percentage}% | {value}/{total} directories | {speed} files/sec | {filesUpdated} files updated',
299
+ barCompleteChar: '\u2588',
300
+ barIncompleteChar: '\u2591',
301
+ hideCursor: true,
302
+ clearOnComplete: false,
303
+ stopOnComplete: true,
304
+ },
305
+ cliProgress.Presets.shades_classic,
306
+ );
307
+
308
+ const startTime = Date.now();
309
+ let filesUpdated = 0;
310
+
311
+ // Start progress bar with actual total
312
+ progressBar.start(totalPedimentos, 0, {
313
+ speed: '0',
314
+ filesUpdated: 0,
315
+ });
316
+
317
+ try {
318
+ while (hasMore) {
319
+ // Fetch batch of pedimento sources
320
+ const pedimentos = await this.scanApiService.fetchPedimentoSources(
321
+ this.tableName,
322
+ offset,
323
+ this.options.batchSize,
324
+ );
325
+
326
+ // Validate response
327
+ if (!pedimentos || !Array.isArray(pedimentos)) {
328
+ logger.error(
329
+ 'Invalid response from fetchPedimentoSources:',
330
+ pedimentos,
331
+ );
332
+ throw new Error('API returned invalid data format (expected array)');
333
+ }
334
+
335
+ if (pedimentos.length === 0) {
336
+ hasMore = false;
337
+ break;
338
+ }
339
+
340
+ // Process each pedimento's directory
341
+ for (const pedimento of pedimentos) {
342
+ const {
343
+ id,
344
+ directory_path,
345
+ arela_path,
346
+ rfc,
347
+ detected_pedimento,
348
+ detected_pedimento_year,
349
+ } = pedimento;
350
+
351
+ // Fetch files in the same directory
352
+ const files =
353
+ await this.scanApiService.fetchFilesNeedingPropagationByDirectory(
354
+ this.tableName,
355
+ directory_path,
356
+ );
357
+
358
+ // Validate response
359
+ if (!files || !Array.isArray(files)) {
360
+ logger.error(
361
+ `Invalid response for directory ${directory_path}:`,
362
+ files,
363
+ );
364
+ this.stats.filesFailed++;
365
+ continue;
366
+ }
367
+
368
+ if (files.length > 0) {
369
+ // Prepare batch update
370
+ const updates = files.map((file) => ({
371
+ id: file.id,
372
+ arelaPath: arela_path,
373
+ rfc: rfc,
374
+ detectedPedimento: detected_pedimento,
375
+ detectedPedimentoYear: detected_pedimento_year,
376
+ propagatedFromId: id,
377
+ propagationError: null,
378
+ }));
379
+
380
+ // Send batch update to API
381
+ try {
382
+ const result = await this.scanApiService.batchUpdatePropagation(
383
+ this.tableName,
384
+ updates,
385
+ );
386
+
387
+ filesUpdated += result.updated;
388
+ this.stats.filesUpdated += result.updated;
389
+ this.stats.filesFailed += result.errors;
390
+ } catch (error) {
391
+ logger.error(
392
+ `Failed to update files in directory ${directory_path}:`,
393
+ error,
394
+ );
395
+ this.stats.filesFailed += files.length;
396
+ }
397
+ }
398
+
399
+ this.stats.directoriesProcessed++;
400
+ this.stats.pedimentosProcessed++;
401
+ processedCount++;
402
+
403
+ // Update progress bar
404
+ const elapsed = (Date.now() - startTime) / 1000;
405
+ const speed = elapsed > 0 ? Math.round(filesUpdated / elapsed) : 0;
406
+
407
+ progressBar.update(processedCount, {
408
+ speed: speed.toString(),
409
+ filesUpdated: filesUpdated.toLocaleString(),
410
+ });
411
+ }
412
+
413
+ // Move to next batch
414
+ offset += pedimentos.length;
415
+
416
+ // Check if we got fewer results than requested (indicates last batch)
417
+ if (pedimentos.length < this.options.batchSize) {
418
+ hasMore = false;
419
+ }
420
+ }
421
+ } finally {
422
+ progressBar.stop();
423
+ }
424
+
425
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
426
+ const speed =
427
+ duration > 0 ? Math.round(filesUpdated / parseFloat(duration)) : 0;
428
+
429
+ console.log('\n šŸ“Š Results:');
430
+ console.log(
431
+ ` Pedimentos Processed: ${this.stats.pedimentosProcessed.toLocaleString()}`,
432
+ );
433
+ console.log(
434
+ ` Directories Processed: ${this.stats.directoriesProcessed.toLocaleString()}`,
435
+ );
436
+ console.log(
437
+ ` Files Updated: ${this.stats.filesUpdated.toLocaleString()}`,
438
+ );
439
+ console.log(` Errors: ${this.stats.filesFailed.toLocaleString()}`);
440
+ console.log(` Duration: ${duration}s`);
441
+ console.log(` Speed: ${speed} files/sec\n`);
442
+
443
+ return {
444
+ pedimentosProcessed: this.stats.pedimentosProcessed,
445
+ filesUpdated: this.stats.filesUpdated,
446
+ filesFailed: this.stats.filesFailed,
447
+ directoriesProcessed: this.stats.directoriesProcessed,
448
+ };
449
+ }
450
+
451
+ /**
452
+ * Cross-table propagation phase
453
+ * Matches files with detected_pedimento in one table to pedimento sources in other tables.
454
+ * This enables facturas (in a different directory/table) to get arela_path from their pedimento.
455
+ * @private
456
+ * @param {Object} scanConfig - Scan configuration with companySlug, serverId, basePathFull
457
+ * @param {Array} tables - All tables for this instance
458
+ * @returns {Promise<Object>} { filesUpdated, filesFailed }
459
+ */
460
+ async #processCrossTablePropagation(scanConfig, tables) {
461
+ console.log('\nšŸ”— Cross-table propagation phase...\n');
462
+
463
+ const stats = { filesUpdated: 0, filesFailed: 0 };
464
+
465
+ // Step 1: Fetch all pedimento sources across all tables
466
+ const pedimentoSources =
467
+ await this.scanApiService.fetchCrossTablePedimentoSources(
468
+ scanConfig.companySlug,
469
+ scanConfig.serverId,
470
+ scanConfig.basePathFull,
471
+ );
472
+
473
+ if (pedimentoSources.length === 0) {
474
+ console.log(
475
+ ' ā„¹ļø No pedimento sources found across tables. Skipping cross-table phase.\n',
476
+ );
477
+ return stats;
478
+ }
479
+
480
+ // Build a map: detected_pedimento → source info
481
+ const sourceMap = new Map();
482
+ for (const source of pedimentoSources) {
483
+ sourceMap.set(source.detected_pedimento, source);
484
+ }
485
+
486
+ console.log(
487
+ ` šŸ“‹ Found ${sourceMap.size} unique pedimento sources across ${tables.length} tables`,
488
+ );
489
+
490
+ // Step 2: For each table, find orphan files (have pedimento, no arela_path)
491
+ let totalOrphans = 0;
492
+
493
+ for (const table of tables) {
494
+ let offset = 0;
495
+ let hasMore = true;
496
+
497
+ while (hasMore) {
498
+ const orphanFiles =
499
+ await this.scanApiService.fetchFilesWithPedimentoNoArelaPath(
500
+ table.tableName,
501
+ offset,
502
+ this.options.batchSize,
503
+ );
504
+
505
+ if (orphanFiles.length === 0) {
506
+ hasMore = false;
507
+ break;
508
+ }
509
+
510
+ // Step 3: Match orphans against pedimento source map
511
+ const updates = [];
512
+ for (const file of orphanFiles) {
513
+ const source = sourceMap.get(file.detected_pedimento);
514
+ if (source) {
515
+ updates.push({
516
+ id: file.id,
517
+ arelaPath: source.arela_path,
518
+ rfc: source.rfc,
519
+ detectedPedimento: file.detected_pedimento,
520
+ detectedPedimentoYear: source.detected_pedimento_year,
521
+ propagatedFromId: source.source_id,
522
+ propagatedFromTable: source.source_table,
523
+ propagationError: null,
524
+ });
525
+ }
526
+ }
527
+
528
+ totalOrphans += orphanFiles.length;
529
+
530
+ // Step 4: Batch update matched files
531
+ if (updates.length > 0) {
532
+ try {
533
+ const result = await this.scanApiService.batchUpdatePropagation(
534
+ table.tableName,
535
+ updates,
536
+ );
537
+ stats.filesUpdated += result.updated;
538
+ stats.filesFailed += result.errors;
539
+ } catch (error) {
540
+ logger.error(
541
+ `Failed cross-table update on ${table.tableName}:`,
542
+ error,
543
+ );
544
+ stats.filesFailed += updates.length;
545
+ }
546
+ }
547
+
548
+ offset += orphanFiles.length;
549
+ if (orphanFiles.length < this.options.batchSize) {
550
+ hasMore = false;
551
+ }
552
+ }
553
+ }
554
+
555
+ console.log(` šŸ“Š Cross-table results:`);
556
+ console.log(` Orphan files checked: ${totalOrphans}`);
557
+ console.log(` Files updated: ${stats.filesUpdated}`);
558
+ console.log(` Files failed: ${stats.filesFailed}\n`);
559
+
560
+ return stats;
561
+ }
562
+
563
+ /**
564
+ * Show final propagation statistics
565
+ * @private
566
+ */
567
+ async #showFinalStats() {
568
+ try {
569
+ const stats = await this.scanApiService.getPropagationStats(
570
+ this.tableName,
571
+ );
572
+
573
+ console.log('šŸ“ˆ Final Status:');
574
+ console.log(` Total Files: ${stats.totalFiles.toLocaleString()}`);
575
+ console.log(
576
+ ` With arela_path: ${stats.withArelaPath.toLocaleString()}`,
577
+ );
578
+ console.log(
579
+ ` Needs Propagation: ${stats.needsPropagation.toLocaleString()}`,
580
+ );
581
+ console.log(` Pending: ${stats.pending.toLocaleString()}`);
582
+ console.log(` Errors: ${stats.errors.toLocaleString()}`);
583
+
584
+ if (stats.maxAttemptsReached > 0) {
585
+ console.log(
586
+ `\nāš ļø ${stats.maxAttemptsReached} files reached max propagation attempts.`,
587
+ );
588
+ console.log(
589
+ ' Run with increased max_propagation_attempts if needed, or review propagation errors.',
590
+ );
591
+ }
592
+
593
+ if (this.options.showStats) {
594
+ const duration = ((Date.now() - this.stats.startTime) / 1000).toFixed(
595
+ 2,
596
+ );
597
+ console.log('\nšŸ’» Performance Stats:');
598
+ console.log(` Total Duration: ${duration}s`);
599
+ console.log(` Memory Used: ${this.#getMemoryUsage()}`);
600
+ }
601
+ } catch (error) {
602
+ logger.error('Failed to fetch final stats:', error);
603
+ // Don't throw - command was successful even if we can't fetch final stats
604
+ }
605
+ }
606
+
607
+ /**
608
+ * Get formatted memory usage
609
+ * @private
610
+ */
611
+ #getMemoryUsage() {
612
+ const used = process.memoryUsage();
613
+ return `${Math.round(used.heapUsed / 1024 / 1024)}MB`;
614
+ }
615
+
616
+ /**
617
+ * Report progress to callback if available (for worker mode)
618
+ * @private
619
+ * @param {number} percent - Progress percentage (0-100)
620
+ * @param {string} message - Progress message
621
+ */
622
+ #reportProgress(percent, message) {
623
+ if (
624
+ this.options.onProgress &&
625
+ typeof this.options.onProgress === 'function'
626
+ ) {
627
+ try {
628
+ this.options.onProgress(percent, message);
629
+ } catch (error) {
630
+ logger.debug(`Progress callback error: ${error.message}`);
631
+ }
632
+ }
633
+ }
634
+ }
635
+
636
+ export default PropagateCommand;