facturas 0.8.0 → 0.9.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.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createPadronService
3
- } from "./chunk-NUV5RZPZ.mjs";
3
+ } from "./chunk-7LEKQ4P7.mjs";
4
4
  import {
5
5
  buildFacturaB,
6
6
  buildFacturaC,
@@ -10,7 +10,7 @@ import {
10
10
  normalizeWsfeDateInput,
11
11
  normalizeWsfeVoucherInput,
12
12
  serializeArcaExchangeRate
13
- } from "./chunk-TOVSOJ3G.mjs";
13
+ } from "./chunk-RCM4NAAG.mjs";
14
14
  import {
15
15
  ARCA_CURRENCY_IDS,
16
16
  ARCA_DOCUMENT_TYPES,
@@ -22,8 +22,8 @@ import {
22
22
  } from "./chunk-76WU5BVI.mjs";
23
23
  import {
24
24
  createWsmtxcaService
25
- } from "./chunk-ZX4OOCML.mjs";
26
- import "./chunk-PPYGVNFA.mjs";
25
+ } from "./chunk-XNBD5NVL.mjs";
26
+ import "./chunk-4QVFXT35.mjs";
27
27
  import {
28
28
  ArcaAuthenticationError,
29
29
  ArcaConfigurationError,
@@ -37,7 +37,7 @@ import {
37
37
  createSafeErrorDiagnostic,
38
38
  isArcaAuthenticationError,
39
39
  toArcaSafeErrorMetadata
40
- } from "./chunk-HUT3PFKF.mjs";
40
+ } from "./chunk-ZOSNASXB.mjs";
41
41
 
42
42
  // src/config.ts
43
43
  var ARCA_ENVIRONMENTS = ["production", "test"];
@@ -88,17 +88,7 @@ function assertArcaClientConfig(config) {
88
88
  "Encrypted private keys are not supported. Provide an unencrypted PKCS#8 or RSA private key PEM."
89
89
  );
90
90
  }
91
- if (!/^\d{11}$/.test(normalized.taxId)) {
92
- invalidFields.push("taxId");
93
- }
94
- if (!normalized.certificatePem.startsWith("-----BEGIN CERTIFICATE-----")) {
95
- invalidFields.push("certificatePem");
96
- }
97
- if (!PRIVATE_KEY_PEM_PREFIXES.some(
98
- (prefix) => normalized.privateKeyPem.startsWith(prefix)
99
- )) {
100
- invalidFields.push("privateKeyPem");
101
- }
91
+ invalidFields.push(...invalidCredentialFields(normalized));
102
92
  if (!ARCA_ENVIRONMENTS.includes(normalized.environment)) {
103
93
  invalidFields.push("environment");
104
94
  }
@@ -210,10 +200,11 @@ function normalizeArcaClientConfig(config) {
210
200
  const normalizedEnvironment = normalizeEnvironmentValue(String(config.environment)) ?? config.environment;
211
201
  const normalizedLoggerLevel = normalizeLogLevelValue(config.logger?.level);
212
202
  return {
213
- taxId: config.taxId.trim(),
214
- certificatePem: config.certificatePem.trim(),
215
- privateKeyPem: config.privateKeyPem.trim(),
216
- environment: normalizedEnvironment,
203
+ taxId: config.taxId?.trim() ?? "",
204
+ certificatePem: config.certificatePem?.trim() ?? "",
205
+ privateKeyPem: config.privateKeyPem?.trim() ?? "",
206
+ environment: normalizedEnvironment ?? "test",
207
+ ...config.store === void 0 ? {} : { store: config.store },
217
208
  timeout: config.timeout ?? DEFAULT_ARCA_TIMEOUT_MS,
218
209
  retries: config.retries ?? DEFAULT_ARCA_RETRIES,
219
210
  retryDelay: config.retryDelay ?? DEFAULT_ARCA_RETRY_DELAY_MS,
@@ -249,6 +240,40 @@ function normalizeLogLevelValue(value) {
249
240
  }
250
241
  return value;
251
242
  }
243
+ function discoverArcaClientConfig(config) {
244
+ const environment = config.environment ?? readEnv(process.env, ARCA_ENV_VARIABLES.environment);
245
+ if (environment === void 0) {
246
+ throw new ArcaConfigurationError(
247
+ `environment is required: pass environment or set ${ARCA_ENV_VARIABLES.environment} to "test" or "production".`
248
+ );
249
+ }
250
+ return {
251
+ ...config,
252
+ taxId: config.taxId ?? readEnv(process.env, ARCA_ENV_VARIABLES.taxId) ?? "",
253
+ certificatePem: config.certificatePem ?? readEnv(process.env, ARCA_ENV_VARIABLES.certificatePem) ?? "",
254
+ privateKeyPem: config.privateKeyPem ?? readEnv(process.env, ARCA_ENV_VARIABLES.privateKeyPem) ?? "",
255
+ environment
256
+ };
257
+ }
258
+ function invalidCredentialFields(normalized) {
259
+ const invalidFields = [];
260
+ if (!/^\d{11}$/.test(normalized.taxId)) {
261
+ invalidFields.push(normalized.taxId ? "taxId" : "taxId (ARCA_TAX_ID)");
262
+ }
263
+ if (!normalized.certificatePem.startsWith("-----BEGIN CERTIFICATE-----")) {
264
+ invalidFields.push(
265
+ normalized.certificatePem ? "certificatePem" : "certificatePem (ARCA_CERTIFICATE_PEM)"
266
+ );
267
+ }
268
+ if (!PRIVATE_KEY_PEM_PREFIXES.some(
269
+ (prefix) => normalized.privateKeyPem.startsWith(prefix)
270
+ )) {
271
+ invalidFields.push(
272
+ normalized.privateKeyPem ? "privateKeyPem" : "privateKeyPem (ARCA_PRIVATE_KEY_PEM)"
273
+ );
274
+ }
275
+ return invalidFields;
276
+ }
252
277
 
253
278
  // src/internal/logger.ts
254
279
  var ARCA_LOG_LEVELS = ["debug", "info", "warn", "error"];
@@ -301,6 +326,36 @@ function defaultArcaLog(level, message, ...args) {
301
326
  method(message, ...args);
302
327
  }
303
328
 
329
+ // src/store/types.ts
330
+ import { createHash } from "crypto";
331
+ function attemptKey(environment, taxId, key) {
332
+ return `arca:v1:attempt:${environment}:${taxId}:${key}`;
333
+ }
334
+ function canonicalHash(input) {
335
+ return createHash("sha256").update(JSON.stringify(canonical(input))).digest("hex");
336
+ }
337
+ function canonical(value) {
338
+ if (Array.isArray(value)) {
339
+ return value.map(canonical);
340
+ }
341
+ if (value instanceof Date) {
342
+ return value.toJSON();
343
+ }
344
+ if (value !== null && typeof value === "object") {
345
+ return Object.fromEntries(
346
+ Object.entries(value).filter(([, item]) => item !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, item]) => [key, canonical(item)])
347
+ );
348
+ }
349
+ return value;
350
+ }
351
+ async function storeCall(fn) {
352
+ try {
353
+ return await fn();
354
+ } catch (cause) {
355
+ throw new ArcaConfigurationError("ARCA store operation failed.", { cause });
356
+ }
357
+ }
358
+
304
359
  // src/services/wsfe-derive.ts
