@arela/uploader 0.3.0 → 1.0.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.
@@ -50,15 +50,168 @@ class Config {
50
50
 
51
51
  /**
52
52
  * Load API configuration
53
+ * Supports 3 API targets: default, agencia, cliente
54
+ * Configure ARELA_API_CLIENTE_URL/TOKEN for the specific client you want to use
53
55
  * @private
54
56
  */
55
57
  #loadApiConfig() {
56
58
  return {
59
+ // Default/legacy API (backward compatible)
57
60
  baseUrl: process.env.ARELA_API_URL,
58
61
  token: process.env.ARELA_API_TOKEN,
62
+ // Multi-tenant API targets (only 3 options)
63
+ targets: {
64
+ agencia: {
65
+ baseUrl:
66
+ process.env.ARELA_API_AGENCIA_URL || process.env.ARELA_API_URL,
67
+ token:
68
+ process.env.ARELA_API_AGENCIA_TOKEN || process.env.ARELA_API_TOKEN,
69
+ },
70
+ cliente: {
71
+ baseUrl:
72
+ process.env.ARELA_API_CLIENTE_URL || process.env.ARELA_API_URL,
73
+ token:
74
+ process.env.ARELA_API_CLIENTE_TOKEN || process.env.ARELA_API_TOKEN,
75
+ },
76
+ },
77
+ // Current active target (can be changed at runtime)
78
+ activeTarget: process.env.ARELA_API_TARGET || 'default',
59
79
  };
60
80
  }
61
81
 
82
+ /**
83
+ * Get API configuration for a specific target
84
+ * @param {string} target - API target: 'agencia', 'cliente', or 'default'
85
+ * @returns {Object} API configuration with baseUrl and token
86
+ */
87
+ getApiConfig(target = null) {
88
+ const targetName = target || this.api.activeTarget || 'default';
89
+
90
+ if (targetName === 'default') {
91
+ return {
92
+ baseUrl: this.api.baseUrl,
93
+ token: this.api.token,
94
+ };
95
+ }
96
+
97
+ const targetConfig = this.api.targets[targetName];
98
+ if (!targetConfig) {
99
+ console.warn(
100
+ `⚠️ Unknown API target '${targetName}', falling back to default`,
101
+ );
102
+ return {
103
+ baseUrl: this.api.baseUrl,
104
+ token: this.api.token,
105
+ };
106
+ }
107
+
108
+ return targetConfig;
109
+ }
110
+
111
+ /**
112
+ * Set the active API target at runtime
113
+ * @param {string} target - API target: 'default', 'agencia', or 'cliente'
114
+ */
115
+ setApiTarget(target) {
116
+ const validTargets = ['default', 'agencia', 'cliente'];
117
+ if (!validTargets.includes(target.toLowerCase())) {
118
+ throw new Error(
119
+ `Invalid API target '${target}'. Must be one of: ${validTargets.join(', ')}`,
120
+ );
121
+ }
122
+ this.api.activeTarget = target.toLowerCase();
123
+ const config = this.getApiConfig(target.toLowerCase());
124
+ console.log(`🎯 API target set to: ${target} → ${config.baseUrl}`);
125
+ }
126
+
127
+ /**
128
+ * Set source and target APIs for cross-tenant operations
129
+ * @param {string} sourceTarget - Source API target (for reading data)
130
+ * @param {string} targetTarget - Target API target (for writing data)
131
+ */
132
+ setCrossTenantTargets(sourceTarget, targetTarget) {
133
+ const validTargets = ['default', 'agencia', 'cliente'];
134
+
135
+ if (!validTargets.includes(sourceTarget.toLowerCase())) {
136
+ throw new Error(
137
+ `Invalid source API target '${sourceTarget}'. Must be one of: ${validTargets.join(', ')}`,
138
+ );
139
+ }
140
+ if (!validTargets.includes(targetTarget.toLowerCase())) {
141
+ throw new Error(
142
+ `Invalid target API target '${targetTarget}'. Must be one of: ${validTargets.join(', ')}`,
143
+ );
144
+ }
145
+
146
+ this.api.sourceTarget = sourceTarget.toLowerCase();
147
+ this.api.targetTarget = targetTarget.toLowerCase();
148
+
149
+ const sourceConfig = this.getApiConfig(sourceTarget.toLowerCase());
150
+ const targetConfig = this.getApiConfig(targetTarget.toLowerCase());
151
+
152
+ console.log(`🔗 Cross-tenant mode enabled:`);
153
+ console.log(
154
+ ` 📖 Source (read): ${sourceTarget} → ${sourceConfig.baseUrl}`,
155
+ );
156
+ console.log(
157
+ ` 📝 Target (write): ${targetTarget} → ${targetConfig.baseUrl}`,
158
+ );
159
+ }
160
+
161
+ /**
162
+ * Get source API config for cross-tenant operations
163
+ * @returns {Object} Source API configuration
164
+ */
165
+ getSourceApiConfig() {
166
+ if (this.api.sourceTarget) {
167
+ return this.getApiConfig(this.api.sourceTarget);
168
+ }
169
+ return this.getApiConfig();
170
+ }
171
+
172
+ /**
173
+ * Get target API config for cross-tenant operations
174
+ * @returns {Object} Target API configuration
175
+ */
176
+ getTargetApiConfig() {
177
+ if (this.api.targetTarget) {
178
+ return this.getApiConfig(this.api.targetTarget);
179
+ }
180
+ return this.getApiConfig();
181
+ }
182
+
183
+ /**
184
+ * Check if cross-tenant mode is enabled
185
+ * @returns {boolean}
186
+ */
187
+ isCrossTenantMode() {
188
+ return !!(
189
+ this.api.sourceTarget &&
190
+ this.api.targetTarget &&
191
+ this.api.sourceTarget !== this.api.targetTarget
192
+ );
193
+ }
194
+
195
+ /**
196
+ * Get API target by RFC
197
+ * @param {string} rfc - The RFC to find API target for
198
+ * @returns {string|null} The API target name or null if not found
199
+ */
200
+ getApiTargetByRfc(rfc) {
201
+ if (!rfc) return null;
202
+
203
+ const normalizedRfc = rfc.toUpperCase();
204
+ for (const [targetName, targetConfig] of Object.entries(this.api.targets)) {
205
+ if (
206
+ targetConfig.rfc &&
207
+ targetConfig.rfc.toUpperCase() === normalizedRfc
208
+ ) {
209
+ return targetName;
210
+ }
211
+ }
212
+ return null;
213
+ }
214
+
62
215
  /**
63
216
  * Load upload configuration
64
217
  * @private
@@ -169,10 +322,12 @@ class Config {
169
322
 
170
323
  /**
171
324
  * Check if API mode is available
325
+ * @param {string} target - Optional API target to check
172
326
  * @returns {boolean}
173
327
  */
