@lapyme/arca 0.1.0 → 0.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/dist/index.js CHANGED
@@ -13,6 +13,14 @@ var ArcaConfigurationError = class extends ArcaError {
13
13
  super(message, "ARCA_CONFIGURATION_ERROR", options);
14
14
  }
15
15
  };
16
+ var ArcaInputError = class extends ArcaError {
17
+ name = "ArcaInputError";
18
+ detail;
19
+ constructor(message, options) {
20
+ super(message, "ARCA_INPUT_ERROR", options);
21
+ this.detail = options?.detail;
22
+ }
23
+ };
16
24
  var ArcaTransportError = class extends ArcaError {
17
25
  name = "ArcaTransportError";
18
26
  statusCode;
@@ -50,15 +58,17 @@ var ARCA_ENV_VARIABLES = {
50
58
  taxId: "ARCA_TAX_ID",
51
59
  certificatePem: "ARCA_CERTIFICATE_PEM",
52
60
  privateKeyPem: "ARCA_PRIVATE_KEY_PEM",
53
- environment: "ARCA_ENVIRONMENT",
54
- wsaaCacheMode: "ARCA_WSAA_CACHE_MODE",
55
- wsaaCacheDirectory: "ARCA_WSAA_CACHE_DIRECTORY"
61
+ environment: "ARCA_ENVIRONMENT"
56
62
  };
57
63
  var PRIVATE_KEY_PEM_PREFIXES = [
58
64
  "-----BEGIN PRIVATE KEY-----",
59
65
  "-----BEGIN RSA PRIVATE KEY-----",
60
66
  "-----BEGIN ENCRYPTED PRIVATE KEY-----"
61
67
  ];
68
+ var VALID_ARCA_LOG_LEVELS = ["debug", "info", "warn", "error"];
69
+ var DEFAULT_ARCA_TIMEOUT_MS = 3e4;
70
+ var DEFAULT_ARCA_RETRIES = 0;
71
+ var DEFAULT_ARCA_RETRY_DELAY_MS = 500;
62
72
  function resolveArcaEnvironment(production) {
63
73
  return production ? "production" : "test";
64
74
  }
@@ -70,34 +80,21 @@ function createArcaClientConfigFromEnv(options = {}) {
70
80
  };
71
81
  const environmentInput = readEnv(env, variableNames.environment);
72
82
  const environmentValue = normalizeEnvironmentValue(environmentInput);
73
- const cacheModeInput = readEnv(env, variableNames.wsaaCacheMode);
74
- const cacheModeValue = normalizeCacheMode(cacheModeInput);
75
83
  const config = {
76
84
  taxId: readEnv(env, variableNames.taxId) ?? "",
77
85
  certificatePem: readEnv(env, variableNames.certificatePem) ?? "",
78
86
  privateKeyPem: readEnv(env, variableNames.privateKeyPem) ?? "",
79
87
  environment: environmentValue ?? environmentInput ?? options.defaultEnvironment ?? "test"
80
88
  };
81
- if (cacheModeValue === "disk" || cacheModeInput === "disk") {
82
- config.wsaa = {
83
- cache: {
84
- mode: cacheModeValue ?? cacheModeInput,
85
- directory: readEnv(env, variableNames.wsaaCacheDirectory) ?? ""
86
- }
87
- };
88
- } else if (cacheModeValue === "memory" || cacheModeInput !== void 0) {
89
- config.wsaa = {
90
- cache: {
91
- mode: cacheModeValue ?? cacheModeInput
92
- }
93
- };
94
- }
95
89
  assertArcaClientConfig(config);
96
90
  return normalizeArcaClientConfig(config);
97
91
  }