305
360
  var INVOICE_TYPES = {
306
361
  A: ARCA_VOUCHER_TYPES.FACTURA_A,
@@ -493,14 +548,11 @@ function assertIssueKeys(value, keys, prefix) {
493
548
  for (const key of Object.keys(value)) {
494
549
  if (!keys.includes(key)) {
495
550
  const field = prefix === "input" ? key : `${prefix}.${key}`;
496
- throw new ArcaInputError(
497
- `${field} is not supported by vouchers.issue().`,
498
- {
499
- code: "ARCA_INPUT_RESERVED_FIELD",
500
- field,
501
- expected: "a supported facade field; use the exact API for other fiscal fields"
502
- }
503
- );
551
+ throw new ArcaInputError(`${field} is not supported by issue().`, {
552
+ code: "ARCA_INPUT_RESERVED_FIELD",
553
+ field,
554
+ expected: "a supported facade field; use the exact API for other fiscal fields"
555
+ });
504
556
  }
505
557
  }
506
558
  }
@@ -512,6 +564,129 @@ function invalid(field, expected) {
512
564
  });
513
565
  }
514
566
 
567
+ // src/services/wsfe-cancel.ts
568
+ var NOTES = {
569
+ 1: { voucherType: 3, voucherClass: "A" },
570
+ 6: { voucherType: 8, voucherClass: "B" },
571
+ 11: { voucherType: 13, voucherClass: "C" }
572
+ };
573
+ function invalid2(reason) {
574
+ throw new ArcaInputError(
575
+ `Cannot cancel: ${reason}. Use wsfe.issue() for exact control.`,
576
+ { code: "ARCA_INPUT_INVALID_VALUE" }
577
+ );
578
+ }
579
+ function required(value, field) {
580
+ if (value === void 0 || value === null) {
581
+ invalid2(`original is missing ${field}`);
582
+ }
583
+ return value;
584
+ }
585
+ function deriveWsfeCancellation(original, date, now = /* @__PURE__ */ new Date()) {
586
+ const note = NOTES[original.voucherType ?? 0];
587
+ if (!note) {
588
+ invalid2("original must be an invoice of type 1, 6 or 11");
589
+ }
590
+ if (!(["A", "O"].includes(original.result ?? "") && original.cae?.trim() && original.caeExpiry?.trim())) {
591
+ invalid2("original is not authorized");
592
+ }
593
+ rejectExtensions(original);
594
+ if (original.currencyId !== "PES" && original.currencyId !== "DOL") {
595
+ invalid2("unsupported currency; only ARS and USD are supported");
596
+ }
597
+ const voucherDate = normalizeWsfeDateInput(
598
+ date ?? buenosAiresDate(now),
599
+ "date"
600
+ );
601
+ const originalDate = normalizeWsfeDateInput(
602
+ required(original.voucherDate, "voucherDate"),
603
+ "original.voucherDate"
604
+ );
605
+ if (originalDate > voucherDate && originalDate.slice(0, 6) !== voucherDate.slice(0, 6)) {
606
+ invalid2(
607
+ "original date is later than the note and outside its month (10210)"
608
+ );
609
+ }
610
+ const data = {
611
+ salesPoint: required(original.salesPoint, "salesPoint"),
612
+ voucherType: note.voucherType,
613
+ concept: required(original.concept, "concept"),
614
+ documentType: required(original.documentType, "documentType"),
615
+ documentNumber: Number(required(original.documentNumber, "documentNumber")),
616
+ receiverVatConditionId: required(
617
+ original.receiverVatConditionId,
618
+ "receiverVatConditionId"
619
+ ),
620
+ currencyId: original.currencyId,
621
+ exchangeRate: required(original.exchangeRate, "exchangeRate"),
622
+ voucherDate,
623
+ totalAmount: required(original.totalAmount, "totalAmount"),
624
+ netAmount: required(original.netAmount, "netAmount"),
625
+ vatAmount: required(original.vatAmount, "vatAmount"),
626
+ exemptAmount: required(original.exemptAmount, "exemptAmount"),
627
+ nonTaxableAmount: required(original.nonTaxableAmount, "nonTaxableAmount"),
628
+ taxAmount: required(original.taxAmount, "taxAmount"),
629
+ ...original.vatRates === void 0 ? {} : { vatRates: original.vatRates.map((rate) => ({ ...rate })) },
630
+ associatedVouchers: [
631
+ {
632
+ type: required(original.voucherType, "voucherType"),
633
+ salesPoint: required(original.salesPoint, "salesPoint"),
634
+ number: original.voucherNumber,
635
+ voucherDate: originalDate
636
+ }
637
+ ]
638
+ };
639
+ copyServiceDates(original, data);
640
+ normalizeWsfeVoucherInput(data);
641
+ const total = Number(
642
+ normalizeArcaAmountToMinorUnits(data.totalAmount, "totalAmount")
643
+ );
644
+ return {
645
+ data,
646
+ voucherClass: note.voucherClass,
647
+ amounts: { computedTotal: total, sentTotal: total, vatAdjustment: 0 }
648
+ };
649
+ }
650
+ function rejectExtensions(original) {
651
+ for (const [field, rawField] of [
652
+ ["taxes", "Tributos"],
653
+ ["optionalFields", "Opcionales"],
654
+ ["buyers", "Compradores"],
655
+ ["activities", "Actividades"],
656
+ ["associatedPeriod", "PeriodoAsoc"]
657
+ ]) {
658
+ const value = original[field];
659
+ const raw = original.raw[rawField];
660
+ if (present(value) || present(raw)) {
661
+ invalid2(`unsupported original ${field}`);
662
+ }
663
+ }
664
+ if ((original.taxAmount ?? 0) !== 0) {
665
+ invalid2("unsupported original taxes");
666
+ }
667
+ }
668
+ function present(value) {
669
+ return value !== void 0 && value !== null && value !== "" && (!Array.isArray(value) || value.length > 0);
670
+ }
671
+ function copyServiceDates(original, data) {
672
+ if (data.concept !== 2 && data.concept !== 3) {
673
+ return;
674
+ }
675
+ data.serviceStartDate = required(
676
+ original.serviceStartDate,
677
+ "serviceStartDate"
678
+ );
679
+ data.serviceEndDate = required(
680
+ original.serviceEndDate,
681
+ "serviceEndDate"
682
+ );
683
+ const due = normalizeWsfeDateInput(
684
+ required(original.paymentDueDate, "paymentDueDate"),
685
+ "original.paymentDueDate"
686
+ );
687
+ data.paymentDueDate = due < data.voucherDate ? data.voucherDate : due;
688
+ }
689
+
515
690
  // src/services/wsfe-identity.ts
