facturas 0.8.0 → 0.10.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/README.md +858 -37
- package/dist/{chunk-NUV5RZPZ.mjs → chunk-7LEKQ4P7.mjs} +2 -2
- package/dist/{chunk-ZX4OOCML.mjs → chunk-ALH5IPSY.mjs} +7 -61
- package/dist/chunk-ALH5IPSY.mjs.map +1 -0
- package/dist/{chunk-TOVSOJ3G.mjs → chunk-QHHLHSBW.mjs} +31 -125
- package/dist/chunk-QHHLHSBW.mjs.map +1 -0
- package/dist/{chunk-PPYGVNFA.mjs → chunk-UDX6HLAU.mjs} +2 -18
- package/dist/{chunk-PPYGVNFA.mjs.map → chunk-UDX6HLAU.mjs.map} +1 -1
- package/dist/{chunk-HUT3PFKF.mjs → chunk-ZOSNASXB.mjs} +1 -1
- package/dist/chunk-ZOSNASXB.mjs.map +1 -0
- package/dist/errors.d.ts +2 -2
- package/dist/errors.mjs +1 -1
- package/dist/{errors-B0uouRzR.d.ts → fiscal-evidence-DBzakqBE.d.ts} +77 -77
- package/dist/{index-CVpygFb0.d.ts → index-CVGQyyYM.d.ts} +1 -1
- package/dist/index.d.ts +108 -23
- package/dist/index.mjs +863 -74
- package/dist/index.mjs.map +1 -1
- package/dist/padron.d.ts +2 -2
- package/dist/padron.mjs +2 -2
- package/dist/{types-BXq0da71.d.ts → types-BiznC1-X.d.ts} +12 -1
- package/dist/types.d.ts +1 -1
- package/dist/{wsfe-CG_erq7V.d.ts → wsfe-DDY9E1GJ.d.ts} +9 -22
- package/dist/wsfe.d.ts +4 -4
- package/dist/wsfe.mjs +3 -3
- package/dist/wsmtxca.d.ts +9 -16
- package/dist/wsmtxca.mjs +3 -3
- package/package.json +1 -1
- package/dist/chunk-HUT3PFKF.mjs.map +0 -1
- package/dist/chunk-TOVSOJ3G.mjs.map +0 -1
- package/dist/chunk-ZX4OOCML.mjs.map +0 -1
- /package/dist/{chunk-NUV5RZPZ.mjs.map → chunk-7LEKQ4P7.mjs.map} +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createPadronService
|
|
3
|
-
} from "./chunk-
|
|
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-
|
|
13
|
+
} from "./chunk-QHHLHSBW.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-
|
|
26
|
-
import "./chunk-
|
|
25
|
+
} from "./chunk-ALH5IPSY.mjs";
|
|
26
|
+
import "./chunk-UDX6HLAU.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-
|
|
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
|
-
|
|
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
|
|
214
|
-
certificatePem: config.certificatePem
|
|
215
|
-
privateKeyPem: config.privateKeyPem
|
|
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,
|
|
@@ -324,18 +379,15 @@ function deriveWsfeInvoice(input, now = /* @__PURE__ */ new Date()) {
|
|
|
324
379
|
],
|
|
325
380
|
"input"
|
|
326
381
|
);
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
"issuer",
|
|
330
|
-
"responsable_inscripto, monotributo, exento, or no_alcanzado"
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
if (!Number.isSafeInteger(input.salesPoint) || input.salesPoint < 1 || input.salesPoint > 99999) {
|
|
334
|
-
invalid("salesPoint", "an integer from 1 through 99999");
|
|
335
|
-
}
|
|
382
|
+
assertIssuerCondition(input.issuer);
|
|
383
|
+
assertSalesPoint(input.salesPoint);
|
|
336
384
|
const receiver = deriveReceiver(input.to);
|
|
337
|
-
const voucherClass =
|
|
338
|
-
const { data: amountsData, amounts } = calculateWsfeAmounts(
|
|
385
|
+
const voucherClass = resolveInvoiceClass(input.issuer, input.to.condition);
|
|
386
|
+
const { data: amountsData, amounts } = calculateWsfeAmounts({
|
|
387
|
+
voucherClass,
|
|
388
|
+
items: input.items,
|
|
389
|
+
total: input.total
|
|
390
|
+
});
|
|
339
391
|
const currency = deriveCurrency(input);
|
|
340
392
|
if (input.to.condition === "consumidor_final" && receiver.documentType === ARCA_DOCUMENT_TYPES.CONSUMIDOR_FINAL) {
|
|
341
393
|
const [whole, fraction = ""] = currency.exchangeRate.split(".");
|
|
@@ -375,6 +427,22 @@ function deriveWsfeInvoice(input, now = /* @__PURE__ */ new Date()) {
|
|
|
375
427
|
}
|
|
376
428
|
return { data, voucherClass, amounts };
|
|
377
429
|
}
|
|
430
|
+
function assertIssuerCondition(issuer) {
|
|
431
|
+
if (typeof issuer !== "string" || !Object.hasOwn(ARCA_ISSUER_CONDITION_IDS, issuer)) {
|
|
432
|
+
invalid(
|
|
433
|
+
"issuer",
|
|
434
|
+
"responsable_inscripto, monotributo, exento, or no_alcanzado"
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
function assertSalesPoint(salesPoint) {
|
|
439
|
+
if (!Number.isSafeInteger(salesPoint) || salesPoint < 1 || salesPoint > 99999) {
|
|
440
|
+
invalid("salesPoint", "an integer from 1 through 99999");
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function resolveInvoiceClass(issuer, condition) {
|
|
444
|
+
return ARCA_INVOICE_CLASS_BY_ISSUER[issuer][condition];
|
|
445
|
+
}
|
|
378
446
|
function deriveReceiver(to) {
|
|
379
447
|
assertIssueObject(to, "to");
|
|
380
448
|
assertIssueKeys(to, ["condition", "cuit", "dni"], "to");
|
|
@@ -489,18 +557,15 @@ function assertIssueObject(value, field) {
|
|
|
489
557
|
invalid(field, "an object");
|
|
490
558
|
}
|
|
491
559
|
}
|
|
492
|
-
function assertIssueKeys(value, keys, prefix) {
|
|
560
|
+
function assertIssueKeys(value, keys, prefix, method = "issue()") {
|
|
493
561
|
for (const key of Object.keys(value)) {
|
|
494
562
|
if (!keys.includes(key)) {
|
|
495
563
|
const field = prefix === "input" ? key : `${prefix}.${key}`;
|
|
496
|
-
throw new ArcaInputError(
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
expected: "a supported facade field; use the exact API for other fiscal fields"
|
|
502
|
-
}
|
|
503
|
-
);
|
|
564
|
+
throw new ArcaInputError(`${field} is not supported by ${method}.`, {
|
|
565
|
+
code: "ARCA_INPUT_RESERVED_FIELD",
|
|
566
|
+
field,
|
|
567
|
+
expected: "a supported facade field; use the exact API for other fiscal fields"
|
|
568
|
+
});
|
|
504
569
|
}
|
|
505
570
|
}
|
|
506
571
|
}
|
|
@@ -512,6 +577,286 @@ function invalid(field, expected) {
|
|
|
512
577
|
});
|
|
513
578
|
}
|
|
514
579
|
|
|
580
|
+
// src/services/wsfe-credit-note.ts
|
|
581
|
+
var NOTES = {
|
|
582
|
+
1: { voucherType: 3, voucherClass: "A" },
|
|
583
|
+
6: { voucherType: 8, voucherClass: "B" },
|
|
584
|
+
11: { voucherType: 13, voucherClass: "C" }
|
|
585
|
+
};
|
|
586
|
+
function invalid2(reason) {
|
|
587
|
+
throw new ArcaInputError(
|
|
588
|
+
`issueCreditNote cannot proceed: ${reason}. Use wsfe.issue() for exact control.`,
|
|
589
|
+
{ code: "ARCA_INPUT_INVALID_VALUE" }
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
function required(value, field) {
|
|
593
|
+
if (value === void 0 || value === null) {
|
|
594
|
+
invalid2(`original is missing ${field}`);
|
|
595
|
+
}
|
|
596
|
+
return value;
|
|
597
|
+
}
|
|
598
|
+
function deriveWsfeFullCreditNote(original, input, now = /* @__PURE__ */ new Date()) {
|
|
599
|
+
const { note, header } = prepareCreditNote(original, input, now);
|
|
600
|
+
const data = {
|
|
601
|
+
...header,
|
|
602
|
+
totalAmount: required(original.totalAmount, "totalAmount"),
|
|
603
|
+
netAmount: required(original.netAmount, "netAmount"),
|
|
604
|
+
vatAmount: required(original.vatAmount, "vatAmount"),
|
|
605
|
+
exemptAmount: required(original.exemptAmount, "exemptAmount"),
|
|
606
|
+
nonTaxableAmount: required(original.nonTaxableAmount, "nonTaxableAmount"),
|
|
607
|
+
taxAmount: required(original.taxAmount, "taxAmount"),
|
|
608
|
+
...original.vatRates === void 0 ? {} : { vatRates: original.vatRates.map((rate) => ({ ...rate })) }
|
|
609
|
+
};
|
|
610
|
+
normalizeWsfeVoucherInput(data);
|
|
611
|
+
const total = Number(
|
|
612
|
+
normalizeArcaAmountToMinorUnits(data.totalAmount, "totalAmount")
|
|
613
|
+
);
|
|
614
|
+
return {
|
|
615
|
+
data,
|
|
616
|
+
voucherClass: note.voucherClass,
|
|
617
|
+
amounts: { computedTotal: total, sentTotal: total, vatAdjustment: 0 }
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
function deriveWsfePartialCreditNote(original, input, now = /* @__PURE__ */ new Date()) {
|
|
621
|
+
if (input.items === void 0) {
|
|
622
|
+
invalid2("items is required to credit chosen lines");
|
|
623
|
+
}
|
|
624
|
+
const { note, header } = prepareCreditNote(original, input, now);
|
|
625
|
+
const { data: amountsData, amounts } = calculateWsfeAmounts({
|
|
626
|
+
voucherClass: note.voucherClass,
|
|
627
|
+
items: input.items,
|
|
628
|
+
total: input.total
|
|
629
|
+
});
|
|
630
|
+
const originalTotal = normalizeArcaAmountToMinorUnits(
|
|
631
|
+
required(original.totalAmount, "totalAmount"),
|
|
632
|
+
"totalAmount"
|
|
633
|
+
);
|
|
634
|
+
if (BigInt(amounts.sentTotal) > originalTotal) {
|
|
635
|
+
invalid2(
|
|
636
|
+
"the note total is greater than the original; the SDK does not track earlier notes against an original"
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
const data = { ...header, ...amountsData };
|
|
640
|
+
normalizeWsfeVoucherInput(data);
|
|
641
|
+
return { data, voucherClass: note.voucherClass, amounts };
|
|
642
|
+
}
|
|
643
|
+
function prepareCreditNote(original, input, now) {
|
|
644
|
+
const note = NOTES[original.voucherType ?? 0];
|
|
645
|
+
if (!note) {
|
|
646
|
+
invalid2("original must be an invoice of type 1, 6 or 11");
|
|
647
|
+
}
|
|
648
|
+
if (!(["A", "O"].includes(original.result ?? "") && original.cae?.trim() && original.caeExpiry?.trim())) {
|
|
649
|
+
invalid2("original is not authorized");
|
|
650
|
+
}
|
|
651
|
+
rejectExtensions(original);
|
|
652
|
+
if (original.currencyId !== "PES" && original.currencyId !== "DOL") {
|
|
653
|
+
invalid2("unsupported currency; only ARS and USD are supported");
|
|
654
|
+
}
|
|
655
|
+
const voucherDate = normalizeWsfeDateInput(
|
|
656
|
+
input.date ?? buenosAiresDate(now),
|
|
657
|
+
"date"
|
|
658
|
+
);
|
|
659
|
+
const originalDate = normalizeWsfeDateInput(
|
|
660
|
+
required(original.voucherDate, "voucherDate"),
|
|
661
|
+
"original.voucherDate"
|
|
662
|
+
);
|
|
663
|
+
if (originalDate > voucherDate && originalDate.slice(0, 6) !== voucherDate.slice(0, 6)) {
|
|
664
|
+
invalid2(
|
|
665
|
+
"original date is later than the note and outside its month (10210)"
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
const header = {
|
|
669
|
+
salesPoint: input.salesPoint ?? required(original.salesPoint, "salesPoint"),
|
|
670
|
+
voucherType: note.voucherType,
|
|
671
|
+
concept: required(original.concept, "concept"),
|
|
672
|
+
documentType: required(original.documentType, "documentType"),
|
|
673
|
+
documentNumber: Number(required(original.documentNumber, "documentNumber")),
|
|
674
|
+
receiverVatConditionId: required(
|
|
675
|
+
original.receiverVatConditionId,
|
|
676
|
+
"receiverVatConditionId"
|
|
677
|
+
),
|
|
678
|
+
currencyId: original.currencyId,
|
|
679
|
+
exchangeRate: required(original.exchangeRate, "exchangeRate"),
|
|
680
|
+
voucherDate,
|
|
681
|
+
associatedVouchers: [
|
|
682
|
+
{
|
|
683
|
+
type: required(original.voucherType, "voucherType"),
|
|
684
|
+
salesPoint: required(original.salesPoint, "salesPoint"),
|
|
685
|
+
number: original.voucherNumber,
|
|
686
|
+
voucherDate: originalDate
|
|
687
|
+
}
|
|
688
|
+
]
|
|
689
|
+
};
|
|
690
|
+
copyServiceDates(original, header);
|
|
691
|
+
return { note, header };
|
|
692
|
+
}
|
|
693
|
+
function rejectExtensions(original) {
|
|
694
|
+
for (const [field, rawField] of [
|
|
695
|
+
["taxes", "Tributos"],
|
|
696
|
+
["optionalFields", "Opcionales"],
|
|
697
|
+
["buyers", "Compradores"],
|
|
698
|
+
["activities", "Actividades"],
|
|
699
|
+
["associatedPeriod", "PeriodoAsoc"]
|
|
700
|
+
]) {
|
|
701
|
+
const value = original[field];
|
|
702
|
+
const raw = original.raw[rawField];
|
|
703
|
+
if (present(value) || present(raw)) {
|
|
704
|
+
invalid2(`unsupported original ${field}`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if ((original.taxAmount ?? 0) !== 0) {
|
|
708
|
+
invalid2("unsupported original taxes");
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
function present(value) {
|
|
712
|
+
return value !== void 0 && value !== null && value !== "" && (!Array.isArray(value) || value.length > 0);
|
|
713
|
+
}
|
|
714
|
+
function copyServiceDates(original, header) {
|
|
715
|
+
if (header.concept !== 2 && header.concept !== 3) {
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
header.serviceStartDate = required(
|
|
719
|
+
original.serviceStartDate,
|
|
720
|
+
"serviceStartDate"
|
|
721
|
+
);
|
|
722
|
+
header.serviceEndDate = required(
|
|
723
|
+
original.serviceEndDate,
|
|
724
|
+
"serviceEndDate"
|
|
725
|
+
);
|
|
726
|
+
const due = normalizeWsfeDateInput(
|
|
727
|
+
required(original.paymentDueDate, "paymentDueDate"),
|
|
728
|
+
"original.paymentDueDate"
|
|
729
|
+
);
|
|
730
|
+
header.paymentDueDate = due < header.voucherDate ? header.voucherDate : due;
|
|
731
|
+
}
|
|
732
|
+
var CREDIT_NOTE_KEYS = ["for", "salesPoint", "date", "items", "total", "all"];
|
|
733
|
+
var TARGET_BOUNDS = [
|
|
734
|
+
["salesPoint", 99999],
|
|
735
|
+
["voucherType", 999],
|
|
736
|
+
["number", 99999999]
|
|
737
|
+
];
|
|
738
|
+
function assertCreditNoteInput(input) {
|
|
739
|
+
assertIssueObject(input, "input");
|
|
740
|
+
if ("associatedPeriod" in input) {
|
|
741
|
+
throw new ArcaInputError(
|
|
742
|
+
"issueCreditNote does not support associatedPeriod; a note against a period is exact-layer work. Use wsfe.issue() for exact control.",
|
|
743
|
+
{
|
|
744
|
+
code: "ARCA_INPUT_RESERVED_FIELD",
|
|
745
|
+
field: "associatedPeriod",
|
|
746
|
+
expected: "an associated invoice through for"
|
|
747
|
+
}
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
assertIssueKeys(input, CREDIT_NOTE_KEYS, "input", "issueCreditNote()");
|
|
751
|
+
const target = assertCreditNoteTarget(input.for);
|
|
752
|
+
if (input.salesPoint !== void 0) {
|
|
753
|
+
assertCreditNoteBound(input.salesPoint, 99999, "salesPoint");
|
|
754
|
+
}
|
|
755
|
+
const date = input.date === void 0 ? void 0 : normalizeWsfeDateInput(input.date, "date");
|
|
756
|
+
const common = {
|
|
757
|
+
for: target,
|
|
758
|
+
...input.salesPoint === void 0 ? {} : { salesPoint: input.salesPoint },
|
|
759
|
+
...date === void 0 ? {} : { date }
|
|
760
|
+
};
|
|
761
|
+
if (input.items === void 0 === (input.all === void 0)) {
|
|
762
|
+
throw new ArcaInputError(
|
|
763
|
+
"issueCreditNote needs exactly one mode: items for the credited lines, or all: true for the whole original.",
|
|
764
|
+
{
|
|
765
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
766
|
+
field: input.items === void 0 ? "input.items" : "input.all",
|
|
767
|
+
expected: "either items or all: true, never both and never neither"
|
|
768
|
+
}
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
if (input.all !== void 0) {
|
|
772
|
+
if (input.all !== true) {
|
|
773
|
+
throw new ArcaInputError(
|
|
774
|
+
"issueCreditNote accepts only all: true; pass items to credit chosen lines.",
|
|
775
|
+
{
|
|
776
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
777
|
+
field: "input.all",
|
|
778
|
+
expected: "the literal true"
|
|
779
|
+
}
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
if (input.total !== void 0) {
|
|
783
|
+
throw new ArcaInputError(
|
|
784
|
+
"issueCreditNote takes total only with items; all: true credits the original's own total.",
|
|
785
|
+
{
|
|
786
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
787
|
+
field: "input.total",
|
|
788
|
+
expected: "no total when all is true"
|
|
789
|
+
}
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
return { ...common, all: true };
|
|
793
|
+
}
|
|
794
|
+
return {
|
|
795
|
+
...common,
|
|
796
|
+
items: copyCreditNoteItems(input.items),
|
|
797
|
+
...input.total === void 0 ? {} : { total: input.total }
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
function assertCreditNoteTarget(value) {
|
|
801
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
802
|
+
throw new ArcaInputError(
|
|
803
|
+
"issueCreditNote requires for: the coordinates of the authorized invoice the note corrects.",
|
|
804
|
+
{
|
|
805
|
+
code: "ARCA_INPUT_MISSING_FIELD",
|
|
806
|
+
field: "input.for",
|
|
807
|
+
expected: "{ salesPoint, voucherType, number }"
|
|
808
|
+
}
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
assertIssueKeys(
|
|
812
|
+
value,
|
|
813
|
+
["salesPoint", "voucherType", "number"],
|
|
814
|
+
"input.for",
|
|
815
|
+
"issueCreditNote()"
|
|
816
|
+
);
|
|
817
|
+
for (const [field, max] of TARGET_BOUNDS) {
|
|
818
|
+
assertCreditNoteBound(value[field], max, `for.${field}`);
|
|
819
|
+
}
|
|
820
|
+
if (![1, 6, 11].includes(value.voucherType)) {
|
|
821
|
+
throw new ArcaInputError(
|
|
822
|
+
"issueCreditNote requires an authorized invoice of type 1, 6 or 11 in for.voucherType. Use wsfe.issue() for exact control.",
|
|
823
|
+
{
|
|
824
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
825
|
+
field: "input.for.voucherType",
|
|
826
|
+
expected: "1, 6 or 11"
|
|
827
|
+
}
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
return {
|
|
831
|
+
salesPoint: value.salesPoint,
|
|
832
|
+
voucherType: value.voucherType,
|
|
833
|
+
number: value.number
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
function assertCreditNoteBound(value, max, path) {
|
|
837
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > max) {
|
|
838
|
+
throw new ArcaInputError(
|
|
839
|
+
`issueCreditNote requires input.${path} to be an integer from 1 through ${max}.`,
|
|
840
|
+
{ code: "ARCA_INPUT_INVALID_VALUE", field: `input.${path}` }
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
function copyCreditNoteItems(items) {
|
|
845
|
+
if (!Array.isArray(items)) {
|
|
846
|
+
throw new ArcaInputError(
|
|
847
|
+
"issueCreditNote requires items to be a non-empty array of items.",
|
|
848
|
+
{
|
|
849
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
850
|
+
field: "input.items",
|
|
851
|
+
expected: "a non-empty array of items"
|
|
852
|
+
}
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
return items.map(
|
|
856
|
+
(item) => item === null || typeof item !== "object" ? item : { ...item }
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
|
|
515
860
|
// src/services/wsfe-identity.ts
|
|
516
861
|
function matchWsfeVoucherIdentity(sent, number, found) {
|
|
517
862
|
let missing;
|
|
@@ -581,7 +926,7 @@ function matchWsfeVoucherIdentity(sent, number, found) {
|
|
|
581
926
|
(value) => normalizeArcaAmountToMinorUnits(value, field)
|
|
582
927
|
]);
|
|
583
928
|
}
|
|
584
|
-
if (sent.concept === 2) {
|
|
929
|
+
if (sent.concept === 2 || sent.concept === 3) {
|
|
585
930
|
for (const field of [
|
|
586
931
|
"serviceStartDate",
|
|
587
932
|
"serviceEndDate",
|
|
@@ -608,6 +953,17 @@ function matchWsfeVoucherIdentity(sent, number, found) {
|
|
|
608
953
|
}
|
|
609
954
|
missing ??= rates.reason;
|
|
610
955
|
}
|
|
956
|
+
if (sent.associatedVouchers?.length) {
|
|
957
|
+
if (!found.associatedVouchers) {
|
|
958
|
+
missing ??= "associatedVouchers";
|
|
959
|
+
} else if (associationIdentity(sent.associatedVouchers) !== associationIdentity(found.associatedVouchers)) {
|
|
960
|
+
return {
|
|
961
|
+
matches: false,
|
|
962
|
+
evidence: "conflict",
|
|
963
|
+
reason: "associatedVouchers differ from the sent input"
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
}
|
|
611
967
|
missing ??= incompleteAuthorization(sent, found);
|
|
612
968
|
return missing ? {
|
|
613
969
|
matches: false,
|
|
@@ -617,12 +973,11 @@ function matchWsfeVoucherIdentity(sent, number, found) {
|
|
|
617
973
|
}
|
|
618
974
|
function incompleteAuthorization(sent, found) {
|
|
619
975
|
let missing;
|
|
620
|
-
if (sent.concept !== 1 && sent.concept !== 2) {
|
|
976
|
+
if (sent.concept !== 1 && sent.concept !== 2 && sent.concept !== 3) {
|
|
621
977
|
missing ??= "unsupported concept";
|
|
622
978
|
}
|
|
623
979
|
for (const field of [
|
|
624
980
|
"taxes",
|
|
625
|
-
"associatedVouchers",
|
|
626
981
|
"associatedPeriod",
|
|
627
982
|
"optionalFields",
|
|
628
983
|
"buyers",
|
|
@@ -738,55 +1093,201 @@ function toVoucherSummary(found) {
|
|
|
738
1093
|
}
|
|
739
1094
|
return summary;
|
|
740
1095
|
}
|
|
1096
|
+
function associationIdentity(vouchers) {
|
|
1097
|
+
return JSON.stringify(
|
|
1098
|
+
vouchers.map(({ type, salesPoint, number }) => `${type}:${salesPoint}:${number}`).sort()
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
741
1101
|
|
|
742
1102
|
// src/services/vouchers.ts
|
|
743
|
-
|
|
744
|
-
function createVouchersService(wsfe) {
|
|
1103
|
+
function createVouchersService(wsfe, context) {
|
|
745
1104
|
return {
|
|
1105
|
+
issueCreditNote: async (input, options) => {
|
|
1106
|
+
const result = await issueCreditNote(
|
|
1107
|
+
wsfe,
|
|
1108
|
+
input,
|
|
1109
|
+
options === void 0 ? {} : options,
|
|
1110
|
+
context
|
|
1111
|
+
);
|
|
1112
|
+
return result;
|
|
1113
|
+
},
|
|
746
1114
|
issue: async (input, options) => {
|
|
747
1115
|
const result = await issueInvoice(
|
|
748
1116
|
wsfe,
|
|
749
1117
|
input,
|
|
750
|
-
options === void 0 ? {} : options
|
|
1118
|
+
options === void 0 ? {} : options,
|
|
1119
|
+
context
|
|
751
1120
|
);
|
|
752
1121
|
return result;
|
|
753
|
-
}
|
|
1122
|
+
},
|
|
1123
|
+
preview: (input, options) => previewInvoice(input, options === void 0 ? {} : options)
|
|
754
1124
|
};
|
|
755
1125
|
}
|
|
756
|
-
|
|
757
|
-
|
|
1126
|
+
function previewInvoice(input, options) {
|
|
1127
|
+
assertIssueObject(options, "options");
|
|
1128
|
+
assertIssueKeys(options, ["representedTaxId"], "options");
|
|
1129
|
+
if (options.representedTaxId !== void 0) {
|
|
1130
|
+
issueDocumentNumber(
|
|
1131
|
+
options.representedTaxId,
|
|
1132
|
+
"options.representedTaxId",
|
|
1133
|
+
11,
|
|
1134
|
+
11
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
758
1137
|
const { data, voucherClass, amounts } = deriveWsfeInvoice(input);
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1138
|
+
return {
|
|
1139
|
+
voucherClass,
|
|
1140
|
+
voucherType: data.voucherType,
|
|
1141
|
+
amounts,
|
|
1142
|
+
request: data
|
|
762
1143
|
};
|
|
763
|
-
|
|
764
|
-
|
|
1144
|
+
}
|
|
1145
|
+
async function issueInvoice(wsfe, input, options, context) {
|
|
1146
|
+
validateOptions(options);
|
|
1147
|
+
validateKeyStore(options, context);
|
|
1148
|
+
const prepared = deriveWsfeInvoice(input);
|
|
1149
|
+
return await runOperation(
|
|
1150
|
+
wsfe,
|
|
1151
|
+
"issue",
|
|
1152
|
+
input,
|
|
1153
|
+
() => Promise.resolve(prepared),
|
|
1154
|
+
options,
|
|
1155
|
+
context,
|
|
1156
|
+
prepared.amounts
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
async function runOperation(wsfe, operation, input, prepare, options, context, replayAmounts) {
|
|
1160
|
+
const store = context?.store;
|
|
1161
|
+
if (options.idempotencyKey === void 0 || !store || !context) {
|
|
1162
|
+
return runAuthorization(wsfe, await prepare(), options);
|
|
1163
|
+
}
|
|
1164
|
+
const key = attemptKey(
|
|
1165
|
+
context.environment,
|
|
1166
|
+
context.taxId,
|
|
1167
|
+
options.idempotencyKey
|
|
1168
|
+
);
|
|
1169
|
+
const representedTaxId = options.representedTaxId === void 0 ? void 0 : String(options.representedTaxId);
|
|
1170
|
+
const inputHash = canonicalHash({ input, representedTaxId });
|
|
1171
|
+
const existing = await storeCall(() => store.get(key));
|
|
1172
|
+
if (existing !== null) {
|
|
1173
|
+
return replay(existing);
|
|
1174
|
+
}
|
|
1175
|
+
const prepared = await prepare();
|
|
1176
|
+
const number = await nextNumber(wsfe, prepared.data, options);
|
|
1177
|
+
const record = {
|
|
1178
|
+
v: 1,
|
|
1179
|
+
operation,
|
|
1180
|
+
representedTaxId,
|
|
1181
|
+
inputHash,
|
|
1182
|
+
number,
|
|
1183
|
+
salesPoint: prepared.data.salesPoint,
|
|
1184
|
+
voucherType: prepared.data.voucherType,
|
|
1185
|
+
sent: prepared.data,
|
|
1186
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1187
|
+
};
|
|
1188
|
+
if (await storeCall(() => store.add(key, JSON.stringify(record)))) {
|
|
1189
|
+
return runAuthorization(wsfe, prepared, options, number);
|
|
1190
|
+
}
|
|
1191
|
+
const winner = await storeCall(() => store.get(key));
|
|
1192
|
+
if (winner === null) {
|
|
1193
|
+
throw new ArcaConfigurationError(
|
|
1194
|
+
"ARCA reservation disappeared after atomic creation lost."
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
return replay(winner);
|
|
1198
|
+
function replay(json) {
|
|
1199
|
+
const stored = readRecord(json);
|
|
1200
|
+
if (stored.operation !== operation || stored.inputHash !== inputHash || stored.representedTaxId !== representedTaxId) {
|
|
1201
|
+
throw new ArcaInputError(
|
|
1202
|
+
"The idempotency key was already used with different input or operation.",
|
|
1203
|
+
{
|
|
1204
|
+
code: "ARCA_INPUT_IDEMPOTENCY_MISMATCH",
|
|
1205
|
+
field: "options.idempotencyKey"
|
|
1206
|
+
}
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
return runAuthorization(
|
|
1210
|
+
wsfe,
|
|
1211
|
+
{
|
|
1212
|
+
...preparedFromRecord(stored),
|
|
1213
|
+
...replayAmounts ? { amounts: replayAmounts } : {}
|
|
1214
|
+
},
|
|
1215
|
+
options,
|
|
1216
|
+
stored.number,
|
|
1217
|
+
true
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
function validateKeyStore(options, context) {
|
|
1222
|
+
if (options.idempotencyKey === void 0) {
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
if (typeof options.idempotencyKey !== "string" || options.idempotencyKey.length < 1 || options.idempotencyKey.length > 255) {
|
|
1226
|
+
throw new ArcaInputError(
|
|
1227
|
+
"idempotencyKey must contain 1 to 255 characters.",
|
|
1228
|
+
{ code: "ARCA_INPUT_INVALID_VALUE", field: "options.idempotencyKey" }
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
if (!context?.store) {
|
|
1232
|
+
throw new ArcaConfigurationError(
|
|
1233
|
+
'idempotencyKey requires a store. Add import { createPostgresStore } from "facturas"; and store: createPostgresStore({ query }) to createArcaClient().'
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
function readRecord(json) {
|
|
1238
|
+
try {
|
|
1239
|
+
const record = JSON.parse(json);
|
|
1240
|
+
if (!record || record.v !== 1 || !["issue", "creditNote"].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) {
|
|
1241
|
+
throw new Error("Invalid reservation structure");
|
|
1242
|
+
}
|
|
1243
|
+
normalizeWsfeVoucherInput(record.sent);
|
|
1244
|
+
return record;
|
|
1245
|
+
} catch (cause) {
|
|
1246
|
+
throw new ArcaConfigurationError(
|
|
1247
|
+
"Invalid ARCA reservation record; preserve it for reconciliation.",
|
|
1248
|
+
{ cause }
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
function preparedFromRecord(record) {
|
|
1253
|
+
const sentTotal = Number(
|
|
1254
|
+
normalizeArcaAmountToMinorUnits(record.sent.totalAmount, "totalAmount")
|
|
1255
|
+
);
|
|
1256
|
+
return {
|
|
1257
|
+
data: record.sent,
|
|
1258
|
+
voucherClass: [1, 3].includes(record.voucherType) ? "A" : [6, 8].includes(record.voucherType) ? "B" : "C",
|
|
1259
|
+
amounts: { computedTotal: sentTotal, sentTotal, vatAdjustment: 0 }
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
async function nextNumber(wsfe, data, options) {
|
|
765
1263
|
const number = await wsfe.getNextVoucherNumber({
|
|
766
|
-
|
|
1264
|
+
representedTaxId: options.representedTaxId,
|
|
1265
|
+
forceRefresh: options.forceRefresh,
|
|
767
1266
|
salesPoint: data.salesPoint,
|
|
768
1267
|
voucherType: data.voucherType
|
|
769
1268
|
});
|
|
770
1269
|
if (!Number.isSafeInteger(number) || number < 1 || number > 99999999) {
|
|
771
1270
|
throw new ArcaServiceError(
|
|
772
1271
|
"WSFE returned an invalid next voucher number.",
|
|
773
|
-
{
|
|
774
|
-
service: "wsfe",
|
|
775
|
-
operation: "FECompUltimoAutorizado"
|
|
776
|
-
}
|
|
1272
|
+
{ service: "wsfe", operation: "FECompUltimoAutorizado" }
|
|
777
1273
|
);
|
|
778
1274
|
}
|
|
1275
|
+
return number;
|
|
1276
|
+
}
|
|
1277
|
+
async function runAuthorization(wsfe, { data, voucherClass, amounts }, options, reservedNumber, replay = false) {
|
|
1278
|
+
const auth = {
|
|
1279
|
+
representedTaxId: options.representedTaxId,
|
|
1280
|
+
forceRefresh: options.forceRefresh
|
|
1281
|
+
};
|
|
1282
|
+
const includeRaw = options.include?.raw === true;
|
|
1283
|
+
const includeExact = options.include?.exactInput === true;
|
|
1284
|
+
const number = reservedNumber ?? await nextNumber(wsfe, data, options);
|
|
779
1285
|
const attempted = {
|
|
780
1286
|
salesPoint: data.salesPoint,
|
|
781
1287
|
voucherType: data.voucherType,
|
|
782
1288
|
number
|
|
783
1289
|
};
|
|
784
1290
|
const exact = includeExact ? { sent: data } : {};
|
|
785
|
-
const authorization = await wsfe.authorizeVoucherOutcome({
|
|
786
|
-
...auth,
|
|
787
|
-
data,
|
|
788
|
-
voucherNumber: number
|
|
789
|
-
});
|
|
790
1291
|
const voucher = (cae, caeExpiry) => ({
|
|
791
1292
|
...attempted,
|
|
792
1293
|
voucherClass,
|
|
@@ -795,6 +1296,29 @@ async function issueInvoice(wsfe, input, options) {
|
|
|
795
1296
|
caeExpiry,
|
|
796
1297
|
amounts
|
|
797
1298
|
});
|
|
1299
|
+
const recovery = { wsfe, auth, data, attempted, includeRaw, exact, voucher };
|
|
1300
|
+
if (replay) {
|
|
1301
|
+
const attempt2 = replayEvidence();
|
|
1302
|
+
let lookup;
|
|
1303
|
+
try {
|
|
1304
|
+
lookup = await wsfe.lookupVoucher({ ...auth, ...attempted });
|
|
1305
|
+
} catch (error) {
|
|
1306
|
+
return {
|
|
1307
|
+
kind: "indeterminate",
|
|
1308
|
+
attempted,
|
|
1309
|
+
attempt: attempt2,
|
|
1310
|
+
lookup: { kind: "failed", error: toArcaSafeErrorMetadata(error) }
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
if (lookup.kind === "found") {
|
|
1314
|
+
return recoverInvoice({ ...recovery, attempt: attempt2, lookup });
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
const authorization = await wsfe.issue({
|
|
1318
|
+
...auth,
|
|
1319
|
+
data,
|
|
1320
|
+
voucherNumber: number
|
|
1321
|
+
});
|
|
798
1322
|
if (authorization.kind === "authorized" && authorization.caeExpiry && authorization.voucherNumber === number) {
|
|
799
1323
|
return {
|
|
800
1324
|
kind: "authorized",
|
|
@@ -805,6 +1329,24 @@ async function issueInvoice(wsfe, input, options) {
|
|
|
805
1329
|
};
|
|
806
1330
|
}
|
|
807
1331
|
if (authorization.kind === "rejected") {
|
|
1332
|
+
if (options.idempotencyKey !== void 0 && [...authorization.errors, ...authorization.observations].some(
|
|
1333
|
+
(issue) => issue.code === "10016"
|
|
1334
|
+
)) {
|
|
1335
|
+
const recovered = await recoverInvoice({
|
|
1336
|
+
...recovery,
|
|
1337
|
+
attempt: projectEvidence(
|
|
1338
|
+
{
|
|
1339
|
+
...authorization,
|
|
1340
|
+
kind: "indeterminate",
|
|
1341
|
+
reason: "contradictory_response"
|
|
1342
|
+
},
|
|
1343
|
+
includeRaw
|
|
1344
|
+
)
|
|
1345
|
+
});
|
|
1346
|
+
if (recovered.kind === "authorized" || recovered.kind === "conflict") {
|
|
1347
|
+
return recovered;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
808
1350
|
return {
|
|
809
1351
|
kind: "rejected",
|
|
810
1352
|
attempted,
|
|
@@ -839,11 +1381,12 @@ async function recoverInvoice({
|
|
|
839
1381
|
attempt,
|
|
840
1382
|
includeRaw,
|
|
841
1383
|
exact,
|
|
842
|
-
voucher
|
|
1384
|
+
voucher,
|
|
1385
|
+
lookup: suppliedLookup
|
|
843
1386
|
}) {
|
|
844
1387
|
let lookup;
|
|
845
1388
|
try {
|
|
846
|
-
lookup = await wsfe.lookupVoucher({ ...auth, ...attempted });
|
|
1389
|
+
lookup = suppliedLookup ?? await wsfe.lookupVoucher({ ...auth, ...attempted });
|
|
847
1390
|
} catch (error) {
|
|
848
1391
|
return {
|
|
849
1392
|
kind: "indeterminate",
|
|
@@ -873,7 +1416,7 @@ async function recoverInvoice({
|
|
|
873
1416
|
attempted,
|
|
874
1417
|
attempt,
|
|
875
1418
|
found: { ...toVoucherSummary(lookup.voucher), ...raw },
|
|
876
|
-
reason: `${matched.reason}.
|
|
1419
|
+
reason: `${matched.reason}. Configure a store and pass idempotencyKey for retries.`
|
|
877
1420
|
};
|
|
878
1421
|
}
|
|
879
1422
|
return {
|
|
@@ -947,7 +1490,7 @@ function validateOptions(options) {
|
|
|
947
1490
|
assertIssueObject(options, "options");
|
|
948
1491
|
assertIssueKeys(
|
|
949
1492
|
options,
|
|
950
|
-
["representedTaxId", "forceRefresh", "include"],
|
|
1493
|
+
["representedTaxId", "forceRefresh", "include", "idempotencyKey"],
|
|
951
1494
|
"options"
|
|
952
1495
|
);
|
|
953
1496
|
if (options.representedTaxId !== void 0) {
|
|
@@ -980,6 +1523,55 @@ function validateOptions(options) {
|
|
|
980
1523
|
}
|
|
981
1524
|
}
|
|
982
1525
|
}
|
|
1526
|
+
function replayEvidence() {
|
|
1527
|
+
return {
|
|
1528
|
+
kind: "indeterminate",
|
|
1529
|
+
service: "wsfe",
|
|
1530
|
+
operation: "FECAESolicitar",
|
|
1531
|
+
reason: "incomplete_response",
|
|
1532
|
+
results: {},
|
|
1533
|
+
errors: [],
|
|
1534
|
+
observations: []
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
async function issueCreditNote(wsfe, input, options, context) {
|
|
1538
|
+
validateOptions(options);
|
|
1539
|
+
validateKeyStore(options, context);
|
|
1540
|
+
const note = assertCreditNoteInput(input);
|
|
1541
|
+
const target = note.for;
|
|
1542
|
+
return await runOperation(
|
|
1543
|
+
wsfe,
|
|
1544
|
+
"creditNote",
|
|
1545
|
+
note,
|
|
1546
|
+
async () => {
|
|
1547
|
+
const original = await wsfe.lookupVoucher({
|
|
1548
|
+
representedTaxId: options.representedTaxId,
|
|
1549
|
+
forceRefresh: options.forceRefresh,
|
|
1550
|
+
salesPoint: target.salesPoint,
|
|
1551
|
+
voucherType: target.voucherType,
|
|
1552
|
+
number: target.number
|
|
1553
|
+
});
|
|
1554
|
+
if (original.kind === "not_found") {
|
|
1555
|
+
throw new ArcaInputError(
|
|
1556
|
+
"issueCreditNote failed: original voucher not found.",
|
|
1557
|
+
{
|
|
1558
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
1559
|
+
field: "input.for"
|
|
1560
|
+
}
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
if (original.voucher.salesPoint !== target.salesPoint || original.voucher.voucherType !== target.voucherType || original.voucher.voucherNumber !== target.number) {
|
|
1564
|
+
throw new ArcaInputError(
|
|
1565
|
+
"issueCreditNote failed: the original lookup coordinates do not match input.for.",
|
|
1566
|
+
{ code: "ARCA_INPUT_INVALID_VALUE", field: "input.for" }
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
return note.all === true ? deriveWsfeFullCreditNote(original.voucher, note) : deriveWsfePartialCreditNote(original.voucher, note);
|
|
1570
|
+
},
|
|
1571
|
+
options,
|
|
1572
|
+
context
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
983
1575
|
|
|
984
1576
|
// src/internal/http.ts
|
|
985
1577
|
import https from "https";
|
|
@@ -1389,7 +1981,7 @@ function createSoapTransport(options) {
|
|
|
1389
1981
|
}
|
|
1390
1982
|
|
|
1391
1983
|
// src/wsaa/index.ts
|
|
1392
|
-
import { createHash } from "crypto";
|
|
1984
|
+
import { createHash as createHash2 } from "crypto";
|
|
1393
1985
|
import forge from "node-forge";
|
|
1394
1986
|
|
|
1395
1987
|
// src/wsaa/session-store.ts
|
|
@@ -1623,7 +2215,7 @@ function buildWsaaSessionKey(config, service) {
|
|
|
1623
2215
|
};
|
|
1624
2216
|
}
|
|
1625
2217
|
function getCertificateFingerprint(config) {
|
|
1626
|
-
return
|
|
2218
|
+
return createHash2("sha256").update(config.certificatePem).digest("hex");
|
|
1627
2219
|
}
|
|
1628
2220
|
function getCachedCredentials(cache, cacheKey) {
|
|
1629
2221
|
const localCached = cache.get(cacheKey);
|
|
@@ -1814,10 +2406,40 @@ function parseLoginTicketResponse(xml) {
|
|
|
1814
2406
|
};
|
|
1815
2407
|
}
|
|
1816
2408
|
|
|
2409
|
+
// src/wsaa/store-adapter.ts
|
|
2410
|
+
function createWsaaStoreAdapter(store) {
|
|
2411
|
+
const key = (value) => `arca:v1:wsaa:${serializeWsaaSessionKey(value)}`;
|
|
2412
|
+
const remove = store.delete?.bind(store);
|
|
2413
|
+
const lock = store.withLock?.bind(store);
|
|
2414
|
+
return {
|
|
2415
|
+
get: (value) => storeCall(async () => {
|
|
2416
|
+
const json = await store.get(key(value));
|
|
2417
|
+
if (json === null) {
|
|
2418
|
+
return null;
|
|
2419
|
+
}
|
|
2420
|
+
const credentials = JSON.parse(json);
|
|
2421
|
+
return credentials && typeof credentials.token === "string" && typeof credentials.sign === "string" && isWsaaCredentialValid(credentials) ? credentials : null;
|
|
2422
|
+
}),
|
|
2423
|
+
set: (value, credentials) => storeCall(() => store.set(key(value), JSON.stringify(credentials))),
|
|
2424
|
+
...remove ? {
|
|
2425
|
+
delete: (value) => storeCall(() => remove(key(value)))
|
|
2426
|
+
} : {},
|
|
2427
|
+
...lock ? {
|
|
2428
|
+
withLock: (value, fn) => lock(key(value), fn)
|
|
2429
|
+
} : {}
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
|
|
1817
2433
|
// src/client.ts
|
|
1818
|
-
function createArcaClient(config) {
|
|
1819
|
-
|
|
1820
|
-
|
|
2434
|
+
function createArcaClient(config = {}) {
|
|
2435
|
+
const discovered = discoverArcaClientConfig(config);
|
|
2436
|
+
assertArcaClientConfig(discovered);
|
|
2437
|
+
const normalizedConfig = normalizeArcaClientConfig(discovered);
|
|
2438
|
+
if (normalizedConfig.store && !normalizedConfig.wsaaSessionStore) {
|
|
2439
|
+
normalizedConfig.wsaaSessionStore = createWsaaStoreAdapter(
|
|
2440
|
+
normalizedConfig.store
|
|
2441
|
+
);
|
|
2442
|
+
}
|
|
1821
2443
|
const logger = createArcaLogger(normalizedConfig.logger);
|
|
1822
2444
|
const auth = createWsaaAuthModule({ config: normalizedConfig, logger });
|
|
1823
2445
|
const soap = createSoapTransport({ config: normalizedConfig, logger });
|
|
@@ -1829,14 +2451,177 @@ function createArcaClient(config) {
|
|
|
1829
2451
|
retryDelay: normalizedConfig.retryDelay
|
|
1830
2452
|
});
|
|
1831
2453
|
const wsfe = createWsfeService({ config: normalizedConfig, auth, soap });
|
|
2454
|
+
const vouchers = createVouchersService(wsfe, normalizedConfig);
|
|
1832
2455
|
return {
|
|
1833
2456
|
config: publicConfig,
|
|
2457
|
+
issue: vouchers.issue,
|
|
2458
|
+
preview: vouchers.preview,
|
|
2459
|
+
issueCreditNote: vouchers.issueCreditNote,
|
|
1834
2460
|
wsfe,
|
|
1835
|
-
vouchers: createVouchersService(wsfe),
|
|
1836
2461
|
wsmtxca: createWsmtxcaService({ config: normalizedConfig, auth, soap }),
|
|
1837
2462
|
padron: createPadronService({ config: normalizedConfig, auth, soap })
|
|
1838
2463
|
};
|
|
1839
2464
|
}
|
|
2465
|
+
|
|
2466
|
+
// src/store/file.ts
|
|
2467
|
+
import { createHash as createHash3, randomUUID } from "crypto";
|
|
2468
|
+
import { mkdir, readFile, rename, unlink, writeFile } from "fs/promises";
|
|
2469
|
+
import { join } from "path";
|
|
2470
|
+
function createFileStore(directory) {
|
|
2471
|
+
const path = (key) => join(directory, createHash3("sha256").update(key).digest("hex"));
|
|
2472
|
+
const ensure = () => mkdir(directory, { recursive: true, mode: 448 });
|
|
2473
|
+
return {
|
|
2474
|
+
get: (key) => storeCall(async () => {
|
|
2475
|
+
try {
|
|
2476
|
+
return await readFile(path(key), "utf8");
|
|
2477
|
+
} catch (error) {
|
|
2478
|
+
if (error.code === "ENOENT") {
|
|
2479
|
+
return null;
|
|
2480
|
+
}
|
|
2481
|
+
throw error;
|
|
2482
|
+
}
|
|
2483
|
+
}),
|
|
2484
|
+
set: (key, value) => storeCall(async () => {
|
|
2485
|
+
await ensure();
|
|
2486
|
+
const temporary = `${path(key)}.${randomUUID()}.tmp`;
|
|
2487
|
+
try {
|
|
2488
|
+
await writeFile(temporary, value, { mode: 384, flag: "wx" });
|
|
2489
|
+
await rename(temporary, path(key));
|
|
2490
|
+
} finally {
|
|
2491
|
+
await unlink(temporary).catch(() => void 0);
|
|
2492
|
+
}
|
|
2493
|
+
}),
|
|
2494
|
+
add: (key, value) => storeCall(async () => {
|
|
2495
|
+
await ensure();
|
|
2496
|
+
try {
|
|
2497
|
+
await writeFile(path(key), value, { flag: "wx", mode: 384 });
|
|
2498
|
+
return true;
|
|
2499
|
+
} catch (error) {
|
|
2500
|
+
if (error.code === "EEXIST") {
|
|
2501
|
+
return false;
|
|
2502
|
+
}
|
|
2503
|
+
throw error;
|
|
2504
|
+
}
|
|
2505
|
+
}),
|
|
2506
|
+
delete: (key) => storeCall(async () => {
|
|
2507
|
+
try {
|
|
2508
|
+
await unlink(path(key));
|
|
2509
|
+
} catch (error) {
|
|
2510
|
+
if (error.code !== "ENOENT") {
|
|
2511
|
+
throw error;
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
})
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
// src/store/memory.ts
|
|
2519
|
+
function createMemoryStore() {
|
|
2520
|
+
const values = /* @__PURE__ */ new Map();
|
|
2521
|
+
const locks = /* @__PURE__ */ new Map();
|
|
2522
|
+
return {
|
|
2523
|
+
get: (key) => Promise.resolve(values.get(key) ?? null),
|
|
2524
|
+
set(key, value) {
|
|
2525
|
+
values.set(key, value);
|
|
2526
|
+
return Promise.resolve();
|
|
2527
|
+
},
|
|
2528
|
+
add(key, value) {
|
|
2529
|
+
if (values.has(key)) {
|
|
2530
|
+
return Promise.resolve(false);
|
|
2531
|
+
}
|
|
2532
|
+
values.set(key, value);
|
|
2533
|
+
return Promise.resolve(true);
|
|
2534
|
+
},
|
|
2535
|
+
delete(key) {
|
|
2536
|
+
values.delete(key);
|
|
2537
|
+
return Promise.resolve();
|
|
2538
|
+
},
|
|
2539
|
+
async withLock(key, fn) {
|
|
2540
|
+
const previous = locks.get(key) ?? Promise.resolve();
|
|
2541
|
+
let release = () => void 0;
|
|
2542
|
+
const current = new Promise((resolve) => {
|
|
2543
|
+
release = resolve;
|
|
2544
|
+
});
|
|
2545
|
+
const queued = previous.catch(() => void 0).then(() => current);
|
|
2546
|
+
locks.set(key, queued);
|
|
2547
|
+
await previous.catch(() => void 0);
|
|
2548
|
+
try {
|
|
2549
|
+
return await fn();
|
|
2550
|
+
} finally {
|
|
2551
|
+
release();
|
|
2552
|
+
if (locks.get(key) === queued) {
|
|
2553
|
+
locks.delete(key);
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
// src/store/postgres.ts
|
|
2561
|
+
function createPostgresStore({
|
|
2562
|
+
query,
|
|
2563
|
+
table = "arca_store"
|
|
2564
|
+
}) {
|
|
2565
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) {
|
|
2566
|
+
throw new ArcaConfigurationError("Invalid ARCA store table identifier.");
|
|
2567
|
+
}
|
|
2568
|
+
const name = `"${table}"`;
|
|
2569
|
+
const run = (text, params) => storeCall(async () => {
|
|
2570
|
+
const result = await query(text, params);
|
|
2571
|
+
return Array.isArray(result) ? result : result.rows;
|
|
2572
|
+
});
|
|
2573
|
+
return {
|
|
2574
|
+
async get(key) {
|
|
2575
|
+
return (await run(`SELECT value FROM ${name} WHERE key = $1`, [key]))[0]?.value ?? null;
|
|
2576
|
+
},
|
|
2577
|
+
async set(key, value) {
|
|
2578
|
+
await run(
|
|
2579
|
+
`INSERT INTO ${name} (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now()`,
|
|
2580
|
+
[key, value]
|
|
2581
|
+
);
|
|
2582
|
+
},
|
|
2583
|
+
async add(key, value) {
|
|
2584
|
+
return (await run(
|
|
2585
|
+
`INSERT INTO ${name} (key, value) VALUES ($1, $2) ON CONFLICT DO NOTHING RETURNING key`,
|
|
2586
|
+
[key, value]
|
|
2587
|
+
)).length > 0;
|
|
2588
|
+
},
|
|
2589
|
+
async delete(key) {
|
|
2590
|
+
await run(`DELETE FROM ${name} WHERE key = $1`, [key]);
|
|
2591
|
+
}
|
|
2592
|
+
};
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
// src/store/redis.ts
|
|
2596
|
+
function createRedisStore(client, options = {}) {
|
|
2597
|
+
const flavor = options.flavor ?? (typeof client.call === "function" ? "ioredis" : "upstash");
|
|
2598
|
+
if (flavor === "ioredis" && !client.call) {
|
|
2599
|
+
throw new ArcaConfigurationError(
|
|
2600
|
+
"ARCA Redis ioredis flavor requires client.call."
|
|
2601
|
+
);
|
|
2602
|
+
}
|
|
2603
|
+
const upstashSet = client.set;
|
|
2604
|
+
return {
|
|
2605
|
+
get: (key) => storeCall(async () => {
|
|
2606
|
+
const value = await client.get(key);
|
|
2607
|
+
if (value === null) {
|
|
2608
|
+
return null;
|
|
2609
|
+
}
|
|
2610
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
2611
|
+
}),
|
|
2612
|
+
set: (key, value) => storeCall(async () => {
|
|
2613
|
+
await client.set(key, value);
|
|
2614
|
+
}),
|
|
2615
|
+
add: (key, value) => storeCall(
|
|
2616
|
+
async () => (flavor === "ioredis" ? await client.call?.("SET", key, value, "NX") : await upstashSet.call(client, key, value, { nx: true })) === "OK"
|
|
2617
|
+
),
|
|
2618
|
+
...client.del ? {
|
|
2619
|
+
delete: (key) => storeCall(async () => {
|
|
2620
|
+
await client.del?.(key);
|
|
2621
|
+
})
|
|
2622
|
+
} : {}
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
1840
2625
|
export {
|
|
1841
2626
|
ARCA_ENVIRONMENTS,
|
|
1842
2627
|
ARCA_ENV_VARIABLES,
|
|
@@ -1857,8 +2642,12 @@ export {
|
|
|
1857
2642
|
buildFacturaC,
|
|
1858
2643
|
createArcaClient,
|
|
1859
2644
|
createArcaClientConfigFromEnv,
|
|
2645
|
+
createFileStore,
|
|
2646
|
+
createMemoryStore,
|
|
1860
2647
|
createMemoryWsaaSessionStore,
|
|
1861
2648
|
createPadronService,
|
|
2649
|
+
createPostgresStore,
|
|
2650
|
+
createRedisStore,
|
|
1862
2651
|
createWsfeService,
|
|
1863
2652
|
createWsmtxcaService,
|
|
1864
2653
|
isArcaAuthenticationError,
|