98
92
  function assertArcaClientConfig(config) {
99
93
  const invalidFields = [];
100
94
  const normalized = normalizeArcaClientConfig(config);
95
+ const timeout = normalized.timeout ?? DEFAULT_ARCA_TIMEOUT_MS;
96
+ const retries = normalized.retries ?? DEFAULT_ARCA_RETRIES;
97
+ const retryDelay = normalized.retryDelay ?? DEFAULT_ARCA_RETRY_DELAY_MS;
101
98
  if (!/^\d{11}$/.test(normalized.taxId)) {
102
99
  invalidFields.push("taxId");
103
100
  }
@@ -112,15 +109,21 @@ function assertArcaClientConfig(config) {
112
109
  if (!ARCA_ENVIRONMENTS.includes(normalized.environment)) {
113
110
  invalidFields.push("environment");
114
111
  }
115
- const cacheConfig = config.wsaa?.cache;
116
- if (cacheConfig) {
117
- const cacheMode = cacheConfig.mode?.trim().toLowerCase() ?? "memory";
118
- if (cacheMode !== "memory" && cacheMode !== "disk") {
119
- invalidFields.push("wsaa.cache.mode");
120
- }
121
- if (cacheMode === "disk" && (!("directory" in cacheConfig) || cacheConfig.directory.trim().length < 1)) {
122
- invalidFields.push("wsaa.cache.directory");
123
- }
112
+ if (!Number.isFinite(timeout) || timeout <= 0) {
113
+ invalidFields.push("timeout");
114
+ }
115
+ if (!Number.isInteger(retries) || retries < 0) {
116
+ invalidFields.push("retries");
117
+ }
118
+ if (!Number.isFinite(retryDelay) || retryDelay < 0) {
119
+ invalidFields.push("retryDelay");
120
+ }
121
+ const loggerLevel = normalized.logger?.level;
122
+ if (loggerLevel !== void 0 && !VALID_ARCA_LOG_LEVELS.includes(loggerLevel)) {
123
+ invalidFields.push("logger.level");
124
+ }
125
+ if (normalized.logger?.log !== void 0 && typeof normalized.logger.log !== "function") {
126
+ invalidFields.push("logger.log");
124
127
  }
125
128
  if (invalidFields.length > 0) {
126
129
  throw new ArcaConfigurationError(
@@ -191,32 +194,21 @@ function getArcaServiceConfig(service) {
191
194
  }
192
195
  function normalizeArcaClientConfig(config) {
193
196
  const normalizedEnvironment = normalizeEnvironmentValue(String(config.environment)) ?? config.environment;
194
- const normalizedCache = normalizeWsaaCacheConfig(config.wsaa?.cache);
197
+ const normalizedLoggerLevel = normalizeLogLevelValue(config.logger?.level);
195
198
  return {
196
199
  taxId: config.taxId.trim(),
197
200
  certificatePem: config.certificatePem.trim(),
198
201
  privateKeyPem: config.privateKeyPem.trim(),
199
202
  environment: normalizedEnvironment,
200
- ...normalizedCache ? {
201
- wsaa: {
202
- cache: normalizedCache
203
+ timeout: config.timeout ?? DEFAULT_ARCA_TIMEOUT_MS,
204
+ retries: config.retries ?? DEFAULT_ARCA_RETRIES,
205
+ retryDelay: config.retryDelay ?? DEFAULT_ARCA_RETRY_DELAY_MS,
206
+ ...config.logger === void 0 ? {} : {
207
+ logger: {
208
+ ...config.logger,
209
+ ...normalizedLoggerLevel === void 0 ? {} : { level: normalizedLoggerLevel }
203
210
  }
204
- } : {}
205
- };
206
- }
207
- function normalizeWsaaCacheConfig(cache) {
208
- if (!cache) {
209
- return void 0;
210
- }
211
- const mode = normalizeCacheMode(cache.mode) ?? "memory";
212
- if (mode !== "disk") {
213
- return {
214
- mode: "memory"
215
- };
216
- }
217
- return {
218
- mode,
219
- directory: ("directory" in cache ? cache.directory : "").trim()
211
+ }
220
212
  };
221
213
  }
222
214
  function normalizeEnvironmentValue(value) {
@@ -229,18 +221,69 @@ function normalizeEnvironmentValue(value) {
229
221
  }
230
222
  return void 0;
231
223
  }
232
- function normalizeCacheMode(value) {
224
+ function readEnv(env, variableName) {
225
+ return env[variableName]?.trim() || void 0;
226
+ }
227
+ function normalizeLogLevelValue(value) {
233
228
  if (!value) {
234
229
  return void 0;
235
230
  }
236
231
  const normalized = value.trim().toLowerCase();
237
- if (normalized === "memory" || normalized === "disk") {
232
+ if (VALID_ARCA_LOG_LEVELS.includes(normalized)) {
238
233
  return normalized;
239
234
  }
240
- return void 0;
235
+ return value;
241
236
  }
242
- function readEnv(env, variableName) {
243
- return env[variableName]?.trim() || void 0;
237
+
238
+ // src/internal/logger.ts
239
+ var ARCA_LOG_LEVELS = ["debug", "info", "warn", "error"];
240
+ function createArcaLogger(config) {
241
+ const disabled = config?.disabled ?? false;
242
+ const level = resolveArcaLogLevel(config?.level);
243
+ const sink = config?.log ?? defaultArcaLog;
244
+ const log = (messageLevel, message, ...args) => {
245
+ if (disabled || !shouldLog(level, messageLevel)) {
246
+ return;
247
+ }
248
+ sink(messageLevel, message, ...args);
249
+ };
250
+ return {
251
+ disabled,
252
+ level,
253
+ log,
254
+ debug(message, ...args) {
255
+ log("debug", message, ...args);
256
+ },
257
+ info(message, ...args) {
258
+ log("info", message, ...args);
259
+ },
260
+ warn(message, ...args) {
261
+ log("warn", message, ...args);
262
+ },
263
+ error(message, ...args) {
264
+ log("error", message, ...args);
265
+ }
266
+ };
267
+ }
268
+ function resolveArcaLogLevel(level) {
269
+ if (isArcaLogLevel(level)) {
270
+ return level;
271
+ }
272
+ const envLevel = process.env.ARCA_LOG_LEVEL?.trim().toLowerCase();
273
+ if (isArcaLogLevel(envLevel)) {
274
+ return envLevel;
275
+ }
276
+ return "warn";
277
+ }
278
+ function shouldLog(threshold, messageLevel) {
279
+ return ARCA_LOG_LEVELS.indexOf(messageLevel) >= ARCA_LOG_LEVELS.indexOf(threshold);
280
+ }
281
+ function isArcaLogLevel(value) {
282
+ return ARCA_LOG_LEVELS.includes(value);
283
+ }
284
+ function defaultArcaLog(level, message, ...args) {
285
+ const method = level === "debug" ? console.debug : level === "info" ? console.info : level === "warn" ? console.warn : console.error;
286
+ method(message, ...args);
244
287
  }
245
288
 
246
289
  // src/services/padron.ts
@@ -262,7 +305,7 @@ function createPadronService(options) {
262
305
  const datosGenerales = record.datosGenerales;
263
306
  return {
264
307
  taxId: String(record.idPersona ?? ""),
265
- ...record.tipoPersona !== void 0 ? { personType: String(record.tipoPersona) } : {},
308
+ ...record.tipoPersona === void 0 ? {} : { personType: String(record.tipoPersona) },
266
309
  ...datosGenerales ? { name: extractPadronName(datosGenerales) } : {},
267
310
  raw: record
268
311
  };
@@ -281,7 +324,7 @@ function createPadronService(options) {
281
324
  }
282
325
  const record = raw;
283
326
  const idPersona = record.idPersona;
284
- const taxIds = Array.isArray(idPersona) ? idPersona.map(String) : idPersona !== void 0 ? [String(idPersona)] : [];
327
+ const taxIds = Array.isArray(idPersona) ? idPersona.map(String) : idPersona === void 0 ? [] : [String(idPersona)];
285
328
  return {
286
329
  taxIds,
287
330
  raw: record
@@ -331,7 +374,10 @@ async function executePadronOperation(options, service, operation, body) {
331
374
  }
332
375
  return operationResponse.return ?? null;
333
376
  } catch (error) {
334
- if (error instanceof ArcaSoapFaultError && error.message.toLowerCase().includes("no existe")) {
377
+ if (error instanceof ArcaSoapFaultError && // Public Padron A5/A13 WSDLs expose only a generic validation fault, so
378
+ // there is no documented not-found-specific fault code to match here.
379
+ // Keep the current message fallback, but treat it as fragile.
380
+ error.message.toLowerCase().includes("no existe")) {
335
381
  return null;
336
382
  }
337
383
  throw error;
@@ -340,43 +386,68 @@ async function executePadronOperation(options, service, operation, body) {
340
386
 
341
387
  // src/services/wsfe.ts
342
388
  function createWsfeService(options) {
343
- async function getLastVoucher({
344
- representedTaxId,
345
- salesPoint,
346
- voucherType,
347
- forceAuthRefresh
348
- }) {
389
+ async function executeWsfeAuthenticatedOperation(operation, input, body = {}) {
349
390
  const auth = await options.auth.login("wsfe", {
350
- representedTaxId,
351
- forceRefresh: forceAuthRefresh
391
+ representedTaxId: input.representedTaxId,
392
+ forceRefresh: input.forceAuthRefresh
352
393
  });
353
394
  const response = await options.soap.execute({
354
395
  service: "wsfe",
355
- operation: "FECompUltimoAutorizado",
396
+ operation,
356
397
  body: {
357
398
  Auth: createWsfeAuth(
358
- representedTaxId ?? options.config.taxId,
399
+ input.representedTaxId ?? options.config.taxId,
359
400
  auth.token,
360
401
  auth.sign
361
402
  ),
362
- PtoVta: salesPoint,
363
- CbteTipo: voucherType
403
+ ...body
364
404
  }
365
405
  });
366
- const result = unwrapWsfeOperationResult(
406
+ return unwrapWsfeOperationResult(operation, response.result);
407
+ }
408
+ async function executeWsfeOperation(operation, body = {}) {
409
+ const response = await options.soap.execute({
410
+ service: "wsfe",
411
+ operation,
412
+ body
413
+ });
414
+ return unwrapWsfeOperationResult(operation, response.result);
415
+ }
416
+ async function getNextVoucherNumber({
417
+ representedTaxId,
418
+ salesPoint,
419
+ voucherType,
420
+ forceAuthRefresh
421
+ }) {
422
+ const result = await executeWsfeAuthenticatedOperation(
367
423
  "FECompUltimoAutorizado",
368
- response.result
424
+ {
425
+ representedTaxId,
426
+ forceAuthRefresh
427
+ },
428
+ {
429
+ PtoVta: salesPoint,
430
+ CbteTipo: voucherType
431
+ }
369
432
  );
370
433
  return Number(result.CbteNro ?? 0) + 1;
371
434
  }
435
+ async function getWsfeCatalog(operation, resultKey, input) {
436
+ const result = await executeWsfeAuthenticatedOperation(operation, {
437
+ representedTaxId: input.representedTaxId,
438
+ forceAuthRefresh: input.forceAuthRefresh
439
+ });
440
+ return getWsfeResultEntries(result, resultKey).map(mapWsfeCatalogEntry);
441
+ }
372
442
  return {
373
443
  async createNextVoucher({ representedTaxId, data }) {
374
- const voucherNumber = await getLastVoucher({
444
+ const normalizedInput = normalizeWsfeVoucherInput(data);
445
+ const voucherNumber = await getNextVoucherNumber({
375
446
  representedTaxId,
376
- salesPoint: data.salesPoint,
377
- voucherType: data.voucherType
447
+ salesPoint: normalizedInput.salesPoint,
448
+ voucherType: normalizedInput.voucherType
378
449
  });
379
- const requestData = mapWsfeVoucherInput(data, voucherNumber);
450
+ const requestData = mapWsfeVoucherInput(normalizedInput, voucherNumber);
380
451
  const auth = await options.auth.login("wsfe", { representedTaxId });
381
452
  const response = await options.soap.execute({
382
453
  service: "wsfe",
@@ -390,8 +461,8 @@ function createWsfeService(options) {
390
461
  FeCAEReq: {
391
462
  FeCabReq: {
392
463
  CantReg: 1,
393
- PtoVta: data.salesPoint,
394
- CbteTipo: data.voucherType
464
+ PtoVta: normalizedInput.salesPoint,
465
+ CbteTipo: normalizedInput.voucherType
395
466
  },
396
467
  FeDetReq: {
397
468
  FECAEDetRequest: requestData
@@ -419,61 +490,89 @@ function createWsfeService(options) {
419
490
  raw: result
420
491
  };
421
492
  },
422
- getLastVoucher,
493
+ getNextVoucherNumber,
494
+ getLastVoucher(input) {
495
+ return getNextVoucherNumber(input);
496
+ },
423
497
  async getSalesPoints({ representedTaxId, forceAuthRefresh }) {
424
- const auth = await options.auth.login("wsfe", {
425
- representedTaxId,
426
- forceRefresh: forceAuthRefresh
427
- });
428
- const response = await options.soap.execute({
429
- service: "wsfe",
430
- operation: "FEParamGetPtosVenta",
431
- body: {
432
- Auth: createWsfeAuth(
433
- representedTaxId ?? options.config.taxId,
434
- auth.token,
435
- auth.sign
436
- )
437
- }
438
- });
439
- const result = unwrapWsfeOperationResult(
498
+ const result = await executeWsfeAuthenticatedOperation(
440
499
  "FEParamGetPtosVenta",
441
- response.result
500
+ {
501
+ representedTaxId,
502
+ forceAuthRefresh
503
+ }
442
504
  );
443
- const resultGet = result.ResultGet;
444
- const rawPoints = resultGet?.PtoVenta;
505
+ const rawPoints = result.ResultGet?.PtoVenta;
445
506
  if (!rawPoints) {
446
507
  return [];
447
508
  }
448
509
  const entries = Array.isArray(rawPoints) ? rawPoints : [rawPoints];
449
510
  return entries.map(mapWsfeSalesPoint);
450
511
  },
512
+ getVoucherTypes(input) {
513
+ return getWsfeCatalog("FEParamGetTiposCbte", "CbteTipo", input);
514
+ },
515
+ getDocumentTypes(input) {
516
+ return getWsfeCatalog("FEParamGetTiposDoc", "DocTipo", input);
517
+ },
518
+ getConceptTypes(input) {
519
+ return getWsfeCatalog("FEParamGetTiposConcepto", "ConceptoTipo", input);
520
+ },
521
+ async getCurrencyTypes({ representedTaxId, forceAuthRefresh }) {
522
+ const result = await executeWsfeAuthenticatedOperation(
523
+ "FEParamGetTiposMonedas",
524
+ {
525
+ representedTaxId,
526
+ forceAuthRefresh
527
+ }
528
+ );
529
+ return getWsfeResultEntries(result, "Moneda").map(mapWsfeCurrencyType);
530
+ },
531
+ getVatRates(input) {
532
+ return getWsfeCatalog("FEParamGetTiposIva", "IvaTipo", input);
533
+ },
534
+ getTaxTypes(input) {
535
+ return getWsfeCatalog("FEParamGetTiposTributos", "TributoTipo", input);
536
+ },
537
+ getOptionalTypes(input) {
538
+ return getWsfeCatalog("FEParamGetTiposOpcional", "OpcionalTipo", input);
539
+ },
540
+ async getServerStatus() {
541
+ const result = await executeWsfeOperation("FEDummy");
542
+ return mapWsfeServerStatus(result);
543
+ },
544
+ async getQuotation({ currencyId, representedTaxId, forceAuthRefresh }) {
545
+ const result = await executeWsfeAuthenticatedOperation(
546
+ "FEParamGetCotizacion",
547
+ {
548
+ representedTaxId,
549
+ forceAuthRefresh
550
+ },
551
+ {
552
+ MonId: currencyId
553
+ }
554
+ );
555
+ const raw = result.ResultGet ?? {};
556
+ return mapWsfeQuotation(raw);
557
+ },
451
558
  async getVoucherInfo({
452
559
  representedTaxId,
453
560
  number,
454
561
  salesPoint,
455
562
  voucherType
456
563
  }) {
457
- const auth = await options.auth.login("wsfe", { representedTaxId });
458
- const response = await options.soap.execute({
459
- service: "wsfe",
460
- operation: "FECompConsultar",
461
- body: {
462
- Auth: createWsfeAuth(
463
- representedTaxId ?? options.config.taxId,
464
- auth.token,
465
- auth.sign
466
- ),
564
+ const result = await executeWsfeAuthenticatedOperation(
565
+ "FECompConsultar",
566
+ {
567
+ representedTaxId
568
+ },
569
+ {
467
570
  FeCompConsReq: {
468
571
  CbteNro: number,
469
572
  PtoVta: salesPoint,
470
573
  CbteTipo: voucherType
471
574
  }
472
575
  }
473
- });
474
- const result = unwrapWsfeOperationResult(
475
- "FECompConsultar",
476
- response.result
477
576
  );
478
577
  const raw = result.ResultGet ?? null;
479
578
  if (!raw) {
@@ -520,8 +619,8 @@ function mapWsfeVoucherInput(input, voucherNumber) {
520
619
  Tipo: v.type,
521
620
  PtoVta: v.salesPoint,
522
621
  Nro: v.number,
523
- ...v.taxId !== void 0 ? { Cuit: v.taxId } : {},
524
- ...v.voucherDate !== void 0 ? { CbteFch: v.voucherDate } : {}
622
+ ...v.taxId === void 0 ? {} : { Cuit: v.taxId },
623
+ ...v.voucherDate === void 0 ? {} : { CbteFch: v.voucherDate }
525
624
  }))
526
625
  };
527
626
  }
@@ -529,7 +628,7 @@ function mapWsfeVoucherInput(input, voucherNumber) {
529
628
  data.Tributos = {
530
629
  Tributo: input.taxes.map((t) => ({
531
630
  Id: t.id,
532
- ...t.description !== void 0 ? { Desc: t.description } : {},
631
+ ...t.description === void 0 ? {} : { Desc: t.description },
533
632
  BaseImp: t.baseAmount,
534
633
  Alic: t.rate,
535
634
  Importe: t.amount
@@ -564,25 +663,144 @@ function mapWsfeVoucherInput(input, voucherNumber) {
564
663
  }
565
664
  return data;
566
665
  }
666
+ function normalizeWsfeVoucherInput(input) {
667
+ const {
668
+ voucherDate,
669
+ serviceStartDate,
670
+ serviceEndDate,
671
+ paymentDueDate,
672
+ associatedVouchers,
673
+ ...rest
674
+ } = input;
675
+ return {
676
+ ...rest,
677
+ voucherDate: normalizeWsfeDateInput(voucherDate, "voucherDate"),
678
+ ...serviceStartDate === void 0 ? {} : {
679
+ serviceStartDate: normalizeWsfeDateInput(
680
+ serviceStartDate,
681
+ "serviceStartDate"
682
+ )
683
+ },
684
+ ...serviceEndDate === void 0 ? {} : {
685
+ serviceEndDate: normalizeWsfeDateInput(
686
+ serviceEndDate,
687
+ "serviceEndDate"
688
+ )
689
+ },
690
+ ...paymentDueDate === void 0 ? {} : {
691
+ paymentDueDate: normalizeWsfeDateInput(
692
+ paymentDueDate,
693
+ "paymentDueDate"
694
+ )
695
+ },
696
+ ...associatedVouchers === void 0 ? {} : {
697
+ associatedVouchers: associatedVouchers.map((voucher, index) => {
698
+ const { voucherDate: associatedVoucherDate, ...associatedRest } = voucher;
699
+ return {
700
+ ...associatedRest,
701
+ ...associatedVoucherDate === void 0 ? {} : {
702
+ voucherDate: normalizeWsfeDateInput(
703
+ associatedVoucherDate,
704
+ `associatedVouchers[${index}].voucherDate`
705
+ )
706
+ }
707
+ };
708
+ })
709
+ }
710
+ };
711
+ }
712
+ function normalizeWsfeDateInput(value, fieldName) {
713
+ if (typeof value !== "string") {
714
+ throw new ArcaInputError(
715
+ `Invalid WSFE ${fieldName}: expected a YYYY-MM-DD or YYYYMMDD string`,
716
+ {
717
+ detail: { field: fieldName, value }
718
+ }
719
+ );
720
+ }
721
+ const normalizedValue = value.trim();
722
+ const afipMatch = normalizedValue.match(/^(\d{4})(\d{2})(\d{2})$/);
723
+ if (afipMatch) {
724
+ const [, year, month, day] = afipMatch;
725
+ assertValidCalendarDate(year, month, day, fieldName, normalizedValue);
726
+ return normalizedValue;
727
+ }
728
+ const isoMatch = normalizedValue.match(/^(\d{4})-(\d{2})-(\d{2})$/);
729
+ if (isoMatch) {
730
+ const [, year, month, day] = isoMatch;
731
+ assertValidCalendarDate(year, month, day, fieldName, normalizedValue);
732
+ return `${year}${month}${day}`;
733
+ }
734
+ throw new ArcaInputError(
735
+ `Invalid WSFE ${fieldName}: expected a YYYY-MM-DD or YYYYMMDD string`,
736
+ {
737
+ detail: { field: fieldName, value: normalizedValue }
738
+ }
739
+ );
740
+ }
741
+ function assertValidCalendarDate(yearInput, monthInput, dayInput, fieldName, value) {
742
+ const year = Number(yearInput);
743
+ const month = Number(monthInput);
744
+ const day = Number(dayInput);
745
+ const candidate = new Date(Date.UTC(year, month - 1, day));
746
+ if (candidate.getUTCFullYear() !== year || candidate.getUTCMonth() !== month - 1 || candidate.getUTCDate() !== day) {
747
+ throw new ArcaInputError(
748
+ `Invalid WSFE ${fieldName}: received a non-existent calendar date`,
749
+ {
750
+ detail: { field: fieldName, value }
751
+ }
752
+ );
753
+ }
754
+ }
567
755
  function mapWsfeSalesPoint(raw) {
568
756
  const record = raw;
569
757
  return {
570
758
  number: Number(record.Nro ?? 0),
571
- ...record.EmisionTipo !== void 0 ? { emissionType: String(record.EmisionTipo) } : {},
572
- ...record.Bloqueado !== void 0 ? { blocked: String(record.Bloqueado) } : {},
573
- ...record.FchBaja !== void 0 ? { deletedSince: String(record.FchBaja) } : {}
759
+ ...record.EmisionTipo === void 0 ? {} : { emissionType: String(record.EmisionTipo) },
760
+ ...record.Bloqueado === void 0 ? {} : { blocked: String(record.Bloqueado) },
761
+ ...record.FchBaja === void 0 ? {} : { deletedSince: String(record.FchBaja) }
762
+ };
763
+ }
764
+ function mapWsfeCatalogEntry(raw) {
765
+ const record = raw;
766
+ return {
767
+ id: Number(record.Id ?? 0),
768
+ description: String(record.Desc ?? "")
769
+ };
770
+ }
771
+ function mapWsfeCurrencyType(raw) {
772
+ const record = raw;
773
+ return {
774
+ id: String(record.Id ?? ""),
775
+ description: String(record.Desc ?? ""),
776
+ validFrom: String(record.FchDesde ?? ""),
777
+ validTo: String(record.FchHasta ?? "")
778
+ };
779
+ }
780
+ function mapWsfeServerStatus(raw) {
781
+ return {
782
+ appServer: String(raw.AppServer ?? ""),
783
+ dbServer: String(raw.DbServer ?? ""),
784
+ authServer: String(raw.AuthServer ?? "")
785
+ };
786
+ }
787
+ function mapWsfeQuotation(raw) {
788
+ return {
789
+ currencyId: String(raw.MonId ?? ""),
790
+ rate: Number(raw.MonCotiz ?? 0),
791
+ date: String(raw.FchCotiz ?? "")
574
792
  };
575
793
  }
576
794
  function mapWsfeVoucherInfo(raw) {
577
795
  return {
578
796
  voucherNumber: Number(raw.CbteDesde ?? raw.CbteHasta ?? 0),
579
- ...raw.CbteFch !== void 0 ? { voucherDate: String(raw.CbteFch) } : {},
580
- ...raw.PtoVta !== void 0 ? { salesPoint: Number(raw.PtoVta) } : {},
581
- ...raw.CbteTipo !== void 0 ? { voucherType: Number(raw.CbteTipo) } : {},
582
- ...raw.ImpTotal !== void 0 ? { totalAmount: Number(raw.ImpTotal) } : {},
583
- ...raw.Resultado !== void 0 ? { result: String(raw.Resultado) } : {},
584
- ...raw.CAE !== void 0 ? { cae: String(raw.CAE) } : {},
585
- ...raw.CAEFchVto !== void 0 ? { caeExpiry: String(raw.CAEFchVto) } : {},
797
+ ...raw.CbteFch === void 0 ? {} : { voucherDate: String(raw.CbteFch) },
798
+ ...raw.PtoVta === void 0 ? {} : { salesPoint: Number(raw.PtoVta) },
799
+ ...raw.CbteTipo === void 0 ? {} : { voucherType: Number(raw.CbteTipo) },
800
+ ...raw.ImpTotal === void 0 ? {} : { totalAmount: Number(raw.ImpTotal) },
801
+ ...raw.Resultado === void 0 ? {} : { result: String(raw.Resultado) },
802
+ ...raw.CAE === void 0 ? {} : { cae: String(raw.CAE) },
803
+ ...raw.CAEFchVto === void 0 ? {} : { caeExpiry: String(raw.CAEFchVto) },
586
804
  raw
587
805
  };
588
806
  }
@@ -654,6 +872,15 @@ function normalizeWsfeErrors(rawErrors) {
654
872
  };
655
873
  });
656
874
  }
875
+ function getWsfeResultEntries(result, key) {
876
+ const rawEntries = result.ResultGet?.[key];
877
+ if (!rawEntries) {
878
+ return [];
879
+ }
880
+ return (Array.isArray(rawEntries) ? rawEntries : [rawEntries]).map(
881
+ (entry) => entry
882
+ );
883
+ }
657
884
 
658
885
  // src/services/wsmtxca.ts
659
886
  function createWsmtxcaService(options) {
@@ -888,13 +1115,69 @@ var legacyTlsAgent = new https.Agent({
888
1115
  keepAlive: true,
889
1116
  ciphers: "DEFAULT@SECLEVEL=0"
890
1117
  });
891
- var REQUEST_TIMEOUT_MS = 3e4;
892
1118
  async function postXml({
893
1119
  url,
894
1120
  body,
895
1121
  contentType,
896
1122
  soapAction,
897
- useLegacyTlsSecurityLevel0 = false
1123
+ useLegacyTlsSecurityLevel0 = false,
1124
+ timeout = 3e4,
1125
+ retries = 0,
1126
+ retryDelay = 500,
1127
+ logger,
1128
+ service,
1129
+ operation
1130
+ }) {
1131
+ const totalAttempts = retries + 1;
1132
+ for (let attempt = 1; attempt <= totalAttempts; attempt += 1) {
1133
+ try {
1134
+ return await postXmlOnce({
1135
+ url,
1136
+ body,
1137
+ contentType,
1138
+ soapAction,
1139
+ useLegacyTlsSecurityLevel0,
1140
+ timeout
1141
+ });
1142
+ } catch (error) {
1143
+ if (!(error instanceof ArcaTransportError)) {
1144
+ throw error;
1145
+ }
1146
+ if (attempt >= totalAttempts) {
1147
+ logger?.error("ARCA transport request failed", {
1148
+ service,
1149
+ operation,
1150
+ url,
1151
+ attempt,
1152
+ attempts: totalAttempts,
1153
+ error
1154
+ });
1155
+ throw error;
1156
+ }
1157
+ const nextAttempt = attempt + 1;
1158
+ logger?.warn(
1159
+ `Retrying ARCA request after transport failure (attempt ${nextAttempt}/${totalAttempts})`,
1160
+ {
1161
+ service,
1162
+ operation,
1163
+ url,
1164
+ attempt: nextAttempt,
1165
+ attempts: totalAttempts,
1166
+ error
1167
+ }
1168
+ );
1169
+ await delay(retryDelay);
1170
+ }
1171
+ }
1172
+ throw new ArcaTransportError("ARCA HTTP request exhausted retries");
1173
+ }
1174
+ async function postXmlOnce({
1175
+ url,
1176
+ body,
1177
+ contentType,
1178
+ soapAction,
1179
+ useLegacyTlsSecurityLevel0,
1180
+ timeout
898
1181
  }) {
899
1182
  const endpoint = new URL(url);
900
1183
  const requestBody = Buffer.from(body, "utf8");
@@ -983,9 +1266,9 @@ async function postXml({
983
1266
  });
984
1267
  }
985
1268
  );
986
- request.setTimeout(REQUEST_TIMEOUT_MS, () => {
1269
+ request.setTimeout(timeout, () => {
987
1270
  request.destroy(
988
- new Error(`ARCA HTTP request timed out after ${REQUEST_TIMEOUT_MS}ms`)
1271
+ new Error(`ARCA HTTP request timed out after ${timeout}ms`)
989
1272
  );
990
1273
  });
991
1274
  request.on("error", (error) => {
@@ -1006,6 +1289,11 @@ function isXmlLikeResponse(body, contentType) {
1006
1289
  }
1007
1290
  return body.trimStart().startsWith("<");
1008
1291
  }
1292
+ function delay(ms) {
1293
+ return new Promise((resolve) => {
1294
+ setTimeout(resolve, ms);
1295
+ });
1296
+ }
1009
1297
 
1010
1298
  // src/internal/xml.ts
1011
1299
  import { XMLBuilder, XMLParser } from "fast-xml-parser";
@@ -1093,9 +1381,9 @@ function createSoapFaultError(fault) {
1093
1381
  detail: fault
1094
1382
  });
1095
1383
  }
1096
- function getNestedString(value, path2) {
1384
+ function getNestedString(value, path) {
1097
1385
  let current = value;
1098
- for (const key of path2) {
1386
+ for (const key of path) {
1099
1387
  if (!current || typeof current !== "object") {
1100
1388
  return null;
1101
1389
  }
@@ -1109,6 +1397,7 @@ function createSoapTransport(options) {
1109
1397
  return {
1110
1398
  async execute(request) {
1111
1399
  const serviceConfig = getArcaServiceConfig(request.service);
1400
+ const url = serviceConfig.endpoint[options.config.environment];
1112
1401
  const soapActionOperation = request.operation;
1113
1402
  const bodyElementName = request.bodyElementName ?? request.operation;
1114
1403
  const soapAction = serviceConfig.usesEmptySoapAction ? "" : `${serviceConfig.soapActionBase}${soapActionOperation}`;
@@ -1122,34 +1411,61 @@ function createSoapTransport(options) {
1122
1411
  namespaceMode: request.bodyElementNamespaceMode
1123
1412
  }
1124
1413
  );
1125
- const responseXml = await postXml({
1126
- url: serviceConfig.endpoint[options.config.environment],
1127
- body: xml,
1128
- contentType,
1129
- soapAction: serviceConfig.soapVersion === "1.1" ? soapAction : void 0,
1130
- useLegacyTlsSecurityLevel0: options.config.environment === "production" && serviceConfig.useLegacyTlsSecurityLevel0 === true
1131
- });
1132
- const soapBody = parseSoapBody(responseXml);
1133
- const [, result] = getSingleBodyEntry(soapBody);
1134
- return {
1414
+ const startedAt = Date.now();
1415
+ options.logger?.debug("Sending ARCA SOAP request", {
1135
1416
  service: request.service,
1136
1417
  operation: request.operation,
1137
- raw: responseXml,
1138
- result
1139
- };
1418
+ url
1419
+ });
1420
+ try {
1421
+ const responseXml = await postXml({
1422
+ url,
1423
+ body: xml,
1424
+ contentType,
1425
+ soapAction: serviceConfig.soapVersion === "1.1" ? soapAction : void 0,
1426
+ useLegacyTlsSecurityLevel0: options.config.environment === "production" && serviceConfig.useLegacyTlsSecurityLevel0 === true,
1427
+ timeout: options.config.timeout,
1428
+ retries: options.config.retries,
1429
+ retryDelay: options.config.retryDelay,
1430
+ logger: options.logger,
1431
+ service: request.service,
1432
+ operation: request.operation
1433
+ });
1434
+ options.logger?.debug("Received ARCA SOAP response", {
1435
+ service: request.service,
1436
+ operation: request.operation,
1437
+ durationMs: Date.now() - startedAt
1438
+ });
1439
+ const soapBody = parseSoapBody(responseXml);
1440
+ const [, result] = getSingleBodyEntry(soapBody);
1441
+ return {
1442
+ service: request.service,
1443
+ operation: request.operation,
1444
+ raw: responseXml,
1445
+ result
1446
+ };
1447
+ } catch (error) {
1448
+ if (error instanceof ArcaSoapFaultError) {
1449
+ options.logger?.error("ARCA SOAP fault response", {
1450
+ service: request.service,
1451
+ operation: request.operation,
1452
+ url,
1453
+ faultCode: error.faultCode,
1454
+ error
1455
+ });
1456
+ }
1457
+ throw error;
1458
+ }
1140
1459
  }
1141
1460
  };
1142
1461
  }
1143
1462
 
1144
1463
  // src/wsaa/index.ts
1145
1464
  import { createHash } from "crypto";
1146
- import { mkdir, readFile, writeFile } from "fs/promises";
1147
- import path from "path";
1148
1465
  import forge from "node-forge";
1149
1466
  function createWsaaAuthModule(options) {
1150
1467
  const cache = /* @__PURE__ */ new Map();
1151
1468
  const inFlight = /* @__PURE__ */ new Map();
1152
- const persistedCache = createPersistedCredentialStore(options.config.wsaa?.cache);
1153
1469
  return {
1154
1470
  async login(service, authOptions = {}) {
1155
1471
  const cacheKey = buildWsaaCacheKey(options.config, service);
@@ -1159,18 +1475,27 @@ function createWsaaAuthModule(options) {
1159
1475
  }
1160
1476
  const loginPromise = (async () => {
1161
1477
  if (!authOptions.forceRefresh) {
1162
- const cached = await getCachedCredentials(
1163
- cache,
1164
- cacheKey,
1165
- persistedCache
1166
- );
1478
+ const cached = getCachedCredentials(cache, cacheKey);
1167
1479
  if (cached) {
1480
+ options.logger?.debug("Attempting WSAA login", {
1481
+ service,
1482
+ source: "cached"
1483
+ });
1168
1484
  return cached;
1169
1485
  }
1170
1486
  }
1171
- const credentials = await requestCredentials(options.config, service);
1487
+ options.logger?.debug("Attempting WSAA login", {
1488
+ service,
1489
+ source: "fresh"
1490
+ });
1491
+ const credentials = await requestCredentials(options.config, service, {
1492
+ logger: options.logger
1493
+ });
1494
+ options.logger?.info("WSAA login succeeded", {
1495
+ service,
1496
+ expiresAt: credentials.expiresAt
1497
+ });
1172
1498
  cache.set(cacheKey, credentials);
1173
- await persistedCache.write(cacheKey, credentials);
1174
1499
  return credentials;
1175
1500
  })();
1176
1501
  inFlight.set(cacheKey, loginPromise);
@@ -1178,15 +1503,27 @@ function createWsaaAuthModule(options) {
1178
1503
  return await loginPromise;
1179
1504
  } catch (error) {
1180
1505
  if (!authOptions.forceRefresh && error instanceof ArcaSoapFaultError && error.faultCode === "ns1:coe.alreadyAuthenticated") {
1181
- const cached = await getCachedCredentials(
1182
- cache,
1183
- cacheKey,
1184
- persistedCache
1185
- );
1506
+ const cached = getCachedCredentials(cache, cacheKey);
1186
1507
  if (cached) {
1508
+ options.logger?.warn(
1509
+ "Recovered WSAA coe.alreadyAuthenticated fault",
1510
+ {
1511
+ service,
1512
+ faultCode: error.faultCode
1513
+ }
1514
+ );
1187
1515
  return cached;
1188
1516
  }
1189
1517
  }
1518
+ if (error instanceof ArcaSoapFaultError) {
1519
+ options.logger?.error("WSAA SOAP fault response", {
1520
+ service,
1521
+ operation: "loginCms",
1522
+ url: ARCA_WSAA_CONFIG.endpoint[options.config.environment],
1523
+ faultCode: error.faultCode,
1524
+ error
1525
+ });
1526
+ }
1190
1527
  throw error;
1191
1528
  } finally {
1192
1529
  inFlight.delete(cacheKey);
@@ -1194,7 +1531,7 @@ function createWsaaAuthModule(options) {
1194
1531
  }
1195
1532
  };
1196
1533
  }
1197
- async function requestCredentials(config, service) {
1534
+ async function requestCredentials(config, service, options) {
1198
1535
  const loginTicketRequestXml = buildLoginTicketRequest(service);
1199
1536
  const signedCms = signLoginTicketRequest(loginTicketRequestXml, {
1200
1537
  certificatePem: config.certificatePem,
@@ -1210,7 +1547,13 @@ async function requestCredentials(config, service) {
1210
1547
  url: ARCA_WSAA_CONFIG.endpoint[config.environment],
1211
1548
  body: requestXml,
1212
1549
  contentType: 'text/xml; charset="utf-8"',
1213
- soapAction: ARCA_WSAA_CONFIG.soapActionBase
1550
+ soapAction: ARCA_WSAA_CONFIG.soapActionBase,
1551
+ timeout: config.timeout,
1552
+ retries: config.retries,
1553
+ retryDelay: config.retryDelay,
1554
+ logger: options?.logger,
1555
+ service: "wsaa",
1556
+ operation: "loginCms"
1214
1557
  });
1215
1558
  const soapBody = parseSoapBody(responseXml);
1216
1559
  const [, response] = getSingleBodyEntry(soapBody);
@@ -1233,64 +1576,13 @@ function getCertificateFingerprint(config) {
1233
1576
  function isCredentialValid(credentials) {
1234
1577
  return new Date(credentials.expiresAt).getTime() - Date.now() > 6e4;
1235
1578
  }
1236
- async function getCachedCredentials(cache, cacheKey, persistedCache) {
1579
+ function getCachedCredentials(cache, cacheKey) {
1237
1580
  const localCached = cache.get(cacheKey);
1238
1581
  if (localCached && isCredentialValid(localCached)) {
1239
1582
  return localCached;
1240
1583
  }
1241
- const persisted = await persistedCache.read(cacheKey);
1242
- if (persisted) {
1243
- cache.set(cacheKey, persisted);
1244
- return persisted;
1245
- }
1246
1584
  return null;
1247
1585
  }
1248
- function createPersistedCredentialStore(cacheConfig) {
1249
- if (!cacheConfig || cacheConfig.mode !== "disk") {
1250
- return {
1251
- async read() {
1252
- return null;
1253
- },
1254
- async write() {
1255
- }
1256
- };
1257
- }
1258
- const cacheDir = path.resolve(cacheConfig.directory);
1259
- return {
1260
- async read(cacheKey) {
1261
- try {
1262
- const serialized = await readFile(
1263
- getSharedCacheFilePath(cacheDir, cacheKey),
1264
- "utf8"
1265
- );
1266
- const parsed = JSON.parse(serialized);
1267
- if (typeof parsed.token !== "string" || typeof parsed.sign !== "string" || typeof parsed.expiresAt !== "string") {
1268
- return null;
1269
- }
1270
- const credentials = {
1271
- token: parsed.token,
1272
- sign: parsed.sign,
1273
- expiresAt: parsed.expiresAt
1274
- };
1275
- return isCredentialValid(credentials) ? credentials : null;
1276
- } catch {
1277
- return null;
1278
- }
1279
- },
1280
- async write(cacheKey, credentials) {
1281
- await mkdir(cacheDir, { recursive: true });
1282
- await writeFile(
1283
- getSharedCacheFilePath(cacheDir, cacheKey),
1284
- JSON.stringify(credentials),
1285
- "utf8"
1286
- );
1287
- }
1288
- };
1289
- }
1290
- function getSharedCacheFilePath(cacheDir, cacheKey) {
1291
- const fileName = `${createHash("sha256").update(cacheKey).digest("hex")}.json`;
1292
- return path.join(cacheDir, fileName);
1293
- }
1294
1586
  function buildLoginTicketRequest(service) {
1295
1587
  const uniqueId = Math.floor(Date.now() / 1e3);
1296
1588
  const generationTime = new Date(Date.now() - 5 * 6e4).toISOString().replace(".000Z", "Z");
@@ -1359,8 +1651,9 @@ function parseLoginTicketResponse(xml) {
1359
1651
  function createArcaClient(config) {
1360
1652
  assertArcaClientConfig(config);
1361
1653
  const normalizedConfig = normalizeArcaClientConfig(config);
1362
- const auth = createWsaaAuthModule({ config: normalizedConfig });
1363
- const soap = createSoapTransport({ config: normalizedConfig });
1654
+ const logger = createArcaLogger(normalizedConfig.logger);
1655
+ const auth = createWsaaAuthModule({ config: normalizedConfig, logger });
1656
+ const soap = createSoapTransport({ config: normalizedConfig, logger });
1364
1657
  return {
1365
1658
  config: normalizedConfig,
1366
1659
  wsfe: createWsfeService({ config: normalizedConfig, auth, soap }),
@@ -1373,6 +1666,7 @@ export {
1373
1666
  ARCA_ENV_VARIABLES,
1374
1667
  ArcaConfigurationError,
1375
1668
  ArcaError,
1669
+ ArcaInputError,
1376
1670
  ArcaServiceError,
1377
1671
  ArcaSoapFaultError,
1378
1672
  ArcaTransportError,