174
- isApiModeAvailable() {
175
- return !!(this.api.baseUrl && this.api.token);
328
+ isApiModeAvailable(target = null) {
329
+ const config = this.getApiConfig(target);
330
+ return !!(config.baseUrl && config.token);
176
331
  }
177
332
 
178
333
  /**
@@ -19,13 +19,12 @@ export const supportDocumentDefinition = {
19
19
  ];
20
20
 
21
21
  // Detect customs-related metadata
22
- const customsClues = [
23
- /rfc|RFC/,
24
- /patente|aduana|customs|pedimento/i,
25
- ];
22
+ const customsClues = [/rfc|RFC/, /patente|aduana|customs|pedimento/i];
26
23
 
27
24
  const soapFound = soapClues.filter((clue) => clue.test(source)).length;
28
- const customsFound = customsClues.filter((clue) => clue.test(source)).length;
25
+ const customsFound = customsClues.filter((clue) =>
26
+ clue.test(source),
27
+ ).length;
29
28
 
30
29
  // Must have SOAP structure OR customs metadata
31
30
  return soapFound >= 2 || customsFound >= 2;
@@ -89,6 +89,7 @@ export class FileDetectionService {
89
89
  fields: [],
90
90
  detectedPedimento: null,
91
91
  detectedPedimentoYear: null,
92
+ rfc: null,
92
93
  arelaPath: null,
93
94
  text: '',
94
95
  error: `Unsupported file type: ${fileExtension}`,
@@ -102,6 +103,7 @@ export class FileDetectionService {
102
103
  fields: [],
103
104
  detectedPedimento: null,
104
105
  detectedPedimentoYear: null,
106
+ rfc: null,
105
107
  arelaPath: null,
106
108
  text: '',
107
109
  error: 'No text could be extracted from file',
@@ -112,6 +114,9 @@ export class FileDetectionService {
112
114
  const [detectedType, fields, detectedPedimento, detectedPedimentoYear] =
113
115
  extractDocumentFields(text, fileExtension, filePath);
114
116
 
117
+ // Extract RFC from fields
118
+ const rfc = fields?.find((f) => f.name === 'rfc')?.value ?? null;
119
+
115
120
  // Compose arela_path for pedimento_simplificado documents
116
121
  const arelaPath = composeArelaPath(
117
122
  detectedType,
@@ -125,6 +130,7 @@ export class FileDetectionService {
125
130
  fields,
126
131
  detectedPedimento,
127
132
  detectedPedimentoYear,
133
+ rfc,
128
134
  arelaPath,
129
135
  text,
130
136
  error: null,
@@ -135,6 +141,7 @@ export class FileDetectionService {
135
141
  detectedType: null,
136
142
  fields: [],
137
143
  detectedPedimento: null,
144
+ rfc: null,
138
145
  detectedPedimentoYear: null,
139
146
  arelaPath: null,
140
147
  text: '',
package/src/index.js CHANGED
@@ -47,6 +47,19 @@ class ArelaUploaderCLI {
47
47
  this.program
48
48
  .command('upload')
49
49
  .description('Upload files to Arela with automatic processing')
50
+ .option(
51
+ '--api <target>',
52
+ 'API target: default|agencia|cliente',
53
+ 'default',
54
+ )
55
+ .option(
56
+ '--source-api <target>',
57
+ 'Source API for reading data (cross-tenant mode): agencia|cliente',
58
+ )
59
+ .option(
60
+ '--target-api <target>',
61
+ 'Target API for uploading files (cross-tenant mode): agencia|cliente',
62
+ )
50
63
  .option(
51
64
  '-b, --batch-size <size>',
52
65
  'Number of files to process in each batch',
@@ -77,6 +90,17 @@ class ArelaUploaderCLI {
77
90
  )
78
91
  .action(async (options) => {
79
92
  try {
93
+ // Handle cross-tenant mode (source and target APIs)
94
+ if (options.sourceApi && options.targetApi) {
95
+ appConfig.setCrossTenantTargets(
96
+ options.sourceApi,
97
+ options.targetApi,
98
+ );
99
+ } else if (options.api && options.api !== 'default') {
100
+ // Set single API target if specified
101
+ appConfig.setApiTarget(options.api);
102
+ }
103
+
80
104
  // Handle --upload-by-rfc as a specific operation
81
105
  if (options.uploadByRfc) {
82
106
  const databaseService = await import(
@@ -87,6 +111,9 @@ class ArelaUploaderCLI {
87
111
  batchSize: parseInt(options.batchSize) || 10,
88
112
  showProgress: true,
89
113
  folderStructure: options.folderStructure,
114
+ apiTarget: options.api,
115
+ sourceApi: options.sourceApi,
116
+ targetApi: options.targetApi,
90
117
  });
91
118
  console.log(
92
119
  `✅ RFC upload completed: ${result.processedCount} processed, ${result.uploadedCount} uploaded, ${result.errorCount} errors`,
@@ -104,6 +131,11 @@ class ArelaUploaderCLI {
104
131
  this.program
105
132
  .command('stats')
106
133
  .description('Collect file statistics without uploading')
134
+ .option(
135
+ '--api <target>',
136
+ 'API target: agencia|cliente|default',
137
+ 'default',
138
+ )
107
139
  .option(
108
140
  '-b, --batch-size <size>',
109
141
  'Number of files to process in each batch',
@@ -121,6 +153,10 @@ class ArelaUploaderCLI {
121
153
  .option('--show-stats', 'Show performance statistics')
122
154
  .action(async (options) => {
123
155
  try {
156
+ // Set API target if specified
157
+ if (options.api && options.api !== 'default') {
158
+ appConfig.setApiTarget(options.api);
159
+ }
124
160
  const statsOptions = { ...options, statsOnly: true };
125
161
  await this.uploadCommand.execute(statsOptions);
126
162
  } catch (error) {
@@ -132,6 +168,11 @@ class ArelaUploaderCLI {
132
168
  this.program
133
169
  .command('detect')
134
170
  .description('Run document detection on existing file records')
171
+ .option(
172
+ '--api <target>',
173
+ 'API target: agencia|cliente|default',
174
+ 'default',
175
+ )
135
176
  .option(
136
177
  '-b, --batch-size <size>',
137
178
  'Number of files to process in each batch',
@@ -147,6 +188,11 @@ class ArelaUploaderCLI {
147
188
  )
148
189
  .action(async (options) => {
149
190
  try {
191
+ // Set API target if specified
192
+ if (options.api && options.api !== 'default') {
193
+ appConfig.setApiTarget(options.api);
194
+ }
195
+
150
196
  const databaseService = await import('./services/DatabaseService.js');
151
197
 
152
198
  // Handle --propagate-arela-path as a specific operation
@@ -237,6 +283,19 @@ class ArelaUploaderCLI {
237
283
  .description(
238
284
  'Monitor directories for file changes and upload automatically',
239
285
  )
286
+ .option(
287
+ '--api <target>',
288
+ 'API target: default|agencia|cliente',
289
+ 'default',
290
+ )
291
+ .option(
292
+ '--source-api <target>',
293
+ 'Source API for reading data (cross-tenant mode): agencia|cliente',
294
+ )
295
+ .option(
296
+ '--target-api <target>',
297
+ 'Target API for uploading files (cross-tenant mode): agencia|cliente',
298
+ )
240
299
  .option(
241
300
  '-d, --directories <paths>',
242
301
  'Comma-separated directories to watch',
@@ -267,6 +326,17 @@ class ArelaUploaderCLI {
267
326
  .option('--verbose', 'Enable verbose logging')
268
327
  .action(async (options) => {
269
328
  try {
329
+ // Handle cross-tenant mode (source and target APIs)
330
+ if (options.sourceApi && options.targetApi) {
331
+ appConfig.setCrossTenantTargets(
332
+ options.sourceApi,
333
+ options.targetApi,
334
+ );
335
+ } else if (options.api && options.api !== 'default') {
336
+ // Set single API target if specified
337
+ appConfig.setApiTarget(options.api);
338
+ }
339
+
270
340
  await this.watchCommand.execute(options);
271
341
  } catch (error) {
272
342
  this.errorHandler.handleFatalError(error, { command: 'watch' });
@@ -330,9 +400,27 @@ class ArelaUploaderCLI {
330
400
  #showConfiguration() {
331
401
  console.log('🔧 Current Configuration:');
332
402
  console.log(` Version: ${appConfig.packageVersion}`);
333
- console.log('\n📡 API Configuration:');
334
- console.log(` Base URL: ${appConfig.api.baseUrl || 'Not configured'}`);
335
- console.log(` Token: ${appConfig.api.token ? '✅ Set' : '❌ Not set'}`);
403
+ console.log('\n📡 API Configuration (Multi-Tenant):');
404
+ console.log(` Active Target: ${appConfig.api.activeTarget || 'default'}`);
405
+ console.log('\n 🌐 Default API:');
406
+ console.log(` URL: ${appConfig.api.baseUrl || 'Not configured'}`);
407
+ console.log(
408
+ ` Token: ${appConfig.api.token ? '✅ Set' : '❌ Not set'}`,
409
+ );
410
+ console.log('\n 🏢 Agencia API:');
411
+ console.log(
412
+ ` URL: ${appConfig.api.targets.agencia?.baseUrl || 'Not configured'}`,
413
+ );
414
+ console.log(
415
+ ` Token: ${appConfig.api.targets.agencia?.token ? '✅ Set' : '❌ Not set'}`,
416
+ );
417
+ console.log('\n 👤 Cliente API:');
418
+ console.log(
419
+ ` URL: ${appConfig.api.targets.cliente?.baseUrl || 'Not configured'}`,
420
+ );
421
+ console.log(
422
+ ` Token: ${appConfig.api.targets.cliente?.token ? '✅ Set' : '❌ Not set'}`,
423
+ );
336
424
  console.log('\n🗄️ Supabase Configuration:');
337
425
  console.log(` URL: ${appConfig.supabase.url || 'Not configured'}`);
338
426
  console.log(` Key: ${appConfig.supabase.key ? '✅ Set' : '❌ Not set'}`);
@@ -360,11 +448,38 @@ class ArelaUploaderCLI {
360
448
  console.log(` Log File: ${appConfig.logging.logFilePath}`);
361
449
  console.log('\n🎯 Service Availability:');
362
450
  console.log(
363
- ` API Mode: ${appConfig.isApiModeAvailable() ? '✅ Available' : '❌ Not available'}`,
451
+ ` API Mode (default): ${appConfig.isApiModeAvailable() ? '✅ Available' : '❌ Not available'}`,
452
+ );
453
+ console.log(
454
+ ` API Mode (agencia): ${appConfig.isApiModeAvailable('agencia') ? '✅ Available' : '❌ Not available'}`,
455
+ );
456
+ console.log(
457
+ ` API Mode (cliente): ${appConfig.isApiModeAvailable('cliente') ? '✅ Available' : '❌ Not available'}`,
364
458
  );
365
459
  console.log(
366
460
  ` Supabase Mode: ${appConfig.isSupabaseModeAvailable() ? '✅ Available' : '❌ Not available'}`,
367
461
  );
462
+ console.log('\n📋 Uso Multi-Tenant:');
463
+ console.log(
464
+ ' arela stats --api agencia # Registrar archivos en BD agencia',
465
+ );
466
+ console.log(
467
+ ' arela detect --api agencia # Detectar pedimentos en BD agencia',
468
+ );
469
+ console.log(
470
+ ' arela upload --api cliente # Subir archivos al cliente configurado',
471
+ );
472
+ console.log('\n👁️ Modo Watch Multi-Tenant:');
473
+ console.log(
474
+ ' arela watch --api cliente # Watch con API cliente',
475
+ );
476
+ console.log(
477
+ ' arela watch --source-api agencia --target-api cliente # Cross-tenant watch',
478
+ );
479
+ console.log(
480
+ '\n💡 Tip: Configura ARELA_API_CLIENTE_URL y ARELA_API_CLIENTE_TOKEN',
481
+ );
482
+ console.log(' en .env para apuntar al cliente específico que necesites.');
368
483
  }
369
484
 
370
485
  /**