516
691
  function matchWsfeVoucherIdentity(sent, number, found) {
517
692
  let missing;
@@ -581,7 +756,7 @@ function matchWsfeVoucherIdentity(sent, number, found) {
581
756
  (value) => normalizeArcaAmountToMinorUnits(value, field)
582
757
  ]);
583
758
  }
584
- if (sent.concept === 2) {
759
+ if (sent.concept === 2 || sent.concept === 3) {
585
760
  for (const field of [
586
761
  "serviceStartDate",
587
762
  "serviceEndDate",
@@ -608,6 +783,17 @@ function matchWsfeVoucherIdentity(sent, number, found) {
608
783
  }
609
784
  missing ??= rates.reason;
610
785
  }
786
+ if (sent.associatedVouchers?.length) {
787
+ if (!found.associatedVouchers) {
788
+ missing ??= "associatedVouchers";
789
+ } else if (associationIdentity(sent.associatedVouchers) !== associationIdentity(found.associatedVouchers)) {
790
+ return {
791
+ matches: false,
792
+ evidence: "conflict",
793
+ reason: "associatedVouchers differ from the sent input"
794
+ };
795
+ }
796
+ }
611
797
  missing ??= incompleteAuthorization(sent, found);
612
798
  return missing ? {
613
799
  matches: false,
@@ -617,12 +803,11 @@ function matchWsfeVoucherIdentity(sent, number, found) {
617
803
  }
618
804
  function incompleteAuthorization(sent, found) {
619
805
  let missing;
620
- if (sent.concept !== 1 && sent.concept !== 2) {
806
+ if (sent.concept !== 1 && sent.concept !== 2 && sent.concept !== 3) {
621
807
  missing ??= "unsupported concept";
622
808
  }
623
809
  for (const field of [
624
810
  "taxes",
625
- "associatedVouchers",
626
811
  "associatedPeriod",
627
812
  "optionalFields",
628
813
  "buyers",
@@ -738,55 +923,176 @@ function toVoucherSummary(found) {
738
923
  }
739
924
  return summary;
740
925
  }
926
+ function associationIdentity(vouchers) {
927
+ return JSON.stringify(
928
+ vouchers.map(({ type, salesPoint, number }) => `${type}:${salesPoint}:${number}`).sort()
929
+ );
930
+ }
741
931
 
742
932
  // src/services/vouchers.ts
743
- var SERIALIZATION_NOTICE = "Serialize calls per (representedTaxId, salesPoint, voucherType). The SDK does not coordinate writers; uncoordinated calls collide on 10016. Servers and queues must persist attempts and use wsfe.authorizeVoucherOutcome().";
744
- function createVouchersService(wsfe) {
933
+ function createVouchersService(wsfe, context) {
745
934
  return {
935
+ cancel: async (target, options) => {
936
+ const result = await cancelInvoice(wsfe, target, options ?? {}, context);
937
+ return result;
938
+ },
746
939
  issue: async (input, options) => {
747
940
  const result = await issueInvoice(
748
941
  wsfe,
749
942
  input,
750
- options === void 0 ? {} : options
943
+ options === void 0 ? {} : options,
944
+ context
751
945
  );
752
946
  return result;
753
947
  }
754
948
  };
755
949
  }
756
- async function issueInvoice(wsfe, input, options) {
950
+ async function issueInvoice(wsfe, input, options, context) {
757
951
  validateOptions(options);
758
- const { data, voucherClass, amounts } = deriveWsfeInvoice(input);
759
- const auth = {
760
- representedTaxId: options.representedTaxId,
761
- forceRefresh: options.forceRefresh
952
+ validateKeyStore(options, context);
953
+ const prepared = deriveWsfeInvoice(input);
954
+ return await runOperation(
955
+ wsfe,
956
+ "issue",
957
+ input,
958
+ () => Promise.resolve(prepared),
959
+ options,
960
+ context,
961
+ prepared.amounts
962
+ );
963
+ }
964
+ async function runOperation(wsfe, operation, input, prepare, options, context, replayAmounts) {
965
+ const store = context?.store;
966
+ if (options.idempotencyKey === void 0 || !store || !context) {
967
+ return runAuthorization(wsfe, await prepare(), options);
968
+ }
969
+ const key = attemptKey(
970
+ context.environment,
971
+ context.taxId,
972
+ options.idempotencyKey
973
+ );
974
+ const representedTaxId = options.representedTaxId === void 0 ? void 0 : String(options.representedTaxId);
975
+ const inputHash = canonicalHash({ input, representedTaxId });
976
+ const existing = await storeCall(() => store.get(key));
977
+ if (existing !== null) {
978
+ return replay(existing);
979
+ }
980
+ const prepared = await prepare();
981
+ const number = await nextNumber(wsfe, prepared.data, options);
982
+ const record = {
983
+ v: 1,
984
+ operation,
985
+ representedTaxId,
986
+ inputHash,
987
+ number,
988
+ salesPoint: prepared.data.salesPoint,
989
+ voucherType: prepared.data.voucherType,
990
+ sent: prepared.data,
991
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
762
992
  };
763
- const includeRaw = options.include?.raw === true;
764
- const includeExact = options.include?.exactInput === true;
993
+ if (await storeCall(() => store.add(key, JSON.stringify(record)))) {
994
+ return runAuthorization(wsfe, prepared, options, number);
995
+ }
996
+ const winner = await storeCall(() => store.get(key));
997
+ if (winner === null) {
998
+ throw new ArcaConfigurationError(
999
+ "ARCA reservation disappeared after atomic creation lost."
1000
+ );
1001
+ }
1002
+ return replay(winner);
1003
+ function replay(json) {
1004
+ const stored = readRecord(json);
1005
+ if (stored.operation !== operation || stored.inputHash !== inputHash || stored.representedTaxId !== representedTaxId) {
1006
+ throw new ArcaInputError(
1007
+ "The idempotency key was already used with different input or operation.",
1008
+ {
1009
+ code: "ARCA_INPUT_IDEMPOTENCY_MISMATCH",
1010
+ field: "options.idempotencyKey"
1011
+ }
1012
+ );
1013
+ }
1014
+ return runAuthorization(
1015
+ wsfe,
1016
+ {
1017
+ ...preparedFromRecord(stored),
1018
+ ...replayAmounts ? { amounts: replayAmounts } : {}
1019
+ },
1020
+ options,
1021
+ stored.number,
1022
+ true
1023
+ );
1024
+ }
1025
+ }
1026
+ function validateKeyStore(options, context) {
1027
+ if (options.idempotencyKey === void 0) {
1028
+ return;
1029
+ }
1030
+ if (typeof options.idempotencyKey !== "string" || options.idempotencyKey.length < 1 || options.idempotencyKey.length > 255) {
1031
+ throw new ArcaInputError(
1032
+ "idempotencyKey must contain 1 to 255 characters.",
1033
+ { code: "ARCA_INPUT_INVALID_VALUE", field: "options.idempotencyKey" }
1034
+ );
1035
+ }
1036
+ if (!context?.store) {
1037
+ throw new ArcaConfigurationError(
1038
+ 'idempotencyKey requires a store. Add import { createPostgresStore } from "facturas"; and store: createPostgresStore({ query }) to createArcaClient().'
1039
+ );
1040
+ }
1041
+ }
1042
+ function readRecord(json) {
1043
+ try {
1044
+ const record = JSON.parse(json);
1045
+ if (!record || record.v !== 1 || !["issue", "cancel"].includes(record.operation) || typeof record.inputHash !== "string" || !record.sent || !Number.isSafeInteger(record.number) || record.number < 1 || record.number > 99999999 || record.sent.salesPoint !== record.salesPoint || record.sent.voucherType !== record.voucherType) {
1046
+ throw new Error("Invalid reservation structure");
1047
+ }
1048
+ normalizeWsfeVoucherInput(record.sent);
1049
+ return record;
1050
+ } catch (cause) {
1051
+ throw new ArcaConfigurationError(
1052
+ "Invalid ARCA reservation record; preserve it for reconciliation.",
1053
+ { cause }
1054
+ );
1055
+ }
1056
+ }
1057
+ function preparedFromRecord(record) {
1058
+ const sentTotal = Number(
1059
+ normalizeArcaAmountToMinorUnits(record.sent.totalAmount, "totalAmount")
1060
+ );
1061
+ return {
1062
+ data: record.sent,
1063
+ voucherClass: [1, 3].includes(record.voucherType) ? "A" : [6, 8].includes(record.voucherType) ? "B" : "C",
1064
+ amounts: { computedTotal: sentTotal, sentTotal, vatAdjustment: 0 }
1065
+ };
1066
+ }
1067
+ async function nextNumber(wsfe, data, options) {
765
1068
  const number = await wsfe.getNextVoucherNumber({
766
- ...auth,
1069
+ representedTaxId: options.representedTaxId,
1070
+ forceRefresh: options.forceRefresh,
767
1071
  salesPoint: data.salesPoint,
768
1072
  voucherType: data.voucherType
769
1073
  });
770
1074
  if (!Number.isSafeInteger(number) || number < 1 || number > 99999999) {
771
1075
  throw new ArcaServiceError(
772
1076
  "WSFE returned an invalid next voucher number.",
773
- {
774
- service: "wsfe",
775
- operation: "FECompUltimoAutorizado"
776
- }
1077
+ { service: "wsfe", operation: "FECompUltimoAutorizado" }
777
1078
  );
778
1079
  }
1080
+ return number;
1081
+ }
1082
+ async function runAuthorization(wsfe, { data, voucherClass, amounts }, options, reservedNumber, replay = false) {
1083
+ const auth = {
1084
+ representedTaxId: options.representedTaxId,
1085
+ forceRefresh: options.forceRefresh
1086
+ };
1087
+ const includeRaw = options.include?.raw === true;
1088
+ const includeExact = options.include?.exactInput === true;
1089
+ const number = reservedNumber ?? await nextNumber(wsfe, data, options);
779
1090
  const attempted = {
780
1091
  salesPoint: data.salesPoint,
781
1092
  voucherType: data.voucherType,
782
1093
  number
783
1094
  };
784
1095
  const exact = includeExact ? { sent: data } : {};
785
- const authorization = await wsfe.authorizeVoucherOutcome({
786
- ...auth,
787
- data,
788
- voucherNumber: number
789
- });
790
1096
  const voucher = (cae, caeExpiry) => ({
791
1097
  ...attempted,
792
1098
  voucherClass,
@@ -795,6 +1101,29 @@ async function issueInvoice(wsfe, input, options) {
795
1101
  caeExpiry,
796
1102
  amounts
797
1103
  });
1104
+ const recovery = { wsfe, auth, data, attempted, includeRaw, exact, voucher };
1105
+ if (replay) {
1106
+ const attempt2 = replayEvidence();
1107
+ let lookup;
1108
+ try {
1109
+ lookup = await wsfe.lookupVoucher({ ...auth, ...attempted });
1110
+ } catch (error) {
1111
+ return {
1112
+ kind: "indeterminate",
1113
+ attempted,
1114
+ attempt: attempt2,
1115
+ lookup: { kind: "failed", error: toArcaSafeErrorMetadata(error) }
1116
+ };
1117
+ }
1118
+ if (lookup.kind === "found") {
1119
+ return recoverInvoice({ ...recovery, attempt: attempt2, lookup });
1120
+ }
1121
+ }
1122
+ const authorization = await wsfe.issue({
1123
+ ...auth,
1124
+ data,
1125
+ voucherNumber: number
1126
+ });
798
1127
  if (authorization.kind === "authorized" && authorization.caeExpiry && authorization.voucherNumber === number) {
799
1128
  return {
800
1129
  kind: "authorized",
@@ -805,6 +1134,24 @@ async function issueInvoice(wsfe, input, options) {
805
1134
  };
806
1135
  }
807
1136
  if (authorization.kind === "rejected") {
1137
+ if (options.idempotencyKey !== void 0 && [...authorization.errors, ...authorization.observations].some(
1138
+ (issue) => issue.code === "10016"
1139
+ )) {
1140
+ const recovered = await recoverInvoice({
1141
+ ...recovery,
1142
+ attempt: projectEvidence(
1143
+ {
1144
+ ...authorization,
1145
+ kind: "indeterminate",
1146
+ reason: "contradictory_response"
1147
+ },
1148
+ includeRaw
1149
+ )
1150
+ });
1151
+ if (recovered.kind === "authorized" || recovered.kind === "conflict") {
1152
+ return recovered;
1153
+ }
1154
+ }
808
1155
  return {
809
1156
  kind: "rejected",
810
1157
  attempted,
@@ -839,11 +1186,12 @@ async function recoverInvoice({
839
1186
  attempt,
840
1187
  includeRaw,
841
1188
  exact,
842
- voucher
1189
+ voucher,
1190
+ lookup: suppliedLookup
843
1191
  }) {
844
1192
  let lookup;
845
1193
  try {
846
- lookup = await wsfe.lookupVoucher({ ...auth, ...attempted });
1194
+ lookup = suppliedLookup ?? await wsfe.lookupVoucher({ ...auth, ...attempted });
847
1195
  } catch (error) {
848
1196
  return {
849
1197
  kind: "indeterminate",
@@ -873,7 +1221,7 @@ async function recoverInvoice({
873
1221
  attempted,
874
1222
  attempt,
875
1223
  found: { ...toVoucherSummary(lookup.voucher), ...raw },
876
- reason: `${matched.reason}. ${SERIALIZATION_NOTICE}`
1224
+ reason: `${matched.reason}. Configure a store and pass idempotencyKey for retries.`
877
1225
  };
878
1226
  }
879
1227
  return {
@@ -947,7 +1295,7 @@ function validateOptions(options) {
947
1295
  assertIssueObject(options, "options");
948
1296
  assertIssueKeys(
949
1297
  options,
950
- ["representedTaxId", "forceRefresh", "include"],
1298
+ ["representedTaxId", "forceRefresh", "include", "idempotencyKey"],
951
1299
  "options"
952
1300
  );
953
1301
  if (options.representedTaxId !== void 0) {
@@ -980,6 +1328,73 @@ function validateOptions(options) {
980
1328
  }
981
1329
  }
982
1330
  }
1331
+ function replayEvidence() {
1332
+ return {
1333
+ kind: "indeterminate",
1334
+ service: "wsfe",
1335
+ operation: "FECAESolicitar",
1336
+ reason: "incomplete_response",
1337
+ results: {},
1338
+ errors: [],
1339
+ observations: []
1340
+ };
1341
+ }
1342
+ async function cancelInvoice(wsfe, target, options, context) {
1343
+ assertIssueObject(options, "options");
1344
+ const { date, ...issueOptions } = options;
1345
+ validateOptions(issueOptions);
1346
+ validateKeyStore(issueOptions, context);
1347
+ assertIssueObject(target, "target");
1348
+ for (const [field, max] of [
1349
+ ["salesPoint", 99999],
1350
+ ["voucherType", 999],
1351
+ ["number", 99999999]
1352
+ ]) {
1353
+ if (!Number.isSafeInteger(target[field]) || target[field] < 1 || target[field] > max) {
1354
+ throw new ArcaInputError(`Invalid cancel target ${field}.`, {
1355
+ code: "ARCA_INPUT_INVALID_VALUE",
1356
+ field
1357
+ });
1358
+ }
1359
+ }
1360
+ if (![1, 6, 11].includes(target.voucherType)) {
1361
+ throw new ArcaInputError(
1362
+ "cancel requires an invoice of type 1, 6 or 11; use wsfe.issue() for exact control.",
1363
+ { code: "ARCA_INPUT_INVALID_VALUE" }
1364
+ );
1365
+ }
1366
+ if (date !== void 0) {
1367
+ normalizeWsfeDateInput(date, "date");
1368
+ }
1369
+ return await runOperation(
1370
+ wsfe,
1371
+ "cancel",
1372
+ { target, date },
1373
+ async () => {
1374
+ const original = await wsfe.lookupVoucher({
1375
+ representedTaxId: issueOptions.representedTaxId,
1376
+ forceRefresh: issueOptions.forceRefresh,
1377
+ salesPoint: target.salesPoint,
1378
+ voucherType: target.voucherType,
1379
+ number: target.number
1380
+ });
1381
+ if (original.kind === "not_found") {
1382
+ throw new ArcaInputError("original voucher not found", {
1383
+ code: "ARCA_INPUT_INVALID_VALUE"
1384
+ });
1385
+ }
1386
+ if (original.voucher.salesPoint !== target.salesPoint || original.voucher.voucherType !== target.voucherType || original.voucher.voucherNumber !== target.number) {
1387
+ throw new ArcaInputError(
1388
+ "Original lookup coordinates do not match the cancel target.",
1389
+ { code: "ARCA_INPUT_INVALID_VALUE" }
1390
+ );
1391
+ }
1392
+ return deriveWsfeCancellation(original.voucher, date);
1393
+ },
1394
+ issueOptions,
1395
+ context
1396
+ );
1397
+ }
983
1398
 
984
1399
  // src/internal/http.ts
985
1400
  import https from "https";
@@ -1389,7 +1804,7 @@ function createSoapTransport(options) {
1389
1804
  }
1390
1805
 
1391
1806
  // src/wsaa/index.ts
1392
- import { createHash } from "crypto";
1807
+ import { createHash as createHash2 } from "crypto";
1393
1808
  import forge from "node-forge";
1394
1809
 
1395
1810
  // src/wsaa/session-store.ts
@@ -1623,7 +2038,7 @@ function buildWsaaSessionKey(config, service) {
1623
2038
  };
1624
2039
  }
1625
2040
  function getCertificateFingerprint(config) {
1626
- return createHash("sha256").update(config.certificatePem).digest("hex");
2041
+ return createHash2("sha256").update(config.certificatePem).digest("hex");
1627
2042
  }
1628
2043
  function getCachedCredentials(cache, cacheKey) {
1629
2044
  const localCached = cache.get(cacheKey);
@@ -1814,10 +2229,40 @@ function parseLoginTicketResponse(xml) {
1814
2229
  };
1815
2230
  }
1816
2231
 
2232
+ // src/wsaa/store-adapter.ts
2233
+ function createWsaaStoreAdapter(store) {
2234
+ const key = (value) => `arca:v1:wsaa:${serializeWsaaSessionKey(value)}`;
2235
+ const remove = store.delete?.bind(store);
2236
+ const lock = store.withLock?.bind(store);
2237
+ return {
2238
+ get: (value) => storeCall(async () => {
2239
+ const json = await store.get(key(value));
2240
+ if (json === null) {
2241
+ return null;
2242
+ }
2243
+ const credentials = JSON.parse(json);
2244
+ return credentials && typeof credentials.token === "string" && typeof credentials.sign === "string" && isWsaaCredentialValid(credentials) ? credentials : null;
2245
+ }),
2246
+ set: (value, credentials) => storeCall(() => store.set(key(value), JSON.stringify(credentials))),
2247
+ ...remove ? {
2248
+ delete: (value) => storeCall(() => remove(key(value)))
2249
+ } : {},
2250
+ ...lock ? {
2251
+ withLock: (value, fn) => lock(key(value), fn)
2252
+ } : {}
2253
+ };
2254
+ }
2255
+
1817
2256
  // src/client.ts
1818
- function createArcaClient(config) {
1819
- assertArcaClientConfig(config);
1820
- const normalizedConfig = normalizeArcaClientConfig(config);
2257
+ function createArcaClient(config = {}) {
2258
+ const discovered = discoverArcaClientConfig(config);
2259
+ assertArcaClientConfig(discovered);
2260
+ const normalizedConfig = normalizeArcaClientConfig(discovered);
2261
+ if (normalizedConfig.store && !normalizedConfig.wsaaSessionStore) {
2262
+ normalizedConfig.wsaaSessionStore = createWsaaStoreAdapter(
2263
+ normalizedConfig.store
2264
+ );
2265
+ }
1821
2266
  const logger = createArcaLogger(normalizedConfig.logger);
1822
2267
  const auth = createWsaaAuthModule({ config: normalizedConfig, logger });
1823
2268
  const soap = createSoapTransport({ config: normalizedConfig, logger });
@@ -1829,14 +2274,176 @@ function createArcaClient(config) {
1829
2274
  retryDelay: normalizedConfig.retryDelay
1830
2275
  });
1831
2276
  const wsfe = createWsfeService({ config: normalizedConfig, auth, soap });
2277
+ const vouchers = createVouchersService(wsfe, normalizedConfig);
1832
2278
  return {
1833
2279
  config: publicConfig,
2280
+ issue: vouchers.issue,
2281
+ cancel: vouchers.cancel,
1834
2282
  wsfe,
1835
- vouchers: createVouchersService(wsfe),
1836
2283
  wsmtxca: createWsmtxcaService({ config: normalizedConfig, auth, soap }),
1837
2284
  padron: createPadronService({ config: normalizedConfig, auth, soap })
1838
2285
  };
1839
2286
  }
2287
+
2288
+ // src/store/file.ts
2289
+ import { createHash as createHash3, randomUUID } from "crypto";
2290
+ import { mkdir, readFile, rename, unlink, writeFile } from "fs/promises";
2291
+ import { join } from "path";
2292
+ function createFileStore(directory) {
2293
+ const path = (key) => join(directory, createHash3("sha256").update(key).digest("hex"));
2294
+ const ensure = () => mkdir(directory, { recursive: true, mode: 448 });
2295
+ return {
2296
+ get: (key) => storeCall(async () => {
2297
+ try {
2298
+ return await readFile(path(key), "utf8");
2299
+ } catch (error) {
2300
+ if (error.code === "ENOENT") {
2301
+ return null;
2302
+ }
2303
+ throw error;
2304
+ }
2305
+ }),
2306
+ set: (key, value) => storeCall(async () => {
2307
+ await ensure();
2308
+ const temporary = `${path(key)}.${randomUUID()}.tmp`;
2309
+ try {
2310
+ await writeFile(temporary, value, { mode: 384, flag: "wx" });
2311
+ await rename(temporary, path(key));
2312
+ } finally {
2313
+ await unlink(temporary).catch(() => void 0);
2314
+ }
2315
+ }),
2316
+ add: (key, value) => storeCall(async () => {
2317
+ await ensure();
2318
+ try {
2319
+ await writeFile(path(key), value, { flag: "wx", mode: 384 });
2320
+ return true;
2321
+ } catch (error) {
2322
+ if (error.code === "EEXIST") {
2323
+ return false;
2324
+ }
2325
+ throw error;
2326
+ }
2327
+ }),
2328
+ delete: (key) => storeCall(async () => {
2329
+ try {
2330
+ await unlink(path(key));
2331
+ } catch (error) {
2332
+ if (error.code !== "ENOENT") {
2333
+ throw error;
2334
+ }
2335
+ }
2336
+ })
2337
+ };
2338
+ }
2339
+
2340
+ // src/store/memory.ts
2341
+ function createMemoryStore() {
2342
+ const values = /* @__PURE__ */ new Map();
2343
+ const locks = /* @__PURE__ */ new Map();
2344
+ return {
2345
+ get: (key) => Promise.resolve(values.get(key) ?? null),
2346
+ set(key, value) {
2347
+ values.set(key, value);
2348
+ return Promise.resolve();
2349
+ },
2350
+ add(key, value) {
2351
+ if (values.has(key)) {
2352
+ return Promise.resolve(false);
2353
+ }
2354
+ values.set(key, value);
2355
+ return Promise.resolve(true);
2356
+ },
2357
+ delete(key) {
2358
+ values.delete(key);
2359
+ return Promise.resolve();
2360
+ },
2361
+ async withLock(key, fn) {
2362
+ const previous = locks.get(key) ?? Promise.resolve();
2363
+ let release = () => void 0;
2364
+ const current = new Promise((resolve) => {
2365
+ release = resolve;
2366
+ });
2367
+ const queued = previous.catch(() => void 0).then(() => current);
2368
+ locks.set(key, queued);
2369
+ await previous.catch(() => void 0);
2370
+ try {
2371
+ return await fn();
2372
+ } finally {
2373
+ release();
2374
+ if (locks.get(key) === queued) {
2375
+ locks.delete(key);
2376
+ }
2377
+ }
2378
+ }
2379
+ };
2380
+ }
2381
+
2382
+ // src/store/postgres.ts
2383
+ function createPostgresStore({
2384
+ query,
2385
+ table = "arca_store"
2386
+ }) {
2387
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) {
2388
+ throw new ArcaConfigurationError("Invalid ARCA store table identifier.");
2389
+ }
2390
+ const name = `"${table}"`;
2391
+ const run = (text, params) => storeCall(async () => {
2392
+ const result = await query(text, params);
2393
+ return Array.isArray(result) ? result : result.rows;
2394
+ });
2395
+ return {
2396
+ async get(key) {
2397
+ return (await run(`SELECT value FROM ${name} WHERE key = $1`, [key]))[0]?.value ?? null;
2398
+ },
2399
+ async set(key, value) {
2400
+ await run(
2401
+ `INSERT INTO ${name} (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now()`,
2402
+ [key, value]
2403
+ );
2404
+ },
2405
+ async add(key, value) {
2406
+ return (await run(
2407
+ `INSERT INTO ${name} (key, value) VALUES ($1, $2) ON CONFLICT DO NOTHING RETURNING key`,
2408
+ [key, value]
2409
+ )).length > 0;
2410
+ },
2411
+ async delete(key) {
2412
+ await run(`DELETE FROM ${name} WHERE key = $1`, [key]);
2413
+ }
2414
+ };
2415
+ }
2416
+
2417
+ // src/store/redis.ts
2418
+ function createRedisStore(client, options = {}) {
2419
+ const flavor = options.flavor ?? (typeof client.call === "function" ? "ioredis" : "upstash");
2420
+ if (flavor === "ioredis" && !client.call) {
2421
+ throw new ArcaConfigurationError(
2422
+ "ARCA Redis ioredis flavor requires client.call."
2423
+ );
2424
+ }
2425
+ const upstashSet = client.set;
2426
+ return {
2427
+ get: (key) => storeCall(async () => {
2428
+ const value = await client.get(key);
2429
+ if (value === null) {
2430
+ return null;
2431
+ }
2432
+ return typeof value === "string" ? value : JSON.stringify(value);
2433
+ }),
2434
+ set: (key, value) => storeCall(async () => {
2435
+ await client.set(key, value);
2436
+ }),
2437
+ add: (key, value) => storeCall(
2438
+ async () => (flavor === "ioredis" ? await client.call?.("SET", key, value, "NX") : await upstashSet.call(client, key, value, { nx: true })) === "OK"
2439
+ ),
2440
+ ...client.del ? {
2441
+ delete: (key) => storeCall(async () => {
2442
+ await client.del?.(key);
2443
+ })
2444
+ } : {}
2445
+ };
2446
+ }
1840
2447
  export {
1841
2448
  ARCA_ENVIRONMENTS,
1842
2449
  ARCA_ENV_VARIABLES,
@@ -1857,8 +2464,12 @@ export {
1857
2464
  buildFacturaC,
1858
2465
  createArcaClient,
1859
2466
  createArcaClientConfigFromEnv,
2467
+ createFileStore,
2468
+ createMemoryStore,
1860
2469
  createMemoryWsaaSessionStore,
1861
2470
  createPadronService,
2471
+ createPostgresStore,
2472
+ createRedisStore,
1862
2473
  createWsfeService,
1863
2474
  createWsmtxcaService,
1864
2475
  isArcaAuthenticationError,