facturas 0.9.0 → 0.11.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 +95 -786
- package/bin/facturas.mjs +4 -0
- package/dist/{chunk-RCM4NAAG.mjs → chunk-5KCTRQJX.mjs} +83 -284
- package/dist/chunk-5KCTRQJX.mjs.map +1 -0
- package/dist/{chunk-XNBD5NVL.mjs → chunk-ALH5IPSY.mjs} +2 -57
- package/dist/chunk-ALH5IPSY.mjs.map +1 -0
- package/dist/{chunk-76WU5BVI.mjs → chunk-GX5ERXT6.mjs} +1 -1
- package/dist/chunk-GX5ERXT6.mjs.map +1 -0
- package/dist/chunk-LOWV7IF6.mjs +3758 -0
- package/dist/chunk-LOWV7IF6.mjs.map +1 -0
- package/dist/chunk-LR4M2AMG.mjs +169 -0
- package/dist/chunk-LR4M2AMG.mjs.map +1 -0
- package/dist/{chunk-4QVFXT35.mjs → chunk-UDX6HLAU.mjs} +1 -17
- package/dist/{chunk-4QVFXT35.mjs.map → chunk-UDX6HLAU.mjs.map} +1 -1
- package/dist/cli.d.ts +49 -0
- package/dist/cli.mjs +2316 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/client-BZxeScuK.d.ts +521 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.mjs +1 -1
- package/dist/{wsfe-DF0jAE2k.d.ts → decimal-B0HtrMFY.d.ts} +9 -66
- package/dist/errors.d.ts +2 -2
- package/dist/{fiscal-evidence-DBzakqBE.d.ts → fiscal-evidence-B1Qe6XMg.d.ts} +2 -2
- package/dist/{index-CVGQyyYM.d.ts → index-D8LGFvxV.d.ts} +1 -1
- package/dist/index.d.ts +11 -235
- package/dist/index.mjs +21 -2315
- package/dist/index.mjs.map +1 -1
- package/dist/padron.d.ts +2 -2
- package/dist/{types-BiznC1-X.d.ts → types-r2nf55gh.d.ts} +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/wsfe.d.ts +45 -4
- package/dist/wsfe.mjs +6 -4
- package/dist/wsmtxca.d.ts +4 -20
- package/dist/wsmtxca.mjs +2 -2
- package/package.json +5 -1
- package/dist/chunk-76WU5BVI.mjs.map +0 -1
- package/dist/chunk-RCM4NAAG.mjs.map +0 -1
- package/dist/chunk-XNBD5NVL.mjs.map +0 -1
|
@@ -0,0 +1,3758 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createPadronService
|
|
3
|
+
} from "./chunk-7LEKQ4P7.mjs";
|
|
4
|
+
import {
|
|
5
|
+
arcaMinorUnitsToNumber,
|
|
6
|
+
assertArcaMinorUnits,
|
|
7
|
+
calculateWsfeAmounts,
|
|
8
|
+
createWsfeService,
|
|
9
|
+
isWithinArcaTolerance,
|
|
10
|
+
normalizeArcaAmountToMinorUnits,
|
|
11
|
+
normalizeWsfeDateInput,
|
|
12
|
+
normalizeWsfeVoucherInput,
|
|
13
|
+
serializeArcaExchangeRate
|
|
14
|
+
} from "./chunk-5KCTRQJX.mjs";
|
|
15
|
+
import {
|
|
16
|
+
ARCA_CURRENCY_IDS,
|
|
17
|
+
ARCA_DOCUMENT_TYPES,
|
|
18
|
+
ARCA_FINAL_CONSUMER_IDENTIFICATION_THRESHOLD_MINOR_UNITS,
|
|
19
|
+
ARCA_INVOICE_CLASS_BY_ISSUER,
|
|
20
|
+
ARCA_ISSUER_CONDITION_IDS,
|
|
21
|
+
ARCA_RECEIVER_CONDITION_IDS,
|
|
22
|
+
ARCA_VOUCHER_TYPES
|
|
23
|
+
} from "./chunk-GX5ERXT6.mjs";
|
|
24
|
+
import {
|
|
25
|
+
createWsmtxcaService
|
|
26
|
+
} from "./chunk-ALH5IPSY.mjs";
|
|
27
|
+
import {
|
|
28
|
+
ArcaConfigurationError,
|
|
29
|
+
ArcaError,
|
|
30
|
+
ArcaInputError,
|
|
31
|
+
ArcaInvalidSoapResponseError,
|
|
32
|
+
ArcaServiceError,
|
|
33
|
+
ArcaSoapFaultError,
|
|
34
|
+
ArcaTransportError,
|
|
35
|
+
createResponseBodyDiagnostic,
|
|
36
|
+
createSafeErrorDiagnostic,
|
|
37
|
+
toArcaSafeErrorMetadata
|
|
38
|
+
} from "./chunk-ZOSNASXB.mjs";
|
|
39
|
+
|
|
40
|
+
// src/config.ts
|
|
41
|
+
var ARCA_ENVIRONMENTS = ["production", "test"];
|
|
42
|
+
var ARCA_ENV_VARIABLES = {
|
|
43
|
+
taxId: "ARCA_TAX_ID",
|
|
44
|
+
certificatePem: "ARCA_CERTIFICATE_PEM",
|
|
45
|
+
privateKeyPem: "ARCA_PRIVATE_KEY_PEM",
|
|
46
|
+
environment: "ARCA_ENVIRONMENT"
|
|
47
|
+
};
|
|
48
|
+
var PRIVATE_KEY_PEM_PREFIXES = [
|
|
49
|
+
"-----BEGIN PRIVATE KEY-----",
|
|
50
|
+
"-----BEGIN RSA PRIVATE KEY-----"
|
|
51
|
+
];
|
|
52
|
+
var ENCRYPTED_PRIVATE_KEY_PEM_PREFIX = "-----BEGIN ENCRYPTED PRIVATE KEY-----";
|
|
53
|
+
var LEGACY_ENCRYPTED_RSA_PRIVATE_KEY_PATTERN = /^-----BEGIN RSA PRIVATE KEY-----[\s\S]*^Proc-Type:\s*4,\s*ENCRYPTED\s*$/m;
|
|
54
|
+
var VALID_ARCA_LOG_LEVELS = ["debug", "info", "warn", "error"];
|
|
55
|
+
var DEFAULT_ARCA_TIMEOUT_MS = 3e4;
|
|
56
|
+
var DEFAULT_ARCA_RETRIES = 0;
|
|
57
|
+
var DEFAULT_ARCA_RETRY_DELAY_MS = 500;
|
|
58
|
+
function resolveArcaEnvironment(production) {
|
|
59
|
+
return production ? "production" : "test";
|
|
60
|
+
}
|
|
61
|
+
function createArcaClientConfigFromEnv(options = {}) {
|
|
62
|
+
const env = options.env ?? process.env;
|
|
63
|
+
const variableNames = {
|
|
64
|
+
...ARCA_ENV_VARIABLES,
|
|
65
|
+
...options.variableNames
|
|
66
|
+
};
|
|
67
|
+
const environmentInput = readEnv(env, variableNames.environment);
|
|
68
|
+
const environmentValue = normalizeEnvironmentValue(environmentInput);
|
|
69
|
+
const config = {
|
|
70
|
+
taxId: readEnv(env, variableNames.taxId) ?? "",
|
|
71
|
+
certificatePem: readEnv(env, variableNames.certificatePem) ?? "",
|
|
72
|
+
privateKeyPem: readEnv(env, variableNames.privateKeyPem) ?? "",
|
|
73
|
+
environment: environmentValue ?? environmentInput ?? options.defaultEnvironment ?? "test"
|
|
74
|
+
};
|
|
75
|
+
assertArcaClientConfig(config);
|
|
76
|
+
return normalizeArcaClientConfig(config);
|
|
77
|
+
}
|
|
78
|
+
function assertArcaClientConfig(config) {
|
|
79
|
+
const invalidFields = [];
|
|
80
|
+
const normalized = normalizeArcaClientConfig(config);
|
|
81
|
+
const timeout = normalized.timeout ?? DEFAULT_ARCA_TIMEOUT_MS;
|
|
82
|
+
const retries = normalized.retries ?? DEFAULT_ARCA_RETRIES;
|
|
83
|
+
const retryDelay = normalized.retryDelay ?? DEFAULT_ARCA_RETRY_DELAY_MS;
|
|
84
|
+
if (normalized.privateKeyPem.startsWith(ENCRYPTED_PRIVATE_KEY_PEM_PREFIX) || LEGACY_ENCRYPTED_RSA_PRIVATE_KEY_PATTERN.test(normalized.privateKeyPem)) {
|
|
85
|
+
throw new ArcaConfigurationError(
|
|
86
|
+
"Encrypted private keys are not supported. Provide an unencrypted PKCS#8 or RSA private key PEM."
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
invalidFields.push(...invalidCredentialFields(normalized));
|
|
90
|
+
if (!ARCA_ENVIRONMENTS.includes(normalized.environment)) {
|
|
91
|
+
invalidFields.push("environment");
|
|
92
|
+
}
|
|
93
|
+
if (!Number.isFinite(timeout) || timeout <= 0) {
|
|
94
|
+
invalidFields.push("timeout");
|
|
95
|
+
}
|
|
96
|
+
if (!Number.isInteger(retries) || retries < 0) {
|
|
97
|
+
invalidFields.push("retries");
|
|
98
|
+
}
|
|
99
|
+
if (!Number.isFinite(retryDelay) || retryDelay < 0) {
|
|
100
|
+
invalidFields.push("retryDelay");
|
|
101
|
+
}
|
|
102
|
+
const loggerLevel = normalized.logger?.level;
|
|
103
|
+
if (loggerLevel !== void 0 && !VALID_ARCA_LOG_LEVELS.includes(loggerLevel)) {
|
|
104
|
+
invalidFields.push("logger.level");
|
|
105
|
+
}
|
|
106
|
+
if (normalized.logger?.log !== void 0 && typeof normalized.logger.log !== "function") {
|
|
107
|
+
invalidFields.push("logger.log");
|
|
108
|
+
}
|
|
109
|
+
invalidFields.push(...getInvalidWsaaSessionStoreFields(normalized));
|
|
110
|
+
if (invalidFields.length > 0) {
|
|
111
|
+
throw new ArcaConfigurationError(
|
|
112
|
+
`Missing or invalid ARCA client config fields: ${invalidFields.join(", ")}`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function getInvalidWsaaSessionStoreFields(config) {
|
|
117
|
+
const store = config.wsaaSessionStore;
|
|
118
|
+
if (store === void 0) {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
const invalidFields = [];
|
|
122
|
+
if (typeof store.get !== "function") {
|
|
123
|
+
invalidFields.push("wsaaSessionStore.get");
|
|
124
|
+
}
|
|
125
|
+
if (typeof store.set !== "function") {
|
|
126
|
+
invalidFields.push("wsaaSessionStore.set");
|
|
127
|
+
}
|
|
128
|
+
if (store.delete !== void 0 && typeof store.delete !== "function") {
|
|
129
|
+
invalidFields.push("wsaaSessionStore.delete");
|
|
130
|
+
}
|
|
131
|
+
if (store.withLock !== void 0 && typeof store.withLock !== "function") {
|
|
132
|
+
invalidFields.push("wsaaSessionStore.withLock");
|
|
133
|
+
}
|
|
134
|
+
return invalidFields;
|
|
135
|
+
}
|
|
136
|
+
var ARCA_WSAA_CONFIG = {
|
|
137
|
+
namespace: "http://wsaa.view.sua.dvadac.desein.afip.gov",
|
|
138
|
+
endpoint: {
|
|
139
|
+
production: "https://wsaa.afip.gov.ar/ws/services/LoginCms",
|
|
140
|
+
test: "https://wsaahomo.afip.gov.ar/ws/services/LoginCms"
|
|
141
|
+
},
|
|
142
|
+
soapVersion: "1.1",
|
|
143
|
+
soapActionBase: "",
|
|
144
|
+
usesEmptySoapAction: true
|
|
145
|
+
};
|
|
146
|
+
var ARCA_SERVICE_CONFIG = {
|
|
147
|
+
wsaa: ARCA_WSAA_CONFIG,
|
|
148
|
+
wsfe: {
|
|
149
|
+
namespace: "http://ar.gov.afip.dif.FEV1/",
|
|
150
|
+
endpoint: {
|
|
151
|
+
production: "https://servicios1.afip.gov.ar/wsfev1/service.asmx",
|
|
152
|
+
test: "https://wswhomo.afip.gov.ar/wsfev1/service.asmx"
|
|
153
|
+
},
|
|
154
|
+
soapVersion: "1.2",
|
|
155
|
+
soapActionBase: "http://ar.gov.afip.dif.FEV1/",
|
|
156
|
+
useLegacyTlsSecurityLevel0: true
|
|
157
|
+
},
|
|
158
|
+
wsmtxca: {
|
|
159
|
+
namespace: "http://impl.service.wsmtxca.afip.gov.ar/service/",
|
|
160
|
+
endpoint: {
|
|
161
|
+
production: "https://serviciosjava.afip.gov.ar/wsmtxca/services/MTXCAService",
|
|
162
|
+
test: "https://fwshomo.afip.gov.ar/wsmtxca/services/MTXCAService"
|
|
163
|
+
},
|
|
164
|
+
soapVersion: "1.1",
|
|
165
|
+
soapActionBase: "http://impl.service.wsmtxca.afip.gov.ar/service/"
|
|
166
|
+
},
|
|
167
|
+
"padron-a5": {
|
|
168
|
+
namespace: "http://a5.soap.ws.server.puc.sr/",
|
|
169
|
+
endpoint: {
|
|
170
|
+
production: "https://aws.afip.gov.ar/sr-padron/webservices/personaServiceA5",
|
|
171
|
+
test: "https://awshomo.afip.gov.ar/sr-padron/webservices/personaServiceA5"
|
|
172
|
+
},
|
|
173
|
+
soapVersion: "1.1",
|
|
174
|
+
soapActionBase: "",
|
|
175
|
+
usesEmptySoapAction: true
|
|
176
|
+
},
|
|
177
|
+
"padron-a13": {
|
|
178
|
+
namespace: "http://a13.soap.ws.server.puc.sr/",
|
|
179
|
+
endpoint: {
|
|
180
|
+
production: "https://aws.afip.gov.ar/sr-padron/webservices/personaServiceA13",
|
|
181
|
+
test: "https://awshomo.afip.gov.ar/sr-padron/webservices/personaServiceA13"
|
|
182
|
+
},
|
|
183
|
+
soapVersion: "1.1",
|
|
184
|
+
soapActionBase: "",
|
|
185
|
+
usesEmptySoapAction: true
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
function getArcaServiceConfig(service) {
|
|
189
|
+
const serviceConfig = ARCA_SERVICE_CONFIG[service];
|
|
190
|
+
if (!serviceConfig) {
|
|
191
|
+
throw new ArcaConfigurationError(
|
|
192
|
+
`Unsupported ARCA service configuration: ${service}`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return serviceConfig;
|
|
196
|
+
}
|
|
197
|
+
function normalizeArcaClientConfig(config) {
|
|
198
|
+
const normalizedEnvironment = normalizeEnvironmentValue(String(config.environment)) ?? config.environment;
|
|
199
|
+
const normalizedLoggerLevel = normalizeLogLevelValue(config.logger?.level);
|
|
200
|
+
return {
|
|
201
|
+
taxId: config.taxId?.trim() ?? "",
|
|
202
|
+
certificatePem: config.certificatePem?.trim() ?? "",
|
|
203
|
+
privateKeyPem: config.privateKeyPem?.trim() ?? "",
|
|
204
|
+
environment: normalizedEnvironment ?? "test",
|
|
205
|
+
...config.store === void 0 ? {} : { store: config.store },
|
|
206
|
+
timeout: config.timeout ?? DEFAULT_ARCA_TIMEOUT_MS,
|
|
207
|
+
retries: config.retries ?? DEFAULT_ARCA_RETRIES,
|
|
208
|
+
retryDelay: config.retryDelay ?? DEFAULT_ARCA_RETRY_DELAY_MS,
|
|
209
|
+
...config.logger === void 0 ? {} : {
|
|
210
|
+
logger: {
|
|
211
|
+
...config.logger,
|
|
212
|
+
...normalizedLoggerLevel === void 0 ? {} : { level: normalizedLoggerLevel }
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
...config.wsaaSessionStore === void 0 ? {} : { wsaaSessionStore: config.wsaaSessionStore }
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function normalizeEnvironmentValue(value) {
|
|
219
|
+
if (!value) {
|
|
220
|
+
return void 0;
|
|
221
|
+
}
|
|
222
|
+
const normalized = value.trim().toLowerCase();
|
|
223
|
+
if (ARCA_ENVIRONMENTS.includes(normalized)) {
|
|
224
|
+
return normalized;
|
|
225
|
+
}
|
|
226
|
+
return void 0;
|
|
227
|
+
}
|
|
228
|
+
function readEnv(env, variableName) {
|
|
229
|
+
return env[variableName]?.trim() || void 0;
|
|
230
|
+
}
|
|
231
|
+
function normalizeLogLevelValue(value) {
|
|
232
|
+
if (!value) {
|
|
233
|
+
return void 0;
|
|
234
|
+
}
|
|
235
|
+
const normalized = value.trim().toLowerCase();
|
|
236
|
+
if (VALID_ARCA_LOG_LEVELS.includes(normalized)) {
|
|
237
|
+
return normalized;
|
|
238
|
+
}
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
241
|
+
function discoverArcaClientConfig(config) {
|
|
242
|
+
const environment = config.environment ?? readEnv(process.env, ARCA_ENV_VARIABLES.environment);
|
|
243
|
+
if (environment === void 0) {
|
|
244
|
+
throw new ArcaConfigurationError(
|
|
245
|
+
`environment is required: pass environment or set ${ARCA_ENV_VARIABLES.environment} to "test" or "production".`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
...config,
|
|
250
|
+
taxId: config.taxId ?? readEnv(process.env, ARCA_ENV_VARIABLES.taxId) ?? "",
|
|
251
|
+
certificatePem: config.certificatePem ?? readEnv(process.env, ARCA_ENV_VARIABLES.certificatePem) ?? "",
|
|
252
|
+
privateKeyPem: config.privateKeyPem ?? readEnv(process.env, ARCA_ENV_VARIABLES.privateKeyPem) ?? "",
|
|
253
|
+
environment
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function invalidCredentialFields(normalized) {
|
|
257
|
+
const invalidFields = [];
|
|
258
|
+
if (!/^\d{11}$/.test(normalized.taxId)) {
|
|
259
|
+
invalidFields.push(normalized.taxId ? "taxId" : "taxId (ARCA_TAX_ID)");
|
|
260
|
+
}
|
|
261
|
+
if (!normalized.certificatePem.startsWith("-----BEGIN CERTIFICATE-----")) {
|
|
262
|
+
invalidFields.push(
|
|
263
|
+
normalized.certificatePem ? "certificatePem" : "certificatePem (ARCA_CERTIFICATE_PEM)"
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (!PRIVATE_KEY_PEM_PREFIXES.some(
|
|
267
|
+
(prefix) => normalized.privateKeyPem.startsWith(prefix)
|
|
268
|
+
)) {
|
|
269
|
+
invalidFields.push(
|
|
270
|
+
normalized.privateKeyPem ? "privateKeyPem" : "privateKeyPem (ARCA_PRIVATE_KEY_PEM)"
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
return invalidFields;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/store/types.ts
|
|
277
|
+
import { createHash } from "crypto";
|
|
278
|
+
function attemptKey(environment, taxId, key) {
|
|
279
|
+
return `arca:v1:attempt:${environment}:${taxId}:${key}`;
|
|
280
|
+
}
|
|
281
|
+
function canonicalHash(input) {
|
|
282
|
+
return createHash("sha256").update(JSON.stringify(canonical(input))).digest("hex");
|
|
283
|
+
}
|
|
284
|
+
function canonical(value) {
|
|
285
|
+
if (Array.isArray(value)) {
|
|
286
|
+
return value.map(canonical);
|
|
287
|
+
}
|
|
288
|
+
if (value instanceof Date) {
|
|
289
|
+
return value.toJSON();
|
|
290
|
+
}
|
|
291
|
+
if (value !== null && typeof value === "object") {
|
|
292
|
+
return Object.fromEntries(
|
|
293
|
+
Object.entries(value).filter(([, item]) => item !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, item]) => [key, canonical(item)])
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
return value;
|
|
297
|
+
}
|
|
298
|
+
async function storeCall(fn) {
|
|
299
|
+
try {
|
|
300
|
+
return await fn();
|
|
301
|
+
} catch (cause) {
|
|
302
|
+
throw new ArcaConfigurationError("ARCA store operation failed.", { cause });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/services/wsfe-identity.ts
|
|
307
|
+
function matchWsfeVoucherIdentity(sent, number, found) {
|
|
308
|
+
let missing;
|
|
309
|
+
const compare = (field, expected, actual, normalize) => {
|
|
310
|
+
if (actual === void 0 || actual === null || expected === void 0) {
|
|
311
|
+
missing ??= field;
|
|
312
|
+
return void 0;
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
const left = normalize ? normalize(expected) : expected;
|
|
316
|
+
const right = normalize ? normalize(actual) : actual;
|
|
317
|
+
if (left !== right) {
|
|
318
|
+
return {
|
|
319
|
+
matches: false,
|
|
320
|
+
evidence: "conflict",
|
|
321
|
+
reason: `${field} differs from the sent input`
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
} catch {
|
|
325
|
+
missing ??= field;
|
|
326
|
+
}
|
|
327
|
+
return void 0;
|
|
328
|
+
};
|
|
329
|
+
const checks = [
|
|
330
|
+
["voucherType", sent.voucherType, found.voucherType],
|
|
331
|
+
["salesPoint", sent.salesPoint, found.salesPoint],
|
|
332
|
+
["number", number, found.voucherNumber, normalizeVoucherNumber],
|
|
333
|
+
[
|
|
334
|
+
"date",
|
|
335
|
+
sent.voucherDate,
|
|
336
|
+
found.voucherDate,
|
|
337
|
+
(value) => normalizeWsfeDateInput(value, "date")
|
|
338
|
+
],
|
|
339
|
+
["concept", sent.concept, found.concept],
|
|
340
|
+
["documentType", sent.documentType, found.documentType],
|
|
341
|
+
[
|
|
342
|
+
"documentNumber",
|
|
343
|
+
sent.documentNumber,
|
|
344
|
+
found.documentNumber,
|
|
345
|
+
normalizeDocument
|
|
346
|
+
],
|
|
347
|
+
[
|
|
348
|
+
"receiverVatConditionId",
|
|
349
|
+
sent.receiverVatConditionId,
|
|
350
|
+
found.receiverVatConditionId
|
|
351
|
+
],
|
|
352
|
+
["currencyId", sent.currencyId, found.currencyId],
|
|
353
|
+
[
|
|
354
|
+
"exchangeRate",
|
|
355
|
+
sent.exchangeRate ?? (sent.currencyId === "PES" ? 1 : void 0),
|
|
356
|
+
found.exchangeRate,
|
|
357
|
+
(value) => serializeArcaExchangeRate(value, "exchangeRate")
|
|
358
|
+
]
|
|
359
|
+
];
|
|
360
|
+
for (const field of [
|
|
361
|
+
"totalAmount",
|
|
362
|
+
"netAmount",
|
|
363
|
+
"vatAmount",
|
|
364
|
+
"exemptAmount",
|
|
365
|
+
"nonTaxableAmount",
|
|
366
|
+
"taxAmount"
|
|
367
|
+
]) {
|
|
368
|
+
checks.push([
|
|
369
|
+
field,
|
|
370
|
+
sent[field],
|
|
371
|
+
found[field],
|
|
372
|
+
(value) => normalizeArcaAmountToMinorUnits(value, field)
|
|
373
|
+
]);
|
|
374
|
+
}
|
|
375
|
+
if (sent.concept === 2 || sent.concept === 3) {
|
|
376
|
+
for (const field of [
|
|
377
|
+
"serviceStartDate",
|
|
378
|
+
"serviceEndDate",
|
|
379
|
+
"paymentDueDate"
|
|
380
|
+
]) {
|
|
381
|
+
checks.push([
|
|
382
|
+
field,
|
|
383
|
+
sent[field],
|
|
384
|
+
found[field],
|
|
385
|
+
(value) => normalizeWsfeDateInput(value, field)
|
|
386
|
+
]);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (sent.concept === 1 && sent.paymentDueDate) {
|
|
390
|
+
checks.push([
|
|
391
|
+
"paymentDueDate",
|
|
392
|
+
sent.paymentDueDate,
|
|
393
|
+
found.paymentDueDate,
|
|
394
|
+
(value) => normalizeWsfeDateInput(value, "paymentDueDate")
|
|
395
|
+
]);
|
|
396
|
+
}
|
|
397
|
+
for (const check of checks) {
|
|
398
|
+
const result = compare(...check);
|
|
399
|
+
if (result) {
|
|
400
|
+
return result;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const detailMatch = compareDetails(sent, found);
|
|
404
|
+
if (!detailMatch.matches) {
|
|
405
|
+
if (detailMatch.evidence === "conflict") {
|
|
406
|
+
return detailMatch;
|
|
407
|
+
}
|
|
408
|
+
missing ??= detailMatch.reason;
|
|
409
|
+
}
|
|
410
|
+
missing ??= incompleteAuthorization(sent, found);
|
|
411
|
+
return missing ? {
|
|
412
|
+
matches: false,
|
|
413
|
+
evidence: "incomplete",
|
|
414
|
+
reason: `Cannot verify ${missing}`
|
|
415
|
+
} : { matches: true };
|
|
416
|
+
}
|
|
417
|
+
function incompleteAuthorization(sent, found) {
|
|
418
|
+
let missing;
|
|
419
|
+
if (sent.concept !== 1 && sent.concept !== 2 && sent.concept !== 3) {
|
|
420
|
+
missing ??= "unsupported concept";
|
|
421
|
+
}
|
|
422
|
+
if (!(found.result === "A" || found.result === "O")) {
|
|
423
|
+
missing ??= "authorized result";
|
|
424
|
+
}
|
|
425
|
+
if (!found.cae?.trim()) {
|
|
426
|
+
missing ??= "cae";
|
|
427
|
+
}
|
|
428
|
+
if (!found.caeExpiry?.trim()) {
|
|
429
|
+
missing ??= "caeExpiry";
|
|
430
|
+
}
|
|
431
|
+
return missing;
|
|
432
|
+
}
|
|
433
|
+
function normalizeVoucherNumber(value) {
|
|
434
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 99999999) {
|
|
435
|
+
throw new Error("Invalid voucher number");
|
|
436
|
+
}
|
|
437
|
+
return value;
|
|
438
|
+
}
|
|
439
|
+
function normalizeDocument(value) {
|
|
440
|
+
const text = String(value);
|
|
441
|
+
if (!/^\d+$/.test(text)) {
|
|
442
|
+
throw new Error("Invalid document number");
|
|
443
|
+
}
|
|
444
|
+
return BigInt(text);
|
|
445
|
+
}
|
|
446
|
+
function compareVatRates(expected, actual) {
|
|
447
|
+
if (actual === void 0) {
|
|
448
|
+
return expected.length === 0 ? { matches: true } : { matches: false, evidence: "incomplete", reason: "vatRates" };
|
|
449
|
+
}
|
|
450
|
+
if (actual.length !== expected.length || new Set(actual.map((rate) => rate.id)).size !== actual.length) {
|
|
451
|
+
return {
|
|
452
|
+
matches: false,
|
|
453
|
+
evidence: "conflict",
|
|
454
|
+
reason: "vatRates ids differ from the sent input"
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
for (const rate of expected) {
|
|
458
|
+
const found = actual.find((item) => item.id === rate.id);
|
|
459
|
+
if (!found) {
|
|
460
|
+
return {
|
|
461
|
+
matches: false,
|
|
462
|
+
evidence: "conflict",
|
|
463
|
+
reason: `vatRates id ${rate.id} differs from the sent input`
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
for (const field of ["baseAmount", "amount"]) {
|
|
467
|
+
try {
|
|
468
|
+
if (normalizeArcaAmountToMinorUnits(rate[field], field) !== normalizeArcaAmountToMinorUnits(found[field], field)) {
|
|
469
|
+
return {
|
|
470
|
+
matches: false,
|
|
471
|
+
evidence: "conflict",
|
|
472
|
+
reason: `vatRates[${rate.id}].${field} differs from the sent input`
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
} catch {
|
|
476
|
+
return {
|
|
477
|
+
matches: false,
|
|
478
|
+
evidence: "incomplete",
|
|
479
|
+
reason: `vatRates[${rate.id}].${field}`
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return { matches: true };
|
|
485
|
+
}
|
|
486
|
+
function toVoucherSummary(found) {
|
|
487
|
+
const summary = { number: found.voucherNumber };
|
|
488
|
+
for (const field of [
|
|
489
|
+
"salesPoint",
|
|
490
|
+
"voucherType",
|
|
491
|
+
"concept",
|
|
492
|
+
"documentType",
|
|
493
|
+
"documentNumber",
|
|
494
|
+
"receiverVatConditionId",
|
|
495
|
+
"currencyId",
|
|
496
|
+
"exchangeRate",
|
|
497
|
+
"totalAmount",
|
|
498
|
+
"netAmount",
|
|
499
|
+
"vatAmount",
|
|
500
|
+
"exemptAmount",
|
|
501
|
+
"nonTaxableAmount",
|
|
502
|
+
"taxAmount",
|
|
503
|
+
"serviceStartDate",
|
|
504
|
+
"serviceEndDate",
|
|
505
|
+
"paymentDueDate",
|
|
506
|
+
"result",
|
|
507
|
+
"cae",
|
|
508
|
+
"caeExpiry"
|
|
509
|
+
]) {
|
|
510
|
+
if (found[field] !== void 0) {
|
|
511
|
+
Object.assign(summary, { [field]: found[field] });
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (found.voucherDate !== void 0) {
|
|
515
|
+
summary.date = found.voucherDate;
|
|
516
|
+
}
|
|
517
|
+
if (found.vatRates !== void 0) {
|
|
518
|
+
summary.vatRates = found.vatRates.map(({ id, baseAmount, amount }) => ({
|
|
519
|
+
id,
|
|
520
|
+
baseAmount,
|
|
521
|
+
amount
|
|
522
|
+
}));
|
|
523
|
+
}
|
|
524
|
+
return summary;
|
|
525
|
+
}
|
|
526
|
+
function compareAssociations(sent, found) {
|
|
527
|
+
const expected = sent.associatedVouchers ?? [];
|
|
528
|
+
const actual = found.associatedVouchers;
|
|
529
|
+
if (!actual) {
|
|
530
|
+
return expected.length ? { matches: false, evidence: "incomplete", reason: "associatedVouchers" } : { matches: true };
|
|
531
|
+
}
|
|
532
|
+
if (expected.length !== actual.length) {
|
|
533
|
+
return {
|
|
534
|
+
matches: false,
|
|
535
|
+
evidence: "conflict",
|
|
536
|
+
reason: "associatedVouchers count differs"
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
for (const association of expected) {
|
|
540
|
+
const match = actual.find(
|
|
541
|
+
(v) => v.type === association.type && v.salesPoint === association.salesPoint && v.number === association.number
|
|
542
|
+
);
|
|
543
|
+
if (!match) {
|
|
544
|
+
return {
|
|
545
|
+
matches: false,
|
|
546
|
+
evidence: "conflict",
|
|
547
|
+
reason: "associatedVouchers differ from the sent input"
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
const metadata = compareAssociationMetadata(association, match);
|
|
551
|
+
if (!metadata.matches) {
|
|
552
|
+
return metadata;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return { matches: true };
|
|
556
|
+
}
|
|
557
|
+
function compareDetails(sent, found) {
|
|
558
|
+
let incomplete;
|
|
559
|
+
for (const result of [
|
|
560
|
+
compareVatRates(sent.vatRates ?? [], found.vatRates),
|
|
561
|
+
compareAssociations(sent, found),
|
|
562
|
+
compareExtensions(sent, found)
|
|
563
|
+
]) {
|
|
564
|
+
if (result.matches) {
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
if (result.evidence === "conflict") {
|
|
568
|
+
return result;
|
|
569
|
+
}
|
|
570
|
+
incomplete ??= result;
|
|
571
|
+
}
|
|
572
|
+
return incomplete ?? { matches: true };
|
|
573
|
+
}
|
|
574
|
+
function extensionIdentity(field, value) {
|
|
575
|
+
if (Array.isArray(value)) {
|
|
576
|
+
return canonicalHash(
|
|
577
|
+
value.map((item) => {
|
|
578
|
+
if (field === "taxes") {
|
|
579
|
+
const tax = item;
|
|
580
|
+
return {
|
|
581
|
+
id: tax.id,
|
|
582
|
+
base: String(
|
|
583
|
+
normalizeArcaAmountToMinorUnits(tax.baseAmount, "base")
|
|
584
|
+
),
|
|
585
|
+
amount: String(
|
|
586
|
+
normalizeArcaAmountToMinorUnits(tax.amount, "amount")
|
|
587
|
+
),
|
|
588
|
+
rate: Number(tax.rate)
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
return item;
|
|
592
|
+
}).sort((a, b) => canonicalHash(a).localeCompare(canonicalHash(b)))
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
if (field === "associatedPeriod" && value) {
|
|
596
|
+
const period = value;
|
|
597
|
+
return canonicalHash({
|
|
598
|
+
start: normalizeWsfeDateInput(period.startDate, "start"),
|
|
599
|
+
end: normalizeWsfeDateInput(period.endDate, "end")
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
return canonicalHash(value ?? null);
|
|
603
|
+
}
|
|
604
|
+
function compareExtensions(sent, found) {
|
|
605
|
+
let missing;
|
|
606
|
+
for (const field of [
|
|
607
|
+
"taxes",
|
|
608
|
+
"optionalFields",
|
|
609
|
+
"buyers",
|
|
610
|
+
"activities",
|
|
611
|
+
"associatedPeriod",
|
|
612
|
+
"sameCurrencyForeignCancellation"
|
|
613
|
+
]) {
|
|
614
|
+
const expected = sent[field];
|
|
615
|
+
const actual = found[field];
|
|
616
|
+
const empty = (value) => value === void 0 || Array.isArray(value) && value.length === 0;
|
|
617
|
+
if (empty(expected) && empty(actual)) {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (actual === void 0) {
|
|
621
|
+
missing ??= field;
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
try {
|
|
625
|
+
if (extensionIdentity(field, expected) !== extensionIdentity(field, actual)) {
|
|
626
|
+
return {
|
|
627
|
+
matches: false,
|
|
628
|
+
evidence: "conflict",
|
|
629
|
+
reason: `${field} differs from the sent input`
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
} catch {
|
|
633
|
+
missing ??= field;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return missing ? { matches: false, evidence: "incomplete", reason: missing } : { matches: true };
|
|
637
|
+
}
|
|
638
|
+
function compareAssociationMetadata(association, match) {
|
|
639
|
+
for (const field of ["taxId", "voucherDate"]) {
|
|
640
|
+
if (association[field] === void 0) {
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
if (match[field] === void 0) {
|
|
644
|
+
return {
|
|
645
|
+
matches: false,
|
|
646
|
+
evidence: "incomplete",
|
|
647
|
+
reason: `associatedVouchers.${field}`
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
const normalize = (value) => field === "taxId" ? String(BigInt(value)) : normalizeWsfeDateInput(
|
|
651
|
+
value,
|
|
652
|
+
"association.date"
|
|
653
|
+
);
|
|
654
|
+
try {
|
|
655
|
+
if (normalize(association[field]) !== normalize(match[field])) {
|
|
656
|
+
return {
|
|
657
|
+
matches: false,
|
|
658
|
+
evidence: "conflict",
|
|
659
|
+
reason: `associatedVouchers.${field} differs`
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
} catch {
|
|
663
|
+
return {
|
|
664
|
+
matches: false,
|
|
665
|
+
evidence: "incomplete",
|
|
666
|
+
reason: `associatedVouchers.${field}`
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return { matches: true };
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// src/wsaa/session-store.ts
|
|
674
|
+
var CREDENTIAL_EXPIRY_SAFETY_MARGIN_MS = 6e4;
|
|
675
|
+
function createMemoryWsaaSessionStore() {
|
|
676
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
677
|
+
const locks = /* @__PURE__ */ new Map();
|
|
678
|
+
return {
|
|
679
|
+
get(key) {
|
|
680
|
+
const credentials = sessions.get(serializeWsaaSessionKey(key));
|
|
681
|
+
if (!(credentials && isWsaaCredentialValid(credentials))) {
|
|
682
|
+
return Promise.resolve(null);
|
|
683
|
+
}
|
|
684
|
+
return Promise.resolve({ ...credentials });
|
|
685
|
+
},
|
|
686
|
+
set(key, credentials) {
|
|
687
|
+
sessions.set(serializeWsaaSessionKey(key), { ...credentials });
|
|
688
|
+
return Promise.resolve();
|
|
689
|
+
},
|
|
690
|
+
delete(key) {
|
|
691
|
+
sessions.delete(serializeWsaaSessionKey(key));
|
|
692
|
+
return Promise.resolve();
|
|
693
|
+
},
|
|
694
|
+
async withLock(key, fn) {
|
|
695
|
+
const lockKey = serializeWsaaSessionKey(key);
|
|
696
|
+
const previous = locks.get(lockKey) ?? Promise.resolve();
|
|
697
|
+
let release = () => void 0;
|
|
698
|
+
const current = new Promise((resolve) => {
|
|
699
|
+
release = resolve;
|
|
700
|
+
});
|
|
701
|
+
const queued = previous.catch(() => void 0).then(() => current);
|
|
702
|
+
locks.set(lockKey, queued);
|
|
703
|
+
await previous.catch(() => void 0);
|
|
704
|
+
try {
|
|
705
|
+
return await fn();
|
|
706
|
+
} finally {
|
|
707
|
+
release();
|
|
708
|
+
if (locks.get(lockKey) === queued) {
|
|
709
|
+
locks.delete(lockKey);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
function serializeWsaaSessionKey(key) {
|
|
716
|
+
return [key.environment, key.service, key.certificateFingerprint].join(":");
|
|
717
|
+
}
|
|
718
|
+
function isWsaaCredentialValid(credentials) {
|
|
719
|
+
return new Date(credentials.expiresAt).getTime() - Date.now() > CREDENTIAL_EXPIRY_SAFETY_MARGIN_MS;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/wsaa/index.ts
|
|
723
|
+
import { createHash as createHash2 } from "crypto";
|
|
724
|
+
import forge from "node-forge";
|
|
725
|
+
|
|
726
|
+
// src/internal/http.ts
|
|
727
|
+
import https from "https";
|
|
728
|
+
var defaultAgent = new https.Agent({
|
|
729
|
+
keepAlive: true
|
|
730
|
+
});
|
|
731
|
+
var legacyTlsAgent = new https.Agent({
|
|
732
|
+
keepAlive: true,
|
|
733
|
+
ciphers: "DEFAULT@SECLEVEL=0"
|
|
734
|
+
});
|
|
735
|
+
async function postXmlWithMetadata({
|
|
736
|
+
url,
|
|
737
|
+
body,
|
|
738
|
+
contentType,
|
|
739
|
+
soapAction,
|
|
740
|
+
useLegacyTlsSecurityLevel0 = false,
|
|
741
|
+
timeout = 3e4,
|
|
742
|
+
retries = 0,
|
|
743
|
+
retryDelay = 500,
|
|
744
|
+
logger,
|
|
745
|
+
service,
|
|
746
|
+
operation
|
|
747
|
+
}) {
|
|
748
|
+
const totalAttempts = retries + 1;
|
|
749
|
+
for (let attempt = 1; attempt <= totalAttempts; attempt += 1) {
|
|
750
|
+
try {
|
|
751
|
+
return await postXmlOnce({
|
|
752
|
+
url,
|
|
753
|
+
body,
|
|
754
|
+
contentType,
|
|
755
|
+
soapAction,
|
|
756
|
+
useLegacyTlsSecurityLevel0,
|
|
757
|
+
timeout
|
|
758
|
+
});
|
|
759
|
+
} catch (error) {
|
|
760
|
+
if (!(error instanceof ArcaTransportError)) {
|
|
761
|
+
throw error;
|
|
762
|
+
}
|
|
763
|
+
if (attempt >= totalAttempts) {
|
|
764
|
+
logger?.error("ARCA transport request failed", {
|
|
765
|
+
service,
|
|
766
|
+
operation,
|
|
767
|
+
url,
|
|
768
|
+
attempt,
|
|
769
|
+
attempts: totalAttempts,
|
|
770
|
+
...createSafeErrorDiagnostic(error)
|
|
771
|
+
});
|
|
772
|
+
throw error;
|
|
773
|
+
}
|
|
774
|
+
const nextAttempt = attempt + 1;
|
|
775
|
+
logger?.warn(
|
|
776
|
+
`Retrying ARCA request after transport failure (attempt ${nextAttempt}/${totalAttempts})`,
|
|
777
|
+
{
|
|
778
|
+
service,
|
|
779
|
+
operation,
|
|
780
|
+
url,
|
|
781
|
+
attempt: nextAttempt,
|
|
782
|
+
attempts: totalAttempts,
|
|
783
|
+
...createSafeErrorDiagnostic(error)
|
|
784
|
+
}
|
|
785
|
+
);
|
|
786
|
+
await delay(retryDelay);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
throw new ArcaTransportError("ARCA HTTP request exhausted retries");
|
|
790
|
+
}
|
|
791
|
+
async function postXmlOnce({
|
|
792
|
+
url,
|
|
793
|
+
body,
|
|
794
|
+
contentType,
|
|
795
|
+
soapAction,
|
|
796
|
+
useLegacyTlsSecurityLevel0,
|
|
797
|
+
timeout
|
|
798
|
+
}) {
|
|
799
|
+
const endpoint = new URL(url);
|
|
800
|
+
const requestBody = Buffer.from(body, "utf8");
|
|
801
|
+
return await new Promise((resolve, reject) => {
|
|
802
|
+
let settled = false;
|
|
803
|
+
const settleResolve = (response) => {
|
|
804
|
+
if (settled) {
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
settled = true;
|
|
808
|
+
resolve(response);
|
|
809
|
+
};
|
|
810
|
+
const settleReject = (error) => {
|
|
811
|
+
if (settled) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
settled = true;
|
|
815
|
+
reject(error);
|
|
816
|
+
};
|
|
817
|
+
const request = https.request(
|
|
818
|
+
{
|
|
819
|
+
protocol: endpoint.protocol,
|
|
820
|
+
hostname: endpoint.hostname,
|
|
821
|
+
port: endpoint.port || void 0,
|
|
822
|
+
path: `${endpoint.pathname}${endpoint.search}`,
|
|
823
|
+
method: "POST",
|
|
824
|
+
agent: useLegacyTlsSecurityLevel0 ? legacyTlsAgent : defaultAgent,
|
|
825
|
+
headers: {
|
|
826
|
+
Accept: "text/xml, application/soap+xml",
|
|
827
|
+
"Content-Length": requestBody.byteLength,
|
|
828
|
+
"Content-Type": contentType,
|
|
829
|
+
...soapAction === void 0 ? {} : { SOAPAction: `"${soapAction}"` }
|
|
830
|
+
}
|
|
831
|
+
},
|
|
832
|
+
(response) => {
|
|
833
|
+
const chunks = [];
|
|
834
|
+
const getResponseBody = () => Buffer.concat(chunks).toString("utf8");
|
|
835
|
+
response.on("data", (chunk) => {
|
|
836
|
+
chunks.push(
|
|
837
|
+
typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk
|
|
838
|
+
);
|
|
839
|
+
});
|
|
840
|
+
response.on("error", (error) => {
|
|
841
|
+
settleReject(
|
|
842
|
+
new ArcaTransportError("ARCA HTTP response stream failed", {
|
|
843
|
+
cause: error,
|
|
844
|
+
statusCode: response.statusCode,
|
|
845
|
+
...createResponseBodyDiagnostic(getResponseBody())
|
|
846
|
+
})
|
|
847
|
+
);
|
|
848
|
+
});
|
|
849
|
+
response.on("aborted", () => {
|
|
850
|
+
settleReject(
|
|
851
|
+
new ArcaTransportError("ARCA HTTP response was aborted", {
|
|
852
|
+
statusCode: response.statusCode,
|
|
853
|
+
...createResponseBodyDiagnostic(getResponseBody())
|
|
854
|
+
})
|
|
855
|
+
);
|
|
856
|
+
});
|
|
857
|
+
response.on("end", () => {
|
|
858
|
+
const responseBody = getResponseBody();
|
|
859
|
+
const statusCode = response.statusCode ?? 500;
|
|
860
|
+
const responseContentType = Array.isArray(
|
|
861
|
+
response.headers["content-type"]
|
|
862
|
+
) ? response.headers["content-type"].join("; ") : response.headers["content-type"];
|
|
863
|
+
if (statusCode >= 200 && statusCode < 300) {
|
|
864
|
+
settleResolve({
|
|
865
|
+
body: responseBody,
|
|
866
|
+
statusCode,
|
|
867
|
+
contentType: responseContentType
|
|
868
|
+
});
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if (isXmlLikeResponse(responseBody, responseContentType)) {
|
|
872
|
+
settleResolve({
|
|
873
|
+
body: responseBody,
|
|
874
|
+
statusCode,
|
|
875
|
+
contentType: responseContentType
|
|
876
|
+
});
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
settleReject(
|
|
880
|
+
new ArcaTransportError(
|
|
881
|
+
`ARCA HTTP request failed with status ${statusCode}`,
|
|
882
|
+
{
|
|
883
|
+
statusCode,
|
|
884
|
+
contentType: responseContentType,
|
|
885
|
+
...createResponseBodyDiagnostic(responseBody)
|
|
886
|
+
}
|
|
887
|
+
)
|
|
888
|
+
);
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
);
|
|
892
|
+
request.setTimeout(timeout, () => {
|
|
893
|
+
const timeoutCause = new Error(
|
|
894
|
+
`ARCA HTTP request timed out after ${timeout}ms`
|
|
895
|
+
);
|
|
896
|
+
settleReject(
|
|
897
|
+
new ArcaTransportError(
|
|
898
|
+
`ARCA HTTP request timed out after ${timeout}ms`,
|
|
899
|
+
{ cause: timeoutCause }
|
|
900
|
+
)
|
|
901
|
+
);
|
|
902
|
+
request.destroy(timeoutCause);
|
|
903
|
+
});
|
|
904
|
+
request.on("error", (error) => {
|
|
905
|
+
settleReject(
|
|
906
|
+
new ArcaTransportError("ARCA HTTP request failed", {
|
|
907
|
+
cause: error
|
|
908
|
+
})
|
|
909
|
+
);
|
|
910
|
+
});
|
|
911
|
+
request.write(requestBody);
|
|
912
|
+
request.end();
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
function isXmlLikeResponse(body, contentType) {
|
|
916
|
+
const normalizedContentType = contentType?.toLowerCase() ?? "";
|
|
917
|
+
if (normalizedContentType.includes("xml") || normalizedContentType.includes("soap")) {
|
|
918
|
+
return true;
|
|
919
|
+
}
|
|
920
|
+
return body.trimStart().startsWith("<");
|
|
921
|
+
}
|
|
922
|
+
function delay(ms) {
|
|
923
|
+
return new Promise((resolve) => {
|
|
924
|
+
setTimeout(resolve, ms);
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// src/internal/xml.ts
|
|
929
|
+
import { XMLBuilder, XMLParser } from "fast-xml-parser";
|
|
930
|
+
var xmlBuilder = new XMLBuilder({
|
|
931
|
+
attributeNamePrefix: "@_",
|
|
932
|
+
format: false,
|
|
933
|
+
ignoreAttributes: false,
|
|
934
|
+
suppressBooleanAttributes: false,
|
|
935
|
+
suppressEmptyNode: true
|
|
936
|
+
});
|
|
937
|
+
var xmlParser = new XMLParser({
|
|
938
|
+
attributeNamePrefix: "@_",
|
|
939
|
+
ignoreAttributes: false,
|
|
940
|
+
parseAttributeValue: false,
|
|
941
|
+
parseTagValue: false,
|
|
942
|
+
removeNSPrefix: true,
|
|
943
|
+
trimValues: true
|
|
944
|
+
});
|
|
945
|
+
function buildSoapEnvelope(soapVersion, operation, namespace, body, options) {
|
|
946
|
+
const prefix = soapVersion === "1.2" ? "soap12" : "soap";
|
|
947
|
+
const envelopeNamespace = soapVersion === "1.2" ? "http://www.w3.org/2003/05/soap-envelope" : "http://schemas.xmlsoap.org/soap/envelope/";
|
|
948
|
+
const namespaceMode = options?.namespaceMode ?? "default";
|
|
949
|
+
const operationElementName = namespaceMode === "prefix" ? `tns:${operation}` : operation;
|
|
950
|
+
const operationNamespaceAttributes = namespaceMode === "prefix" ? { "@_xmlns:tns": namespace } : { "@_xmlns": namespace };
|
|
951
|
+
const payload = {
|
|
952
|
+
[`${prefix}:Envelope`]: {
|
|
953
|
+
"@_xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
|
|
954
|
+
"@_xmlns:xsd": "http://www.w3.org/2001/XMLSchema",
|
|
955
|
+
[`@_xmlns:${prefix}`]: envelopeNamespace,
|
|
956
|
+
[`${prefix}:Body`]: {
|
|
957
|
+
[operationElementName]: {
|
|
958
|
+
...operationNamespaceAttributes,
|
|
959
|
+
...pruneUndefinedDeep(body)
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
return `<?xml version="1.0" encoding="utf-8"?>${xmlBuilder.build(payload)}`;
|
|
965
|
+
}
|
|
966
|
+
function parseSoapBody(xml, context = {}) {
|
|
967
|
+
let parsed;
|
|
968
|
+
try {
|
|
969
|
+
parsed = xmlParser.parse(xml);
|
|
970
|
+
} catch (error) {
|
|
971
|
+
throw createInvalidSoapResponseError(
|
|
972
|
+
"Invalid SOAP response: XML parse failed",
|
|
973
|
+
context,
|
|
974
|
+
error instanceof Error ? error : void 0
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
const envelope = parsed.Envelope;
|
|
978
|
+
const body = envelope?.Body;
|
|
979
|
+
if (!body) {
|
|
980
|
+
throw createInvalidSoapResponseError(
|
|
981
|
+
"Invalid SOAP response: missing body",
|
|
982
|
+
context
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
const fault = body.Fault;
|
|
986
|
+
if (fault) {
|
|
987
|
+
throw createSoapFaultError(fault);
|
|
988
|
+
}
|
|
989
|
+
return body;
|
|
990
|
+
}
|
|
991
|
+
function getSingleBodyEntry(body, context = {}) {
|
|
992
|
+
const entries = Object.entries(body).filter(([key]) => key !== "@_xmlns");
|
|
993
|
+
if (entries.length !== 1) {
|
|
994
|
+
throw createInvalidSoapResponseError(
|
|
995
|
+
`Invalid SOAP response: expected a single body entry, got ${entries.length}`,
|
|
996
|
+
context
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
return entries[0];
|
|
1000
|
+
}
|
|
1001
|
+
function createInvalidSoapResponseError(message, context, cause) {
|
|
1002
|
+
const responseBody = context.responseBody ?? "";
|
|
1003
|
+
return new ArcaInvalidSoapResponseError(message, {
|
|
1004
|
+
cause,
|
|
1005
|
+
service: context.service,
|
|
1006
|
+
operation: context.operation,
|
|
1007
|
+
endpointUrl: context.endpointUrl,
|
|
1008
|
+
statusCode: context.statusCode,
|
|
1009
|
+
contentType: context.contentType,
|
|
1010
|
+
...createResponseBodyDiagnostic(
|
|
1011
|
+
responseBody,
|
|
1012
|
+
context.responseBodyPreviewLength
|
|
1013
|
+
)
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
function parseXmlDocument(xml) {
|
|
1017
|
+
return xmlParser.parse(xml);
|
|
1018
|
+
}
|
|
1019
|
+
function pruneUndefinedDeep(value) {
|
|
1020
|
+
if (Array.isArray(value)) {
|
|
1021
|
+
return value.map((item) => pruneUndefinedDeep(item)).filter((item) => item !== void 0);
|
|
1022
|
+
}
|
|
1023
|
+
if (value && typeof value === "object") {
|
|
1024
|
+
const entries = Object.entries(value).filter(([, nestedValue]) => nestedValue !== void 0).map(([key, nestedValue]) => [key, pruneUndefinedDeep(nestedValue)]);
|
|
1025
|
+
return Object.fromEntries(entries);
|
|
1026
|
+
}
|
|
1027
|
+
return value;
|
|
1028
|
+
}
|
|
1029
|
+
function createSoapFaultError(fault) {
|
|
1030
|
+
const faultCode = typeof fault.faultcode === "string" ? fault.faultcode : getNestedString(fault, ["Code", "Value"]);
|
|
1031
|
+
const message = typeof fault.faultstring === "string" ? fault.faultstring : getNestedString(fault, ["Reason", "Text"]) ?? "ARCA SOAP fault response";
|
|
1032
|
+
return new ArcaSoapFaultError(message, {
|
|
1033
|
+
faultCode: faultCode ?? void 0
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
function getNestedString(value, path) {
|
|
1037
|
+
let current = value;
|
|
1038
|
+
for (const key of path) {
|
|
1039
|
+
if (!current || typeof current !== "object") {
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
current = current[key];
|
|
1043
|
+
}
|
|
1044
|
+
return typeof current === "string" ? current : null;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// src/wsaa/index.ts
|
|
1048
|
+
function createWsaaAuthModule(options) {
|
|
1049
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1050
|
+
const ordinaryInFlight = /* @__PURE__ */ new Map();
|
|
1051
|
+
const forcedInFlight = /* @__PURE__ */ new Map();
|
|
1052
|
+
function trackLogin(target, cacheKey, login) {
|
|
1053
|
+
const promise = login();
|
|
1054
|
+
target.set(cacheKey, promise);
|
|
1055
|
+
const cleanup = () => {
|
|
1056
|
+
if (target.get(cacheKey) === promise) {
|
|
1057
|
+
target.delete(cacheKey);
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
promise.then(cleanup, cleanup);
|
|
1061
|
+
return promise;
|
|
1062
|
+
}
|
|
1063
|
+
async function requestOrReuseWsaaCredentials(service, sessionKey, cacheKey, forceRefresh) {
|
|
1064
|
+
const reuse = await getReusableCredentials({
|
|
1065
|
+
config: options.config,
|
|
1066
|
+
cache,
|
|
1067
|
+
cacheKey,
|
|
1068
|
+
sessionKey,
|
|
1069
|
+
logger: options.logger,
|
|
1070
|
+
service,
|
|
1071
|
+
allowStore: !forceRefresh,
|
|
1072
|
+
allowCache: !forceRefresh
|
|
1073
|
+
});
|
|
1074
|
+
if (reuse) {
|
|
1075
|
+
return reuse;
|
|
1076
|
+
}
|
|
1077
|
+
const refresh = () => refreshWsaaCredentials({
|
|
1078
|
+
config: options.config,
|
|
1079
|
+
cache,
|
|
1080
|
+
cacheKey,
|
|
1081
|
+
sessionKey,
|
|
1082
|
+
logger: options.logger,
|
|
1083
|
+
service,
|
|
1084
|
+
forceRefresh
|
|
1085
|
+
});
|
|
1086
|
+
if (options.config.wsaaSessionStore?.withLock) {
|
|
1087
|
+
return await withWsaaSessionStoreLock(
|
|
1088
|
+
options.config,
|
|
1089
|
+
sessionKey,
|
|
1090
|
+
service,
|
|
1091
|
+
refresh
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
return await refresh();
|
|
1095
|
+
}
|
|
1096
|
+
async function performLogin(service, sessionKey, cacheKey, forceRefresh) {
|
|
1097
|
+
try {
|
|
1098
|
+
return await requestOrReuseWsaaCredentials(
|
|
1099
|
+
service,
|
|
1100
|
+
sessionKey,
|
|
1101
|
+
cacheKey,
|
|
1102
|
+
forceRefresh
|
|
1103
|
+
);
|
|
1104
|
+
} catch (error) {
|
|
1105
|
+
if (error instanceof ArcaSoapFaultError && error.faultCode === "ns1:coe.alreadyAuthenticated") {
|
|
1106
|
+
const recovered = await getReusableCredentials({
|
|
1107
|
+
config: options.config,
|
|
1108
|
+
cache,
|
|
1109
|
+
cacheKey,
|
|
1110
|
+
sessionKey,
|
|
1111
|
+
logger: options.logger,
|
|
1112
|
+
service,
|
|
1113
|
+
allowStore: true,
|
|
1114
|
+
allowCache: true
|
|
1115
|
+
});
|
|
1116
|
+
if (recovered) {
|
|
1117
|
+
options.logger?.warn(
|
|
1118
|
+
"Recovered WSAA coe.alreadyAuthenticated fault",
|
|
1119
|
+
{
|
|
1120
|
+
service,
|
|
1121
|
+
faultCode: error.faultCode
|
|
1122
|
+
}
|
|
1123
|
+
);
|
|
1124
|
+
return recovered;
|
|
1125
|
+
}
|
|
1126
|
+
if (!options.config.wsaaSessionStore) {
|
|
1127
|
+
throw new ArcaConfigurationError(
|
|
1128
|
+
"WSAA login failed because another process likely owns a valid TA. Configure a durable wsaaSessionStore for multi-process or serverless deployments.",
|
|
1129
|
+
{ cause: error }
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
if (error instanceof ArcaSoapFaultError) {
|
|
1134
|
+
options.logger?.error("WSAA SOAP fault response", {
|
|
1135
|
+
service,
|
|
1136
|
+
operation: "loginCms",
|
|
1137
|
+
url: ARCA_WSAA_CONFIG.endpoint[options.config.environment],
|
|
1138
|
+
...createSafeErrorDiagnostic(error)
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
throw error;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return {
|
|
1145
|
+
login(service, authOptions = {}) {
|
|
1146
|
+
const sessionKey = buildWsaaSessionKey(options.config, service);
|
|
1147
|
+
const cacheKey = serializeWsaaSessionKey(sessionKey);
|
|
1148
|
+
if (authOptions.forceRefresh) {
|
|
1149
|
+
const runningForced2 = forcedInFlight.get(cacheKey);
|
|
1150
|
+
if (runningForced2) {
|
|
1151
|
+
return runningForced2;
|
|
1152
|
+
}
|
|
1153
|
+
const runningOrdinary2 = ordinaryInFlight.get(cacheKey);
|
|
1154
|
+
return trackLogin(forcedInFlight, cacheKey, async () => {
|
|
1155
|
+
await runningOrdinary2?.catch(() => void 0);
|
|
1156
|
+
return await performLogin(service, sessionKey, cacheKey, true);
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
const runningOrdinary = ordinaryInFlight.get(cacheKey);
|
|
1160
|
+
if (runningOrdinary) {
|
|
1161
|
+
return runningOrdinary;
|
|
1162
|
+
}
|
|
1163
|
+
const runningForced = forcedInFlight.get(cacheKey);
|
|
1164
|
+
if (runningForced) {
|
|
1165
|
+
return runningForced;
|
|
1166
|
+
}
|
|
1167
|
+
return trackLogin(
|
|
1168
|
+
ordinaryInFlight,
|
|
1169
|
+
cacheKey,
|
|
1170
|
+
() => performLogin(service, sessionKey, cacheKey, false)
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
async function requestCredentials(config, service, options) {
|
|
1176
|
+
const loginTicketRequestXml = buildLoginTicketRequest(service);
|
|
1177
|
+
const signedCms = signLoginTicketRequest(loginTicketRequestXml, {
|
|
1178
|
+
certificatePem: config.certificatePem,
|
|
1179
|
+
privateKeyPem: config.privateKeyPem
|
|
1180
|
+
});
|
|
1181
|
+
const requestXml = buildSoapEnvelope(
|
|
1182
|
+
ARCA_WSAA_CONFIG.soapVersion,
|
|
1183
|
+
"loginCms",
|
|
1184
|
+
ARCA_WSAA_CONFIG.namespace,
|
|
1185
|
+
{ in0: signedCms }
|
|
1186
|
+
);
|
|
1187
|
+
const url = ARCA_WSAA_CONFIG.endpoint[config.environment];
|
|
1188
|
+
const response = await postXmlWithMetadata({
|
|
1189
|
+
url: ARCA_WSAA_CONFIG.endpoint[config.environment],
|
|
1190
|
+
body: requestXml,
|
|
1191
|
+
contentType: 'text/xml; charset="utf-8"',
|
|
1192
|
+
soapAction: ARCA_WSAA_CONFIG.soapActionBase,
|
|
1193
|
+
timeout: config.timeout,
|
|
1194
|
+
retries: config.retries,
|
|
1195
|
+
retryDelay: config.retryDelay,
|
|
1196
|
+
logger: options?.logger,
|
|
1197
|
+
service: "wsaa",
|
|
1198
|
+
operation: "loginCms"
|
|
1199
|
+
});
|
|
1200
|
+
const parseContext = {
|
|
1201
|
+
service: "wsaa",
|
|
1202
|
+
operation: "loginCms",
|
|
1203
|
+
endpointUrl: url,
|
|
1204
|
+
statusCode: response.statusCode,
|
|
1205
|
+
contentType: response.contentType,
|
|
1206
|
+
responseBody: response.body
|
|
1207
|
+
};
|
|
1208
|
+
const soapBody = parseSoapBody(response.body, parseContext);
|
|
1209
|
+
const [, responseBody] = getSingleBodyEntry(
|
|
1210
|
+
soapBody,
|
|
1211
|
+
parseContext
|
|
1212
|
+
);
|
|
1213
|
+
const loginCmsReturn = responseBody.loginCmsReturn;
|
|
1214
|
+
if (typeof loginCmsReturn !== "string" || loginCmsReturn.trim().length < 1) {
|
|
1215
|
+
throw new ArcaTransportError(
|
|
1216
|
+
"WSAA response did not include loginCmsReturn XML"
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
return parseLoginTicketResponse(loginCmsReturn);
|
|
1220
|
+
}
|
|
1221
|
+
function buildWsaaSessionKey(config, service) {
|
|
1222
|
+
return {
|
|
1223
|
+
environment: config.environment,
|
|
1224
|
+
service,
|
|
1225
|
+
certificateFingerprint: getCertificateFingerprint(config)
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
function getCertificateFingerprint(config) {
|
|
1229
|
+
return createHash2("sha256").update(config.certificatePem).digest("hex");
|
|
1230
|
+
}
|
|
1231
|
+
function getCachedCredentials(cache, cacheKey) {
|
|
1232
|
+
const localCached = cache.get(cacheKey);
|
|
1233
|
+
if (localCached && isWsaaCredentialValid(localCached)) {
|
|
1234
|
+
return localCached;
|
|
1235
|
+
}
|
|
1236
|
+
return null;
|
|
1237
|
+
}
|
|
1238
|
+
async function getReusableCredentials(options) {
|
|
1239
|
+
if (options.allowCache) {
|
|
1240
|
+
const cached = getCachedCredentials(options.cache, options.cacheKey);
|
|
1241
|
+
if (cached) {
|
|
1242
|
+
options.logger?.debug("Attempting WSAA login", {
|
|
1243
|
+
service: options.service,
|
|
1244
|
+
source: "cached"
|
|
1245
|
+
});
|
|
1246
|
+
return cached;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
if (!(options.allowStore && options.config.wsaaSessionStore)) {
|
|
1250
|
+
return null;
|
|
1251
|
+
}
|
|
1252
|
+
const stored = await getStoredCredentials(
|
|
1253
|
+
options.config,
|
|
1254
|
+
options.sessionKey,
|
|
1255
|
+
options.service
|
|
1256
|
+
);
|
|
1257
|
+
if (!stored) {
|
|
1258
|
+
return null;
|
|
1259
|
+
}
|
|
1260
|
+
options.cache.set(options.cacheKey, stored);
|
|
1261
|
+
options.logger?.debug("Attempting WSAA login", {
|
|
1262
|
+
service: options.service,
|
|
1263
|
+
source: "store"
|
|
1264
|
+
});
|
|
1265
|
+
return stored;
|
|
1266
|
+
}
|
|
1267
|
+
async function refreshWsaaCredentials(options) {
|
|
1268
|
+
if (!options.forceRefresh) {
|
|
1269
|
+
const reuse = await getReusableCredentials({
|
|
1270
|
+
config: options.config,
|
|
1271
|
+
cache: options.cache,
|
|
1272
|
+
cacheKey: options.cacheKey,
|
|
1273
|
+
sessionKey: options.sessionKey,
|
|
1274
|
+
logger: options.logger,
|
|
1275
|
+
service: options.service,
|
|
1276
|
+
allowStore: true,
|
|
1277
|
+
allowCache: true
|
|
1278
|
+
});
|
|
1279
|
+
if (reuse) {
|
|
1280
|
+
return reuse;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
options.logger?.debug("Attempting WSAA login", {
|
|
1284
|
+
service: options.service,
|
|
1285
|
+
source: "fresh"
|
|
1286
|
+
});
|
|
1287
|
+
const credentials = await requestCredentials(
|
|
1288
|
+
options.config,
|
|
1289
|
+
options.service,
|
|
1290
|
+
{
|
|
1291
|
+
logger: options.logger
|
|
1292
|
+
}
|
|
1293
|
+
);
|
|
1294
|
+
options.logger?.info("WSAA login succeeded", {
|
|
1295
|
+
service: options.service,
|
|
1296
|
+
expiresAt: credentials.expiresAt
|
|
1297
|
+
});
|
|
1298
|
+
options.cache.set(options.cacheKey, credentials);
|
|
1299
|
+
await setStoredCredentials(options.config, options.sessionKey, credentials);
|
|
1300
|
+
return credentials;
|
|
1301
|
+
}
|
|
1302
|
+
async function getStoredCredentials(config, key, service) {
|
|
1303
|
+
if (!config.wsaaSessionStore) {
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
try {
|
|
1307
|
+
const credentials = await config.wsaaSessionStore.get(key);
|
|
1308
|
+
if (!(credentials && isWsaaCredentialValid(credentials))) {
|
|
1309
|
+
return null;
|
|
1310
|
+
}
|
|
1311
|
+
return credentials;
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
throw new ArcaConfigurationError(
|
|
1314
|
+
`WSAA session store get failed for service ${service}`,
|
|
1315
|
+
{ cause: error instanceof Error ? error : void 0 }
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
async function setStoredCredentials(config, key, credentials) {
|
|
1320
|
+
if (!config.wsaaSessionStore) {
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
try {
|
|
1324
|
+
await config.wsaaSessionStore.set(key, credentials);
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
throw new ArcaConfigurationError(
|
|
1327
|
+
`WSAA session store set failed for service ${key.service}`,
|
|
1328
|
+
{ cause: error instanceof Error ? error : void 0 }
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
async function withWsaaSessionStoreLock(config, key, service, fn) {
|
|
1333
|
+
const store = config.wsaaSessionStore;
|
|
1334
|
+
if (!store?.withLock) {
|
|
1335
|
+
return await fn();
|
|
1336
|
+
}
|
|
1337
|
+
let entered = false;
|
|
1338
|
+
try {
|
|
1339
|
+
return await store.withLock(key, async () => {
|
|
1340
|
+
entered = true;
|
|
1341
|
+
return await fn();
|
|
1342
|
+
});
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
if (entered) {
|
|
1345
|
+
throw error;
|
|
1346
|
+
}
|
|
1347
|
+
if (error instanceof ArcaConfigurationError) {
|
|
1348
|
+
throw error;
|
|
1349
|
+
}
|
|
1350
|
+
throw new ArcaConfigurationError(
|
|
1351
|
+
`WSAA session store lock failed for service ${service}`,
|
|
1352
|
+
{ cause: error instanceof Error ? error : void 0 }
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
function buildLoginTicketRequest(service) {
|
|
1357
|
+
const uniqueId = Math.floor(Date.now() / 1e3);
|
|
1358
|
+
const generationTime = new Date(Date.now() - 5 * 6e4).toISOString().replace(".000Z", "Z");
|
|
1359
|
+
const expirationTime = new Date(Date.now() + 5 * 6e4).toISOString().replace(".000Z", "Z");
|
|
1360
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1361
|
+
<loginTicketRequest version="1.0">
|
|
1362
|
+
<header>
|
|
1363
|
+
<uniqueId>${uniqueId}</uniqueId>
|
|
1364
|
+
<generationTime>${generationTime}</generationTime>
|
|
1365
|
+
<expirationTime>${expirationTime}</expirationTime>
|
|
1366
|
+
</header>
|
|
1367
|
+
<service>${service}</service>
|
|
1368
|
+
</loginTicketRequest>`;
|
|
1369
|
+
}
|
|
1370
|
+
function signLoginTicketRequest(loginTicketRequestXml, options) {
|
|
1371
|
+
const certificate = forge.pki.certificateFromPem(options.certificatePem);
|
|
1372
|
+
const privateKey = forge.pki.privateKeyFromPem(options.privateKeyPem);
|
|
1373
|
+
const signedData = forge.pkcs7.createSignedData();
|
|
1374
|
+
signedData.content = forge.util.createBuffer(loginTicketRequestXml, "utf8");
|
|
1375
|
+
signedData.addCertificate(certificate);
|
|
1376
|
+
const authenticatedAttributes = [
|
|
1377
|
+
{
|
|
1378
|
+
type: String(forge.pki.oids.contentType),
|
|
1379
|
+
value: String(forge.pki.oids.data)
|
|
1380
|
+
},
|
|
1381
|
+
{
|
|
1382
|
+
type: String(forge.pki.oids.messageDigest)
|
|
1383
|
+
},
|
|
1384
|
+
{
|
|
1385
|
+
type: String(forge.pki.oids.signingTime),
|
|
1386
|
+
value: /* @__PURE__ */ new Date()
|
|
1387
|
+
}
|
|
1388
|
+
];
|
|
1389
|
+
const signerOptions = {
|
|
1390
|
+
key: privateKey,
|
|
1391
|
+
certificate,
|
|
1392
|
+
digestAlgorithm: String(forge.pki.oids.sha1),
|
|
1393
|
+
authenticatedAttributes
|
|
1394
|
+
};
|
|
1395
|
+
signedData.addSigner(signerOptions);
|
|
1396
|
+
signedData.sign();
|
|
1397
|
+
const der = forge.asn1.toDer(signedData.toAsn1()).getBytes();
|
|
1398
|
+
return Buffer.from(der, "binary").toString("base64");
|
|
1399
|
+
}
|
|
1400
|
+
function parseLoginTicketResponse(xml) {
|
|
1401
|
+
const parsed = parseXmlDocument(xml);
|
|
1402
|
+
const response = parsed.loginTicketResponse ?? parsed;
|
|
1403
|
+
const header = response.header;
|
|
1404
|
+
const credentials = response.credentials;
|
|
1405
|
+
const token = credentials?.token;
|
|
1406
|
+
const sign = credentials?.sign;
|
|
1407
|
+
const expiresAt = header?.expirationTime;
|
|
1408
|
+
if (typeof token !== "string" || typeof sign !== "string" || typeof expiresAt !== "string") {
|
|
1409
|
+
throw new ArcaTransportError(
|
|
1410
|
+
"Invalid WSAA login ticket response structure"
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
return {
|
|
1414
|
+
token,
|
|
1415
|
+
sign,
|
|
1416
|
+
expiresAt
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// src/internal/logger.ts
|
|
1421
|
+
var ARCA_LOG_LEVELS = ["debug", "info", "warn", "error"];
|
|
1422
|
+
function createArcaLogger(config) {
|
|
1423
|
+
const disabled = config?.disabled ?? false;
|
|
1424
|
+
const level = resolveArcaLogLevel(config?.level);
|
|
1425
|
+
const sink = config?.log ?? defaultArcaLog;
|
|
1426
|
+
const log = (messageLevel, message, ...args) => {
|
|
1427
|
+
if (disabled || !shouldLog(level, messageLevel)) {
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
sink(messageLevel, message, ...args);
|
|
1431
|
+
};
|
|
1432
|
+
return {
|
|
1433
|
+
disabled,
|
|
1434
|
+
level,
|
|
1435
|
+
log,
|
|
1436
|
+
debug(message, ...args) {
|
|
1437
|
+
log("debug", message, ...args);
|
|
1438
|
+
},
|
|
1439
|
+
info(message, ...args) {
|
|
1440
|
+
log("info", message, ...args);
|
|
1441
|
+
},
|
|
1442
|
+
warn(message, ...args) {
|
|
1443
|
+
log("warn", message, ...args);
|
|
1444
|
+
},
|
|
1445
|
+
error(message, ...args) {
|
|
1446
|
+
log("error", message, ...args);
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
function resolveArcaLogLevel(level) {
|
|
1451
|
+
if (isArcaLogLevel(level)) {
|
|
1452
|
+
return level;
|
|
1453
|
+
}
|
|
1454
|
+
const envLevel = process.env.ARCA_LOG_LEVEL?.trim().toLowerCase();
|
|
1455
|
+
if (isArcaLogLevel(envLevel)) {
|
|
1456
|
+
return envLevel;
|
|
1457
|
+
}
|
|
1458
|
+
return "warn";
|
|
1459
|
+
}
|
|
1460
|
+
function shouldLog(threshold, messageLevel) {
|
|
1461
|
+
return ARCA_LOG_LEVELS.indexOf(messageLevel) >= ARCA_LOG_LEVELS.indexOf(threshold);
|
|
1462
|
+
}
|
|
1463
|
+
function isArcaLogLevel(value) {
|
|
1464
|
+
return ARCA_LOG_LEVELS.includes(value);
|
|
1465
|
+
}
|
|
1466
|
+
function defaultArcaLog(level, message, ...args) {
|
|
1467
|
+
const method = level === "debug" ? console.debug : level === "info" ? console.info : level === "warn" ? console.warn : console.error;
|
|
1468
|
+
method(message, ...args);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// src/services/issuance-fields.ts
|
|
1472
|
+
var FAMILIES = {
|
|
1473
|
+
ordinary: { A: [1, 2, 3], B: [6, 7, 8], C: [11, 12, 13] },
|
|
1474
|
+
retention_legend: { A: [51, 52, 53] },
|
|
1475
|
+
fce: { A: [201, 202, 203], B: [206, 207, 208], C: [211, 212, 213] }
|
|
1476
|
+
};
|
|
1477
|
+
function voucherFamily(type) {
|
|
1478
|
+
for (const [family, classes] of Object.entries(FAMILIES)) {
|
|
1479
|
+
for (const [voucherClass, types] of Object.entries(classes)) {
|
|
1480
|
+
if (types.includes(type)) {
|
|
1481
|
+
return {
|
|
1482
|
+
family,
|
|
1483
|
+
voucherClass,
|
|
1484
|
+
types
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
throw new ArcaInputError("Unsupported invoice or note type.", {
|
|
1490
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
1491
|
+
field: "voucherType"
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
function invoiceType(family, voucherClass) {
|
|
1495
|
+
const classes = FAMILIES[family];
|
|
1496
|
+
const types = classes && classes[voucherClass];
|
|
1497
|
+
if (!types) {
|
|
1498
|
+
throw new ArcaInputError("Invoice family does not support this class.", {
|
|
1499
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
1500
|
+
field: "family"
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
return types[0];
|
|
1504
|
+
}
|
|
1505
|
+
function minor(value, field) {
|
|
1506
|
+
return arcaMinorUnitsToNumber(assertArcaMinorUnits(value, field), field);
|
|
1507
|
+
}
|
|
1508
|
+
function applyIssuanceFields(data, fields) {
|
|
1509
|
+
if (fields.taxes !== void 0) {
|
|
1510
|
+
if (!Array.isArray(fields.taxes)) {
|
|
1511
|
+
throw new ArcaInputError("taxes must be an array", {
|
|
1512
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
1513
|
+
field: "taxes"
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
data.taxes = fields.taxes.map((tax) => ({
|
|
1517
|
+
id: tax.id,
|
|
1518
|
+
description: tax.description,
|
|
1519
|
+
baseAmount: minor(tax.base, "taxes.base"),
|
|
1520
|
+
rate: tax.rate,
|
|
1521
|
+
amount: minor(tax.amount, "taxes.amount")
|
|
1522
|
+
}));
|
|
1523
|
+
data.taxAmount = minor(tributeTotal(fields.taxes), "taxes.total");
|
|
1524
|
+
}
|
|
1525
|
+
if (fields.amounts) {
|
|
1526
|
+
Object.assign(data, reviewedHeaderAmounts(fields.amounts));
|
|
1527
|
+
}
|
|
1528
|
+
if (fields.concept) {
|
|
1529
|
+
data.concept = { products: 1, services: 2, products_and_services: 3 }[fields.concept];
|
|
1530
|
+
}
|
|
1531
|
+
if (fields.dueDate) {
|
|
1532
|
+
data.paymentDueDate = fields.dueDate;
|
|
1533
|
+
}
|
|
1534
|
+
if (fields.paidInForeignCurrency !== void 0) {
|
|
1535
|
+
if (typeof fields.paidInForeignCurrency !== "boolean") {
|
|
1536
|
+
throw new ArcaInputError("paidInForeignCurrency must be boolean", {
|
|
1537
|
+
code: "ARCA_INPUT_INVALID_VALUE"
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
data.sameCurrencyForeignCancellation = fields.paidInForeignCurrency ? "S" : "N";
|
|
1541
|
+
}
|
|
1542
|
+
for (const key of ["optionalFields", "buyers", "activities"]) {
|
|
1543
|
+
if (fields[key] !== void 0) {
|
|
1544
|
+
Object.assign(data, { [key]: structuredClone(fields[key]) });
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
applyFceFields(data, fields.fce);
|
|
1548
|
+
}
|
|
1549
|
+
function tributeTotal(taxes) {
|
|
1550
|
+
return taxes.reduce((sum, tax) => sum + tax.amount, 0);
|
|
1551
|
+
}
|
|
1552
|
+
function reviewedHeaderAmounts(amounts) {
|
|
1553
|
+
return {
|
|
1554
|
+
netAmount: minor(amounts.net, "amounts.net"),
|
|
1555
|
+
vatAmount: minor(amounts.vat, "amounts.vat"),
|
|
1556
|
+
exemptAmount: minor(amounts.exempt ?? 0, "amounts.exempt"),
|
|
1557
|
+
nonTaxableAmount: minor(amounts.untaxed ?? 0, "amounts.untaxed"),
|
|
1558
|
+
vatRates: amounts.vatRates?.map((rate) => ({
|
|
1559
|
+
id: rate.id,
|
|
1560
|
+
baseAmount: minor(rate.base, "amounts.vatRates.base"),
|
|
1561
|
+
amount: minor(rate.amount, "amounts.vatRates.amount")
|
|
1562
|
+
}))
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
var ISSUANCE_KEYS = [
|
|
1566
|
+
"fce",
|
|
1567
|
+
"taxes",
|
|
1568
|
+
"amounts",
|
|
1569
|
+
"concept",
|
|
1570
|
+
"dueDate",
|
|
1571
|
+
"paidInForeignCurrency",
|
|
1572
|
+
"optionalFields",
|
|
1573
|
+
"buyers",
|
|
1574
|
+
"activities"
|
|
1575
|
+
];
|
|
1576
|
+
function validateIssuanceFields(fields) {
|
|
1577
|
+
validateRows(
|
|
1578
|
+
fields.taxes,
|
|
1579
|
+
"taxes",
|
|
1580
|
+
["id", "description", "base", "rate", "amount"],
|
|
1581
|
+
(row) => {
|
|
1582
|
+
positiveId(row.id, "taxes.id");
|
|
1583
|
+
if (row.description !== void 0 && typeof row.description !== "string") {
|
|
1584
|
+
bad("taxes.description");
|
|
1585
|
+
}
|
|
1586
|
+
minor(row.base, "taxes.base");
|
|
1587
|
+
minor(row.amount, "taxes.amount");
|
|
1588
|
+
if (typeof row.rate !== "number" || !Number.isFinite(row.rate) || row.rate < 0) {
|
|
1589
|
+
bad("taxes.rate");
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
);
|
|
1593
|
+
if (fields.amounts !== void 0) {
|
|
1594
|
+
objectKeys(fields.amounts, "amounts", [
|
|
1595
|
+
"net",
|
|
1596
|
+
"vat",
|
|
1597
|
+
"exempt",
|
|
1598
|
+
"untaxed",
|
|
1599
|
+
"vatRates"
|
|
1600
|
+
]);
|
|
1601
|
+
minor(fields.amounts.net, "amounts.net");
|
|
1602
|
+
minor(fields.amounts.vat, "amounts.vat");
|
|
1603
|
+
validateRows(
|
|
1604
|
+
fields.amounts.vatRates,
|
|
1605
|
+
"amounts.vatRates",
|
|
1606
|
+
["id", "base", "amount"],
|
|
1607
|
+
(row) => {
|
|
1608
|
+
if (![3, 4, 5, 6, 8, 9].includes(row.id)) {
|
|
1609
|
+
bad("amounts.vatRates.id");
|
|
1610
|
+
}
|
|
1611
|
+
minor(row.base, "amounts.vatRates.base");
|
|
1612
|
+
minor(row.amount, "amounts.vatRates.amount");
|
|
1613
|
+
}
|
|
1614
|
+
);
|
|
1615
|
+
const ids = fields.amounts.vatRates?.map((r) => r.id) ?? [];
|
|
1616
|
+
if (new Set(ids).size !== ids.length) {
|
|
1617
|
+
bad("amounts.vatRates");
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
validateRows(
|
|
1621
|
+
fields.optionalFields,
|
|
1622
|
+
"optionalFields",
|
|
1623
|
+
["id", "value"],
|
|
1624
|
+
(row) => {
|
|
1625
|
+
if (typeof row.id !== "string" || !/^\d+$/.test(row.id) || typeof row.value !== "string") {
|
|
1626
|
+
bad("optionalFields");
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
);
|
|
1630
|
+
validateRows(
|
|
1631
|
+
fields.buyers,
|
|
1632
|
+
"buyers",
|
|
1633
|
+
["documentType", "documentNumber", "percentage"],
|
|
1634
|
+
(row) => {
|
|
1635
|
+
positiveId(row.documentType, "buyers.documentType");
|
|
1636
|
+
positiveId(row.documentNumber, "buyers.documentNumber");
|
|
1637
|
+
if (typeof row.percentage !== "number" || !Number.isFinite(row.percentage) || row.percentage <= 0 || row.percentage > 100) {
|
|
1638
|
+
bad("buyers.percentage");
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
);
|
|
1642
|
+
validateRows(
|
|
1643
|
+
fields.activities,
|
|
1644
|
+
"activities",
|
|
1645
|
+
["id"],
|
|
1646
|
+
(row) => positiveId(row.id, "activities.id")
|
|
1647
|
+
);
|
|
1648
|
+
if (fields.concept !== void 0 && !["products", "services", "products_and_services"].includes(fields.concept)) {
|
|
1649
|
+
bad("concept");
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
function positiveId(value, field) {
|
|
1653
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
|
1654
|
+
bad(field);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
function bad(field) {
|
|
1658
|
+
throw new ArcaInputError(`Invalid ${field}`, {
|
|
1659
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
1660
|
+
field
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
function objectKeys(value, field, keys) {
|
|
1664
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !keys.includes(key))) {
|
|
1665
|
+
bad(field);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
function validateRows(value, field, keys, check) {
|
|
1669
|
+
if (value === void 0) {
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
if (!Array.isArray(value)) {
|
|
1673
|
+
bad(field);
|
|
1674
|
+
}
|
|
1675
|
+
for (const row of value) {
|
|
1676
|
+
objectKeys(row, field, keys);
|
|
1677
|
+
check(row);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
function validateFiscalHeader(data) {
|
|
1681
|
+
const family = voucherFamily(data.voucherType);
|
|
1682
|
+
validateFceHeader(data);
|
|
1683
|
+
if (family.voucherClass === "C" && (data.vatAmount !== 0 || data.vatRates?.length || data.exemptAmount !== 0 || data.nonTaxableAmount !== 0)) {
|
|
1684
|
+
bad("amounts");
|
|
1685
|
+
}
|
|
1686
|
+
if (![1, 2, 3].includes(data.concept)) {
|
|
1687
|
+
bad("concept");
|
|
1688
|
+
}
|
|
1689
|
+
const date = normalizeWsfeDateInput(data.voucherDate, "date");
|
|
1690
|
+
if (data.concept === 1 && (data.serviceStartDate || data.serviceEndDate)) {
|
|
1691
|
+
bad("service");
|
|
1692
|
+
}
|
|
1693
|
+
if (data.concept === 2 || data.concept === 3) {
|
|
1694
|
+
if (!(data.serviceStartDate && data.serviceEndDate && data.paymentDueDate)) {
|
|
1695
|
+
bad("service");
|
|
1696
|
+
}
|
|
1697
|
+
const start = normalizeWsfeDateInput(data.serviceStartDate, "service.from");
|
|
1698
|
+
const end = normalizeWsfeDateInput(data.serviceEndDate, "service.to");
|
|
1699
|
+
if (start > end) {
|
|
1700
|
+
bad("service.to");
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
if (data.paymentDueDate && normalizeWsfeDateInput(data.paymentDueDate, "dueDate") < date) {
|
|
1704
|
+
bad("dueDate");
|
|
1705
|
+
}
|
|
1706
|
+
if (data.currencyId === "PES" && data.sameCurrencyForeignCancellation !== void 0) {
|
|
1707
|
+
bad("paidInForeignCurrency");
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
function applyFceFields(data, fce) {
|
|
1711
|
+
if (fce === void 0) {
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
objectKeys(fce, "fce", [
|
|
1715
|
+
"cbu",
|
|
1716
|
+
"alias",
|
|
1717
|
+
"transfer",
|
|
1718
|
+
"annulment",
|
|
1719
|
+
"reference"
|
|
1720
|
+
]);
|
|
1721
|
+
if (voucherFamily(data.voucherType).family !== "fce") {
|
|
1722
|
+
bad("fce");
|
|
1723
|
+
}
|
|
1724
|
+
validateFceOptions(fce);
|
|
1725
|
+
const extra = [
|
|
1726
|
+
...fce.cbu === void 0 ? [] : [{ id: "2101", value: fce.cbu }],
|
|
1727
|
+
...fce.alias === void 0 ? [] : [{ id: "2102", value: fce.alias }],
|
|
1728
|
+
...fce.transfer === void 0 ? [] : [{ id: "27", value: fce.transfer }],
|
|
1729
|
+
...fce.annulment === void 0 ? [] : [{ id: "22", value: fce.annulment ? "S" : "N" }],
|
|
1730
|
+
...fce.reference === void 0 ? [] : [{ id: "23", value: fce.reference }]
|
|
1731
|
+
];
|
|
1732
|
+
const options = [...data.optionalFields ?? [], ...extra];
|
|
1733
|
+
if (new Set(options.map((o) => o.id)).size !== options.length) {
|
|
1734
|
+
bad("fce");
|
|
1735
|
+
}
|
|
1736
|
+
data.optionalFields = options;
|
|
1737
|
+
}
|
|
1738
|
+
function validateFceHeader(data) {
|
|
1739
|
+
const family = voucherFamily(data.voucherType);
|
|
1740
|
+
if (family.family === "fce") {
|
|
1741
|
+
const options = new Map(
|
|
1742
|
+
(data.optionalFields ?? []).map((o) => [o.id, o.value])
|
|
1743
|
+
);
|
|
1744
|
+
if (family.types[0] === data.voucherType) {
|
|
1745
|
+
if (!/^\d{22}$/.test(options.get("2101") ?? "") || options.has("22")) {
|
|
1746
|
+
bad("fce.cbu");
|
|
1747
|
+
}
|
|
1748
|
+
if (!data.paymentDueDate) {
|
|
1749
|
+
bad("dueDate");
|
|
1750
|
+
}
|
|
1751
|
+
} else if (!["S", "N"].includes(options.get("22") ?? "") || options.has("2101") || options.has("2102") || options.has("27")) {
|
|
1752
|
+
bad("fce.annulment");
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
function validateFceOptions(fce) {
|
|
1757
|
+
if (fce.cbu !== void 0 && (typeof fce.cbu !== "string" || !/^\d{22}$/.test(fce.cbu))) {
|
|
1758
|
+
bad("fce.cbu");
|
|
1759
|
+
}
|
|
1760
|
+
if (fce.alias !== void 0 && (typeof fce.alias !== "string" || !/^[A-Za-z0-9.-]{6,20}$/.test(fce.alias))) {
|
|
1761
|
+
bad("fce.alias");
|
|
1762
|
+
}
|
|
1763
|
+
if (fce.transfer !== void 0 && !["ADC", "SCA"].includes(fce.transfer)) {
|
|
1764
|
+
bad("fce.transfer");
|
|
1765
|
+
}
|
|
1766
|
+
if (fce.annulment !== void 0 && typeof fce.annulment !== "boolean") {
|
|
1767
|
+
bad("fce.annulment");
|
|
1768
|
+
}
|
|
1769
|
+
if (fce.reference !== void 0 && (typeof fce.reference !== "string" || !fce.reference.trim())) {
|
|
1770
|
+
bad("fce.reference");
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// src/services/issuance-wsmtxca.ts
|
|
1775
|
+
var iso = (value) => {
|
|
1776
|
+
if (value === void 0) {
|
|
1777
|
+
return void 0;
|
|
1778
|
+
}
|
|
1779
|
+
const date = normalizeWsfeDateInput(
|
|
1780
|
+
value,
|
|
1781
|
+
"date"
|
|
1782
|
+
);
|
|
1783
|
+
return `${date.slice(0, 4)}-${date.slice(4, 6)}-${date.slice(6, 8)}`;
|
|
1784
|
+
};
|
|
1785
|
+
function wsmtxcaRequest(data, number) {
|
|
1786
|
+
if (!data.details?.length) {
|
|
1787
|
+
invalid("details", "WSMTXCA requires detailed items");
|
|
1788
|
+
}
|
|
1789
|
+
const items = data.details.map((item, index) => {
|
|
1790
|
+
if (!item || typeof item !== "object" || Object.keys(item).some(
|
|
1791
|
+
(k) => ![
|
|
1792
|
+
"description",
|
|
1793
|
+
"quantity",
|
|
1794
|
+
"unit",
|
|
1795
|
+
"unitPrice",
|
|
1796
|
+
"discount",
|
|
1797
|
+
"vatCondition",
|
|
1798
|
+
"vatAmount",
|
|
1799
|
+
"amount",
|
|
1800
|
+
"code",
|
|
1801
|
+
"matrixCode",
|
|
1802
|
+
"matrixUnits"
|
|
1803
|
+
].includes(k)
|
|
1804
|
+
)) {
|
|
1805
|
+
invalid(`details[${index}]`, "Invalid detailed item fields");
|
|
1806
|
+
}
|
|
1807
|
+
if (typeof item.description !== "string" || !(item.description.trim() && Number.isFinite(item.quantity)) || item.quantity <= 0 || !Number.isInteger(item.unit) || item.unit < 0 || typeof item.unitPrice !== "string" || !/^\d+(\.\d{1,6})?$/.test(item.unitPrice) || !Number.isInteger(item.vatCondition)) {
|
|
1808
|
+
invalid(`details[${index}]`, "Invalid detailed item");
|
|
1809
|
+
}
|
|
1810
|
+
return {
|
|
1811
|
+
unidadesMtx: item.matrixUnits,
|
|
1812
|
+
codigoMtx: item.matrixCode,
|
|
1813
|
+
codigo: item.code,
|
|
1814
|
+
descripcion: item.description,
|
|
1815
|
+
cantidad: item.quantity,
|
|
1816
|
+
codigoUnidadMedida: item.unit,
|
|
1817
|
+
precioUnitario: item.unitPrice,
|
|
1818
|
+
importeBonificacion: minor(item.discount ?? 0, "details.discount"),
|
|
1819
|
+
codigoCondicionIVA: item.vatCondition,
|
|
1820
|
+
...item.vatAmount === void 0 ? {} : { importeIVA: minor(item.vatAmount, "details.vatAmount") },
|
|
1821
|
+
importeItem: minor(item.amount, "details.amount")
|
|
1822
|
+
};
|
|
1823
|
+
});
|
|
1824
|
+
const itemTotal = data.details.reduce(
|
|
1825
|
+
(sum, item) => sum + BigInt(item.amount),
|
|
1826
|
+
0n
|
|
1827
|
+
);
|
|
1828
|
+
const expected = normalizeArcaAmountToMinorUnits(data.totalAmount, "total") - normalizeArcaAmountToMinorUnits(data.taxAmount, "taxes");
|
|
1829
|
+
if (!isWithinArcaTolerance(itemTotal, expected, 1)) {
|
|
1830
|
+
invalid(
|
|
1831
|
+
"details",
|
|
1832
|
+
"Item totals must equal the voucher total excluding tributes"
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1835
|
+
return {
|
|
1836
|
+
comprobanteCAERequest: {
|
|
1837
|
+
codigoTipoComprobante: data.voucherType,
|
|
1838
|
+
numeroPuntoVenta: data.salesPoint,
|
|
1839
|
+
...number === void 0 ? {} : { numeroComprobante: number },
|
|
1840
|
+
fechaEmision: iso(data.voucherDate),
|
|
1841
|
+
codigoTipoDocumento: data.documentType,
|
|
1842
|
+
numeroDocumento: data.documentNumber,
|
|
1843
|
+
condicionIVAReceptor: data.receiverVatConditionId,
|
|
1844
|
+
importeGravado: data.netAmount,
|
|
1845
|
+
importeNoGravado: data.nonTaxableAmount,
|
|
1846
|
+
importeExento: data.exemptAmount,
|
|
1847
|
+
importeSubtotal: Number(
|
|
1848
|
+
normalizeArcaAmountToMinorUnits(data.netAmount, "net") + normalizeArcaAmountToMinorUnits(data.nonTaxableAmount, "untaxed") + normalizeArcaAmountToMinorUnits(data.exemptAmount, "exempt")
|
|
1849
|
+
) / 100,
|
|
1850
|
+
importeOtrosTributos: data.taxAmount,
|
|
1851
|
+
importeTotal: data.totalAmount,
|
|
1852
|
+
codigoMoneda: data.currencyId,
|
|
1853
|
+
cotizacionMoneda: data.exchangeRate,
|
|
1854
|
+
codigoConcepto: data.concept,
|
|
1855
|
+
fechaServicioDesde: iso(data.serviceStartDate),
|
|
1856
|
+
fechaServicioHasta: iso(data.serviceEndDate),
|
|
1857
|
+
fechaVencimientoPago: iso(data.paymentDueDate),
|
|
1858
|
+
...data.sameCurrencyForeignCancellation === void 0 ? {} : {
|
|
1859
|
+
cancelaEnMismaMonedaExtranjera: data.sameCurrencyForeignCancellation
|
|
1860
|
+
},
|
|
1861
|
+
arrayComprobantesAsociados: data.associatedVouchers?.length ? {
|
|
1862
|
+
comprobanteAsociado: data.associatedVouchers.map((v) => ({
|
|
1863
|
+
codigoTipoComprobante: v.type,
|
|
1864
|
+
numeroPuntoVenta: v.salesPoint,
|
|
1865
|
+
numeroComprobante: v.number,
|
|
1866
|
+
cuit: v.taxId,
|
|
1867
|
+
fechaEmision: iso(v.voucherDate)
|
|
1868
|
+
}))
|
|
1869
|
+
} : void 0,
|
|
1870
|
+
periodoComprobantesAsociados: data.associatedPeriod ? {
|
|
1871
|
+
fechaDesde: iso(data.associatedPeriod.startDate),
|
|
1872
|
+
fechaHasta: iso(data.associatedPeriod.endDate)
|
|
1873
|
+
} : void 0,
|
|
1874
|
+
arrayCompradores: data.buyers?.length ? {
|
|
1875
|
+
comprador: data.buyers.map((b) => ({
|
|
1876
|
+
codigoTipoDocumento: b.documentType,
|
|
1877
|
+
numeroDocumento: b.documentNumber,
|
|
1878
|
+
porcentaje: b.percentage
|
|
1879
|
+
}))
|
|
1880
|
+
} : void 0,
|
|
1881
|
+
arrayOtrosTributos: data.taxes?.length ? {
|
|
1882
|
+
otroTributo: data.taxes.map((t) => ({
|
|
1883
|
+
codigo: t.id,
|
|
1884
|
+
descripcion: t.description,
|
|
1885
|
+
baseImponible: t.baseAmount,
|
|
1886
|
+
importe: t.amount
|
|
1887
|
+
}))
|
|
1888
|
+
} : void 0,
|
|
1889
|
+
arrayItems: { item: items },
|
|
1890
|
+
arraySubtotalesIVA: data.vatRates?.length ? {
|
|
1891
|
+
subtotalIVA: data.vatRates.map((v) => ({
|
|
1892
|
+
codigo: v.id,
|
|
1893
|
+
importe: v.amount
|
|
1894
|
+
}))
|
|
1895
|
+
} : void 0,
|
|
1896
|
+
arrayDatosAdicionales: wsmtxcaAdditionalData(data.optionalFields),
|
|
1897
|
+
arrayActividades: data.activities?.length ? { actividad: data.activities.map((a) => ({ codigo: a.id })) } : void 0
|
|
1898
|
+
}
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
function invalid(field, message) {
|
|
1902
|
+
throw new ArcaInputError(message, {
|
|
1903
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
1904
|
+
field
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
function rows(value, key) {
|
|
1908
|
+
const data = value && typeof value === "object" ? value[key] : void 0;
|
|
1909
|
+
if (data === void 0) {
|
|
1910
|
+
return void 0;
|
|
1911
|
+
}
|
|
1912
|
+
const list = Array.isArray(data) ? data : [data];
|
|
1913
|
+
if (list.some((item) => !item || typeof item !== "object")) {
|
|
1914
|
+
return void 0;
|
|
1915
|
+
}
|
|
1916
|
+
return list;
|
|
1917
|
+
}
|
|
1918
|
+
function wsmtxcaHeader(found) {
|
|
1919
|
+
const raw = found.raw;
|
|
1920
|
+
const items = rows(raw.arrayItems, "item");
|
|
1921
|
+
const details = items?.map((i) => ({
|
|
1922
|
+
description: String(i.descripcion ?? ""),
|
|
1923
|
+
quantity: Number(i.cantidad),
|
|
1924
|
+
unit: Number(i.codigoUnidadMedida),
|
|
1925
|
+
unitPrice: String(i.precioUnitario),
|
|
1926
|
+
discount: Number(
|
|
1927
|
+
normalizeArcaAmountToMinorUnits(
|
|
1928
|
+
Number(i.importeBonificacion ?? 0),
|
|
1929
|
+
"discount"
|
|
1930
|
+
)
|
|
1931
|
+
),
|
|
1932
|
+
vatCondition: Number(i.codigoCondicionIVA),
|
|
1933
|
+
vatAmount: i.importeIVA === void 0 ? void 0 : Number(normalizeArcaAmountToMinorUnits(Number(i.importeIVA), "vat")),
|
|
1934
|
+
amount: Number(
|
|
1935
|
+
normalizeArcaAmountToMinorUnits(Number(i.importeItem), "amount")
|
|
1936
|
+
),
|
|
1937
|
+
code: i.codigo === void 0 ? void 0 : String(i.codigo),
|
|
1938
|
+
matrixCode: i.codigoMtx === void 0 ? void 0 : String(i.codigoMtx),
|
|
1939
|
+
matrixUnits: i.unidadesMtx === void 0 ? void 0 : Number(i.unidadesMtx)
|
|
1940
|
+
}));
|
|
1941
|
+
return {
|
|
1942
|
+
...found,
|
|
1943
|
+
voucherNumber: found.voucherNumber ?? 0,
|
|
1944
|
+
voucherDate: found.invoiceDate,
|
|
1945
|
+
netAmount: found.taxableAmount,
|
|
1946
|
+
// consultarComprobante is an authorized-voucher lookup. No result flag is returned.
|
|
1947
|
+
result: found.cae ? "A" : void 0,
|
|
1948
|
+
serviceStartDate: raw.fechaServicioDesde === void 0 ? void 0 : String(raw.fechaServicioDesde),
|
|
1949
|
+
serviceEndDate: raw.fechaServicioHasta === void 0 ? void 0 : String(raw.fechaServicioHasta),
|
|
1950
|
+
paymentDueDate: raw.fechaVencimientoPago === void 0 ? void 0 : String(raw.fechaVencimientoPago),
|
|
1951
|
+
details,
|
|
1952
|
+
vatRates: rows(raw.arraySubtotalesIVA, "subtotalIVA")?.map((v) => ({
|
|
1953
|
+
id: Number(v.codigo),
|
|
1954
|
+
amount: Number(v.importe),
|
|
1955
|
+
baseAmount: Number(
|
|
1956
|
+
(details ?? []).filter((i) => i.vatCondition === Number(v.codigo)).reduce((sum, i) => sum + BigInt(i.amount), 0n) - normalizeArcaAmountToMinorUnits(Number(v.importe), "vat")
|
|
1957
|
+
) / 100
|
|
1958
|
+
})),
|
|
1959
|
+
taxes: rows(raw.arrayOtrosTributos, "otroTributo")?.map((t) => ({
|
|
1960
|
+
id: Number(t.codigo),
|
|
1961
|
+
description: t.descripcion === void 0 ? void 0 : String(t.descripcion),
|
|
1962
|
+
baseAmount: Number(t.baseImponible),
|
|
1963
|
+
amount: Number(t.importe),
|
|
1964
|
+
rate: 0
|
|
1965
|
+
})),
|
|
1966
|
+
sameCurrencyForeignCancellation: raw.cancelaEnMismaMonedaExtranjera,
|
|
1967
|
+
optionalFields: rows(raw.arrayDatosAdicionales, "datoAdicional")?.map(
|
|
1968
|
+
(v) => ({ id: String(v.t), value: String(v.c1) })
|
|
1969
|
+
),
|
|
1970
|
+
activities: rows(raw.arrayActividades, "actividad")?.map((v) => ({
|
|
1971
|
+
id: Number(v.codigo)
|
|
1972
|
+
})),
|
|
1973
|
+
buyers: rows(raw.arrayCompradores, "comprador")?.map((v) => ({
|
|
1974
|
+
documentType: Number(v.codigoTipoDocumento),
|
|
1975
|
+
documentNumber: Number(v.numeroDocumento),
|
|
1976
|
+
percentage: Number(v.porcentaje)
|
|
1977
|
+
})),
|
|
1978
|
+
associatedVouchers: rows(
|
|
1979
|
+
raw.arrayComprobantesAsociados,
|
|
1980
|
+
"comprobanteAsociado"
|
|
1981
|
+
)?.map((v) => ({
|
|
1982
|
+
type: Number(v.codigoTipoComprobante),
|
|
1983
|
+
salesPoint: Number(v.numeroPuntoVenta),
|
|
1984
|
+
number: Number(v.numeroComprobante),
|
|
1985
|
+
taxId: v.cuit === void 0 ? void 0 : String(v.cuit),
|
|
1986
|
+
voucherDate: v.fechaEmision
|
|
1987
|
+
}))
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
function matchWsmtxcaDetails(data, number, raw) {
|
|
1991
|
+
const request = wsmtxcaRequest(
|
|
1992
|
+
data,
|
|
1993
|
+
number
|
|
1994
|
+
).comprobanteCAERequest;
|
|
1995
|
+
let missing = false;
|
|
1996
|
+
for (const key of Object.keys(request)) {
|
|
1997
|
+
const expected = request[key];
|
|
1998
|
+
const actual = raw[key];
|
|
1999
|
+
if (expected === void 0) {
|
|
2000
|
+
if (!emptyWire(actual)) {
|
|
2001
|
+
return "conflict";
|
|
2002
|
+
}
|
|
2003
|
+
continue;
|
|
2004
|
+
}
|
|
2005
|
+
const result = compareWire(
|
|
2006
|
+
normalizeWire(expected, key),
|
|
2007
|
+
normalizeWire(actual, key)
|
|
2008
|
+
);
|
|
2009
|
+
if (result === "conflict") {
|
|
2010
|
+
return "conflict";
|
|
2011
|
+
}
|
|
2012
|
+
missing ||= result === "incomplete";
|
|
2013
|
+
}
|
|
2014
|
+
return missing ? "incomplete" : "match";
|
|
2015
|
+
}
|
|
2016
|
+
function emptyWire(value) {
|
|
2017
|
+
return value === void 0 || value === null || typeof value === "object" && Object.values(value).every(emptyWire);
|
|
2018
|
+
}
|
|
2019
|
+
function compareWire(expected, actual) {
|
|
2020
|
+
if (actual === void 0 || actual === null) {
|
|
2021
|
+
return "incomplete";
|
|
2022
|
+
}
|
|
2023
|
+
if (expected !== null && typeof expected === "object") {
|
|
2024
|
+
if (typeof actual !== "object" || Array.isArray(expected) !== Array.isArray(actual)) {
|
|
2025
|
+
return "conflict";
|
|
2026
|
+
}
|
|
2027
|
+
const left = expected;
|
|
2028
|
+
const right = actual;
|
|
2029
|
+
if (Object.keys(right).some((key) => !(key in left || emptyWire(right[key])))) {
|
|
2030
|
+
return "conflict";
|
|
2031
|
+
}
|
|
2032
|
+
let missing = false;
|
|
2033
|
+
for (const key of Object.keys(left)) {
|
|
2034
|
+
const result = compareWire(left[key], right[key]);
|
|
2035
|
+
if (result === "conflict") {
|
|
2036
|
+
return result;
|
|
2037
|
+
}
|
|
2038
|
+
missing ||= result === "incomplete";
|
|
2039
|
+
}
|
|
2040
|
+
return missing ? "incomplete" : "match";
|
|
2041
|
+
}
|
|
2042
|
+
return expected === actual ? "match" : "conflict";
|
|
2043
|
+
}
|
|
2044
|
+
var LIST_KEYS = /* @__PURE__ */ new Set([
|
|
2045
|
+
"item",
|
|
2046
|
+
"comprobanteAsociado",
|
|
2047
|
+
"otroTributo",
|
|
2048
|
+
"subtotalIVA",
|
|
2049
|
+
"datoAdicional",
|
|
2050
|
+
"actividad",
|
|
2051
|
+
"comprador"
|
|
2052
|
+
]);
|
|
2053
|
+
var TEXT_KEYS = /* @__PURE__ */ new Set([
|
|
2054
|
+
"codigo",
|
|
2055
|
+
"descripcion",
|
|
2056
|
+
"codigoMtx",
|
|
2057
|
+
"c1",
|
|
2058
|
+
"c2",
|
|
2059
|
+
"c3",
|
|
2060
|
+
"c4",
|
|
2061
|
+
"c5",
|
|
2062
|
+
"c6",
|
|
2063
|
+
"codigoMoneda",
|
|
2064
|
+
"cancelaEnMismaMonedaExtranjera"
|
|
2065
|
+
]);
|
|
2066
|
+
function normalizeWire(value, key = "") {
|
|
2067
|
+
if (value === void 0 || value === null) {
|
|
2068
|
+
return value;
|
|
2069
|
+
}
|
|
2070
|
+
if (LIST_KEYS.has(key)) {
|
|
2071
|
+
return (Array.isArray(value) ? value : [value]).map(
|
|
2072
|
+
(v) => normalizeWire(v)
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
if (Array.isArray(value)) {
|
|
2076
|
+
return value.map((v) => normalizeWire(v));
|
|
2077
|
+
}
|
|
2078
|
+
if (value && typeof value === "object") {
|
|
2079
|
+
return Object.fromEntries(
|
|
2080
|
+
Object.entries(value).filter(([, v]) => v !== void 0).map(([k, v]) => [k, normalizeWire(v, k)])
|
|
2081
|
+
);
|
|
2082
|
+
}
|
|
2083
|
+
if (TEXT_KEYS.has(key) && value !== void 0 && value !== null) {
|
|
2084
|
+
return String(value);
|
|
2085
|
+
}
|
|
2086
|
+
if (!TEXT_KEYS.has(key) && typeof value === "string" && /^\d+(\.\d+)?$/.test(value)) {
|
|
2087
|
+
return Number(value);
|
|
2088
|
+
}
|
|
2089
|
+
return value;
|
|
2090
|
+
}
|
|
2091
|
+
function createWsmtxcaIssuanceService(wsmtxca) {
|
|
2092
|
+
return {
|
|
2093
|
+
getNextVoucherNumber: async (input) => (await wsmtxca.getLastAuthorizedVoucher(input)).voucherNumber + 1,
|
|
2094
|
+
issue: (input) => wsmtxca.issue({
|
|
2095
|
+
representedTaxId: input.representedTaxId,
|
|
2096
|
+
forceRefresh: input.forceRefresh,
|
|
2097
|
+
data: wsmtxcaRequest(input.data, input.voucherNumber)
|
|
2098
|
+
}),
|
|
2099
|
+
lookupVoucher: async (input) => {
|
|
2100
|
+
const result = await wsmtxca.lookupVoucher({
|
|
2101
|
+
...input,
|
|
2102
|
+
voucherNumber: input.number
|
|
2103
|
+
});
|
|
2104
|
+
return result.kind === "found" ? { ...result, voucher: wsmtxcaHeader(result.voucher) } : result;
|
|
2105
|
+
}
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
function wsmtxcaAdditionalData(fields) {
|
|
2109
|
+
if (!fields?.length) {
|
|
2110
|
+
return void 0;
|
|
2111
|
+
}
|
|
2112
|
+
const options = new Map(fields.map((f) => [f.id, f.value]));
|
|
2113
|
+
return {
|
|
2114
|
+
datoAdicional: [
|
|
2115
|
+
...options.has("2101") ? [{ t: 21, c1: options.get("2101"), c2: options.get("2102") }] : [],
|
|
2116
|
+
...fields.filter((f) => f.id !== "2101" && f.id !== "2102").map((f) => ({ t: Number(f.id), c1: f.value }))
|
|
2117
|
+
]
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
// src/services/wsfe-derive.ts
|
|
2122
|
+
var INVOICE_TYPES = {
|
|
2123
|
+
A: ARCA_VOUCHER_TYPES.FACTURA_A,
|
|
2124
|
+
B: ARCA_VOUCHER_TYPES.FACTURA_B,
|
|
2125
|
+
C: ARCA_VOUCHER_TYPES.FACTURA_C
|
|
2126
|
+
};
|
|
2127
|
+
function deriveWsfeInvoice(input, now = /* @__PURE__ */ new Date()) {
|
|
2128
|
+
assertIssueObject(input, "input");
|
|
2129
|
+
assertIssueKeys(
|
|
2130
|
+
input,
|
|
2131
|
+
[
|
|
2132
|
+
...ISSUANCE_KEYS,
|
|
2133
|
+
"family",
|
|
2134
|
+
"details",
|
|
2135
|
+
"issuer",
|
|
2136
|
+
"items",
|
|
2137
|
+
"salesPoint",
|
|
2138
|
+
"to",
|
|
2139
|
+
"total",
|
|
2140
|
+
"date",
|
|
2141
|
+
"currency",
|
|
2142
|
+
"exchangeRate",
|
|
2143
|
+
"service"
|
|
2144
|
+
],
|
|
2145
|
+
"input"
|
|
2146
|
+
);
|
|
2147
|
+
validateIssuanceFields(input);
|
|
2148
|
+
if (input.amounts !== void 0 && input.items !== void 0) {
|
|
2149
|
+
invalid2("amounts", "used instead of items, never with items");
|
|
2150
|
+
}
|
|
2151
|
+
assertIssuerCondition(input.issuer);
|
|
2152
|
+
assertSalesPoint(input.salesPoint);
|
|
2153
|
+
const receiver = deriveReceiver(input.to);
|
|
2154
|
+
const voucherClass = resolveInvoiceClass(input.issuer, input.to.condition);
|
|
2155
|
+
const { data: amountsData, amounts } = input.amounts ? reviewedInvoiceAmounts(input.amounts, input.taxes) : calculateWsfeAmounts({
|
|
2156
|
+
voucherClass,
|
|
2157
|
+
items: input.items,
|
|
2158
|
+
total: input.total === void 0 ? void 0 : input.total - tributeTotal(input.taxes ?? [])
|
|
2159
|
+
});
|
|
2160
|
+
const currency = deriveCurrency(input);
|
|
2161
|
+
const voucherDate = normalizeWsfeDateInput(
|
|
2162
|
+
input.date === void 0 ? buenosAiresDate(now) : input.date,
|
|
2163
|
+
"date"
|
|
2164
|
+
);
|
|
2165
|
+
const data = {
|
|
2166
|
+
salesPoint: input.salesPoint,
|
|
2167
|
+
voucherType: input.family === void 0 ? INVOICE_TYPES[voucherClass] : invoiceType(input.family, voucherClass),
|
|
2168
|
+
voucherDate,
|
|
2169
|
+
...receiver,
|
|
2170
|
+
...currency,
|
|
2171
|
+
...amountsData,
|
|
2172
|
+
...deriveService(input.service, voucherDate)
|
|
2173
|
+
};
|
|
2174
|
+
applyIssuanceFields(data, input);
|
|
2175
|
+
const headerTotal = Number(
|
|
2176
|
+
normalizeArcaAmountToMinorUnits(data.netAmount, "net") + normalizeArcaAmountToMinorUnits(data.vatAmount, "vat") + normalizeArcaAmountToMinorUnits(data.exemptAmount, "exempt") + normalizeArcaAmountToMinorUnits(data.nonTaxableAmount, "untaxed") + normalizeArcaAmountToMinorUnits(data.taxAmount, "tax")
|
|
2177
|
+
);
|
|
2178
|
+
const reviewedTotal = input.amounts ? input.total : void 0;
|
|
2179
|
+
data.totalAmount = reviewedTotal === void 0 ? headerTotal / 100 : minor(reviewedTotal, "total");
|
|
2180
|
+
amounts.computedTotal += headerTotal - amounts.sentTotal;
|
|
2181
|
+
amounts.sentTotal = reviewedTotal ?? headerTotal;
|
|
2182
|
+
if (receiver.receiverVatConditionId === 5 && receiver.documentType === ARCA_DOCUMENT_TYPES.CONSUMIDOR_FINAL) {
|
|
2183
|
+
const [whole, fraction = ""] = currency.exchangeRate.split(".");
|
|
2184
|
+
const rate = BigInt(whole) * 1000000n + BigInt(fraction.padEnd(6, "0"));
|
|
2185
|
+
if (BigInt(amounts.sentTotal) * rate >= ARCA_FINAL_CONSUMER_IDENTIFICATION_THRESHOLD_MINOR_UNITS * 1000000n) {
|
|
2186
|
+
throw new ArcaInputError(
|
|
2187
|
+
"The final consumer must be identified for this amount.",
|
|
2188
|
+
{
|
|
2189
|
+
code: "ARCA_INPUT_MISSING_FIELD",
|
|
2190
|
+
field: "to",
|
|
2191
|
+
expected: "cuit or dni for operations at or above ARS 10,000,000 (RG 5866)"
|
|
2192
|
+
}
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
validateFiscalHeader(data);
|
|
2197
|
+
try {
|
|
2198
|
+
normalizeWsfeVoucherInput(data);
|
|
2199
|
+
} catch (cause) {
|
|
2200
|
+
if (cause instanceof ArcaInputError) {
|
|
2201
|
+
throw cause;
|
|
2202
|
+
}
|
|
2203
|
+
throw new ArcaError(
|
|
2204
|
+
"The derived invoice failed exact WSFE validation. This is an SDK invariant failure.",
|
|
2205
|
+
"ARCA_ISSUE_INVARIANT",
|
|
2206
|
+
{ cause }
|
|
2207
|
+
);
|
|
2208
|
+
}
|
|
2209
|
+
return { data, voucherClass, amounts };
|
|
2210
|
+
}
|
|
2211
|
+
function reviewedInvoiceAmounts(amounts, taxes) {
|
|
2212
|
+
const taxTotal = taxes === void 0 ? 0 : tributeTotal(taxes);
|
|
2213
|
+
const total = amounts.net + amounts.vat + (amounts.exempt ?? 0) + (amounts.untaxed ?? 0) + taxTotal;
|
|
2214
|
+
return {
|
|
2215
|
+
data: {
|
|
2216
|
+
totalAmount: minor(total, "total"),
|
|
2217
|
+
taxAmount: minor(taxTotal, "taxes.total"),
|
|
2218
|
+
...reviewedHeaderAmounts(amounts)
|
|
2219
|
+
},
|
|
2220
|
+
amounts: { computedTotal: total, sentTotal: total, vatAdjustment: 0 }
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
function assertIssuerCondition(issuer) {
|
|
2224
|
+
if (typeof issuer !== "string" || !Object.hasOwn(ARCA_ISSUER_CONDITION_IDS, issuer)) {
|
|
2225
|
+
invalid2(
|
|
2226
|
+
"issuer",
|
|
2227
|
+
"responsable_inscripto, monotributo, exento, or no_alcanzado"
|
|
2228
|
+
);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
function assertSalesPoint(salesPoint) {
|
|
2232
|
+
if (!Number.isSafeInteger(salesPoint) || salesPoint < 1 || salesPoint > 99999) {
|
|
2233
|
+
invalid2("salesPoint", "an integer from 1 through 99999");
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
function resolveInvoiceClass(issuer, condition) {
|
|
2237
|
+
if (typeof condition === "number") {
|
|
2238
|
+
return issuer === "responsable_inscripto" ? [1, 6, 13, 16].includes(condition) ? "A" : "B" : "C";
|
|
2239
|
+
}
|
|
2240
|
+
return ARCA_INVOICE_CLASS_BY_ISSUER[issuer][condition];
|
|
2241
|
+
}
|
|
2242
|
+
function deriveReceiver(to) {
|
|
2243
|
+
assertIssueObject(to, "to");
|
|
2244
|
+
assertIssueKeys(to, ["condition", "cuit", "dni", "document"], "to");
|
|
2245
|
+
if (typeof to.condition === "number") {
|
|
2246
|
+
return deriveNumericReceiver(
|
|
2247
|
+
to
|
|
2248
|
+
);
|
|
2249
|
+
}
|
|
2250
|
+
if (typeof to.condition !== "string" || !Object.hasOwn(ARCA_RECEIVER_CONDITION_IDS, to.condition)) {
|
|
2251
|
+
invalid2("to.condition", "one of the five supported receiver conditions");
|
|
2252
|
+
}
|
|
2253
|
+
if (to.cuit !== void 0 && to.dni !== void 0) {
|
|
2254
|
+
invalid2("to", "either cuit or dni, never both");
|
|
2255
|
+
}
|
|
2256
|
+
if (to.condition !== "consumidor_final" && to.cuit === void 0) {
|
|
2257
|
+
throw new ArcaInputError(
|
|
2258
|
+
"to.cuit is required for this receiver (WSFE 10063 for class A).",
|
|
2259
|
+
{
|
|
2260
|
+
code: "ARCA_INPUT_MISSING_FIELD",
|
|
2261
|
+
field: "to.cuit",
|
|
2262
|
+
expected: "an 11-digit CUIT"
|
|
2263
|
+
}
|
|
2264
|
+
);
|
|
2265
|
+
}
|
|
2266
|
+
const documentType = to.cuit === void 0 ? to.dni === void 0 ? ARCA_DOCUMENT_TYPES.CONSUMIDOR_FINAL : ARCA_DOCUMENT_TYPES.DNI : ARCA_DOCUMENT_TYPES.CUIT;
|
|
2267
|
+
const documentNumber = to.cuit === void 0 ? to.dni === void 0 ? 0 : issueDocumentNumber(to.dni, "to.dni", 1, 11) : issueDocumentNumber(to.cuit, "to.cuit", 11, 11);
|
|
2268
|
+
return {
|
|
2269
|
+
documentType,
|
|
2270
|
+
documentNumber,
|
|
2271
|
+
receiverVatConditionId: ARCA_RECEIVER_CONDITION_IDS[to.condition]
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
2274
|
+
function issueDocumentNumber(value, field, min, max) {
|
|
2275
|
+
if (typeof value !== "number" && typeof value !== "string") {
|
|
2276
|
+
invalid2(field, `a positive document number with ${min} to ${max} digits`);
|
|
2277
|
+
}
|
|
2278
|
+
const text = String(value);
|
|
2279
|
+
if (!/^\d+$/.test(text) || text.length < min || text.length > max || !Number.isSafeInteger(Number(text)) || Number(text) <= 0) {
|
|
2280
|
+
invalid2(field, `a positive document number with ${min} to ${max} digits`);
|
|
2281
|
+
}
|
|
2282
|
+
return Number(text);
|
|
2283
|
+
}
|
|
2284
|
+
function deriveCurrency(input) {
|
|
2285
|
+
const currency = input.currency === void 0 ? "ARS" : input.currency;
|
|
2286
|
+
if (typeof currency === "object" && currency !== null) {
|
|
2287
|
+
assertIssueKeys(currency, ["id"], "currency");
|
|
2288
|
+
if (!/^[A-Z0-9]{3}$/.test(currency.id)) {
|
|
2289
|
+
invalid2("currency.id", "a three-character ARCA currency code");
|
|
2290
|
+
}
|
|
2291
|
+
if (input.exchangeRate === void 0) {
|
|
2292
|
+
invalid2("exchangeRate", "an explicit rate for this currency");
|
|
2293
|
+
}
|
|
2294
|
+
return {
|
|
2295
|
+
currencyId: currency.id,
|
|
2296
|
+
exchangeRate: serializeArcaExchangeRate(
|
|
2297
|
+
input.exchangeRate,
|
|
2298
|
+
"exchangeRate"
|
|
2299
|
+
)
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
if (currency !== "ARS" && currency !== "USD") {
|
|
2303
|
+
invalid2("currency", "ARS or USD");
|
|
2304
|
+
}
|
|
2305
|
+
if (input.exchangeRate !== void 0 && typeof input.exchangeRate !== "string") {
|
|
2306
|
+
invalid2("exchangeRate", "a decimal string");
|
|
2307
|
+
}
|
|
2308
|
+
if (currency === "USD" && input.exchangeRate === void 0) {
|
|
2309
|
+
throw new ArcaInputError("exchangeRate is required for USD.", {
|
|
2310
|
+
code: "ARCA_INPUT_MISSING_FIELD",
|
|
2311
|
+
field: "exchangeRate",
|
|
2312
|
+
expected: "a positive decimal string"
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
const exchangeRate = serializeArcaExchangeRate(
|
|
2316
|
+
input.exchangeRate ?? "1",
|
|
2317
|
+
"exchangeRate"
|
|
2318
|
+
);
|
|
2319
|
+
if (currency === "ARS" && exchangeRate !== "1") {
|
|
2320
|
+
throw new ArcaInputError("exchangeRate must be 1 for ARS.", {
|
|
2321
|
+
code: "ARCA_INPUT_INVALID_EXCHANGE_RATE",
|
|
2322
|
+
field: "exchangeRate",
|
|
2323
|
+
expected: "1 for ARS"
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
return { currencyId: ARCA_CURRENCY_IDS[currency], exchangeRate };
|
|
2327
|
+
}
|
|
2328
|
+
function deriveService(service, date) {
|
|
2329
|
+
if (service === void 0) {
|
|
2330
|
+
return { concept: 1 };
|
|
2331
|
+
}
|
|
2332
|
+
assertIssueObject(service, "service");
|
|
2333
|
+
assertIssueKeys(service, ["from", "to", "dueDate"], "service");
|
|
2334
|
+
for (const field of ["from", "to", "dueDate"]) {
|
|
2335
|
+
if (service[field] === void 0) {
|
|
2336
|
+
throw new ArcaInputError(`service.${field} is required.`, {
|
|
2337
|
+
code: "ARCA_INPUT_MISSING_FIELD",
|
|
2338
|
+
field: `service.${field}`,
|
|
2339
|
+
expected: "a calendar date"
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
const serviceStartDate = normalizeWsfeDateInput(
|
|
2344
|
+
service.from,
|
|
2345
|
+
"service.from"
|
|
2346
|
+
);
|
|
2347
|
+
const serviceEndDate = normalizeWsfeDateInput(
|
|
2348
|
+
service.to,
|
|
2349
|
+
"service.to"
|
|
2350
|
+
);
|
|
2351
|
+
const paymentDueDate = normalizeWsfeDateInput(
|
|
2352
|
+
service.dueDate,
|
|
2353
|
+
"service.dueDate"
|
|
2354
|
+
);
|
|
2355
|
+
if (serviceEndDate < serviceStartDate) {
|
|
2356
|
+
invalid2("service.to", "a date on or after service.from");
|
|
2357
|
+
}
|
|
2358
|
+
if (paymentDueDate < date) {
|
|
2359
|
+
invalid2("service.dueDate", "a date on or after date");
|
|
2360
|
+
}
|
|
2361
|
+
return { concept: 2, serviceStartDate, serviceEndDate, paymentDueDate };
|
|
2362
|
+
}
|
|
2363
|
+
function buenosAiresDate(now) {
|
|
2364
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
2365
|
+
timeZone: "America/Argentina/Buenos_Aires",
|
|
2366
|
+
year: "numeric",
|
|
2367
|
+
month: "2-digit",
|
|
2368
|
+
day: "2-digit"
|
|
2369
|
+
}).formatToParts(now);
|
|
2370
|
+
return ["year", "month", "day"].map((part) => parts.find((entry) => entry.type === part)?.value).join("");
|
|
2371
|
+
}
|
|
2372
|
+
function assertIssueObject(value, field) {
|
|
2373
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
2374
|
+
invalid2(field, "an object");
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
function assertIssueKeys(value, keys, prefix, method = "issue()") {
|
|
2378
|
+
for (const key of Object.keys(value)) {
|
|
2379
|
+
if (!keys.includes(key)) {
|
|
2380
|
+
const field = prefix === "input" ? key : `${prefix}.${key}`;
|
|
2381
|
+
throw new ArcaInputError(`${field} is not supported by ${method}.`, {
|
|
2382
|
+
code: "ARCA_INPUT_RESERVED_FIELD",
|
|
2383
|
+
field,
|
|
2384
|
+
expected: "a supported high-level API field; use the exact API for other fiscal fields"
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
function invalid2(field, expected) {
|
|
2390
|
+
throw new ArcaInputError(`${field} must be ${expected}.`, {
|
|
2391
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
2392
|
+
field,
|
|
2393
|
+
expected
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2396
|
+
function deriveNumericReceiver(to) {
|
|
2397
|
+
if (![1, 4, 5, 6, 7, 8, 9, 10, 13, 15, 16].includes(to.condition)) {
|
|
2398
|
+
invalid2("to.condition", "an ARCA receiver condition");
|
|
2399
|
+
}
|
|
2400
|
+
const document = "document" in to ? to.document : void 0;
|
|
2401
|
+
if (document) {
|
|
2402
|
+
assertIssueKeys(document, ["type", "number"], "to.document");
|
|
2403
|
+
if (to.cuit !== void 0 || to.dni !== void 0) {
|
|
2404
|
+
invalid2("to", "one document identity");
|
|
2405
|
+
}
|
|
2406
|
+
if (!Number.isInteger(document.type) || document.type < 0 || document.type > 99 || !/^\d{1,11}$/.test(String(document.number)) || !Number.isSafeInteger(Number(document.number))) {
|
|
2407
|
+
invalid2("to.document", "a valid document type and number");
|
|
2408
|
+
}
|
|
2409
|
+
if (document.type === 80 && String(document.number).length !== 11) {
|
|
2410
|
+
invalid2("to.document.number", "an 11-digit CUIT");
|
|
2411
|
+
}
|
|
2412
|
+
return {
|
|
2413
|
+
documentType: document.type,
|
|
2414
|
+
documentNumber: Number(document.number),
|
|
2415
|
+
receiverVatConditionId: to.condition
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
if (to.cuit === void 0 && to.dni === void 0) {
|
|
2419
|
+
invalid2("to", "an explicit document");
|
|
2420
|
+
}
|
|
2421
|
+
if (to.cuit !== void 0 && to.dni !== void 0) {
|
|
2422
|
+
invalid2("to", "one document identity");
|
|
2423
|
+
}
|
|
2424
|
+
return {
|
|
2425
|
+
documentType: to.cuit === void 0 ? 96 : 80,
|
|
2426
|
+
documentNumber: issueDocumentNumber(
|
|
2427
|
+
to.cuit ?? to.dni,
|
|
2428
|
+
"to.document",
|
|
2429
|
+
to.cuit === void 0 ? 1 : 11,
|
|
2430
|
+
11
|
|
2431
|
+
),
|
|
2432
|
+
receiverVatConditionId: to.condition
|
|
2433
|
+
};
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
// src/services/wsfe-credit-note.ts
|
|
2437
|
+
function invalid3(reason) {
|
|
2438
|
+
throw new ArcaInputError(
|
|
2439
|
+
`issueCreditNote cannot proceed: ${reason}. Use the exact service API for manual control.`,
|
|
2440
|
+
{ code: "ARCA_INPUT_INVALID_VALUE" }
|
|
2441
|
+
);
|
|
2442
|
+
}
|
|
2443
|
+
function required(value, field) {
|
|
2444
|
+
if (value === void 0 || value === null) {
|
|
2445
|
+
invalid3(`original is missing ${field}`);
|
|
2446
|
+
}
|
|
2447
|
+
return value;
|
|
2448
|
+
}
|
|
2449
|
+
function deriveWsfeFullCreditNote(original, input, now = /* @__PURE__ */ new Date(), kind = "creditNote") {
|
|
2450
|
+
const { note, header } = prepareCreditNote(original, input, now, kind);
|
|
2451
|
+
const data = {
|
|
2452
|
+
...header,
|
|
2453
|
+
...original.taxes ? { taxes: structuredClone(original.taxes) } : {},
|
|
2454
|
+
totalAmount: required(original.totalAmount, "totalAmount"),
|
|
2455
|
+
netAmount: required(original.netAmount, "netAmount"),
|
|
2456
|
+
vatAmount: required(original.vatAmount, "vatAmount"),
|
|
2457
|
+
exemptAmount: required(original.exemptAmount, "exemptAmount"),
|
|
2458
|
+
nonTaxableAmount: required(original.nonTaxableAmount, "nonTaxableAmount"),
|
|
2459
|
+
taxAmount: required(original.taxAmount, "taxAmount"),
|
|
2460
|
+
...original.vatRates === void 0 ? {} : { vatRates: original.vatRates.map((rate) => ({ ...rate })) }
|
|
2461
|
+
};
|
|
2462
|
+
normalizeWsfeVoucherInput(data);
|
|
2463
|
+
const total = Number(
|
|
2464
|
+
normalizeArcaAmountToMinorUnits(data.totalAmount, "totalAmount")
|
|
2465
|
+
);
|
|
2466
|
+
return {
|
|
2467
|
+
data,
|
|
2468
|
+
voucherClass: note.voucherClass,
|
|
2469
|
+
amounts: { computedTotal: total, sentTotal: total, vatAdjustment: 0 }
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
function deriveWsfePartialCreditNote(original, input, now = /* @__PURE__ */ new Date(), kind = "creditNote") {
|
|
2473
|
+
if (input.items === void 0 && input.amounts === void 0) {
|
|
2474
|
+
invalid3("items is required to credit chosen lines");
|
|
2475
|
+
}
|
|
2476
|
+
const requestedTotal = input.total === void 0 ? void 0 : assertArcaMinorUnits(input.total, "total");
|
|
2477
|
+
const { note, header } = prepareCreditNote(original, input, now, kind);
|
|
2478
|
+
const { data: amountsData, amounts } = input.amounts ? reviewedInvoiceAmounts(input.amounts, input.taxes) : calculateWsfeAmounts({
|
|
2479
|
+
voucherClass: note.voucherClass,
|
|
2480
|
+
items: input.items,
|
|
2481
|
+
total: input.total === void 0 ? void 0 : input.total - tributeTotal(input.taxes ?? [])
|
|
2482
|
+
});
|
|
2483
|
+
const originalTotal = normalizeArcaAmountToMinorUnits(
|
|
2484
|
+
required(original.totalAmount, "totalAmount"),
|
|
2485
|
+
"totalAmount"
|
|
2486
|
+
);
|
|
2487
|
+
if (kind === "creditNote" && (requestedTotal ?? BigInt(amounts.sentTotal)) > originalTotal) {
|
|
2488
|
+
invalid3(
|
|
2489
|
+
"the note total is greater than the original; the SDK does not track earlier notes against an original"
|
|
2490
|
+
);
|
|
2491
|
+
}
|
|
2492
|
+
const data = { ...header, ...amountsData };
|
|
2493
|
+
applyIssuanceFields(data, {
|
|
2494
|
+
...input,
|
|
2495
|
+
fce: void 0,
|
|
2496
|
+
optionalFields: void 0
|
|
2497
|
+
});
|
|
2498
|
+
const total = Number(
|
|
2499
|
+
[
|
|
2500
|
+
data.netAmount,
|
|
2501
|
+
data.vatAmount,
|
|
2502
|
+
data.exemptAmount,
|
|
2503
|
+
data.nonTaxableAmount,
|
|
2504
|
+
data.taxAmount
|
|
2505
|
+
].reduce(
|
|
2506
|
+
(sum, amount) => sum + normalizeArcaAmountToMinorUnits(amount, "amount"),
|
|
2507
|
+
0n
|
|
2508
|
+
)
|
|
2509
|
+
);
|
|
2510
|
+
const sentTotal = input.amounts && requestedTotal !== void 0 ? Number(requestedTotal) : total;
|
|
2511
|
+
if (kind === "creditNote" && BigInt(sentTotal) > originalTotal) {
|
|
2512
|
+
invalid3("the note total is greater than the original");
|
|
2513
|
+
}
|
|
2514
|
+
data.totalAmount = minor(sentTotal, "total");
|
|
2515
|
+
amounts.computedTotal += total - amounts.sentTotal;
|
|
2516
|
+
amounts.sentTotal = sentTotal;
|
|
2517
|
+
normalizeWsfeVoucherInput(data);
|
|
2518
|
+
return { data, voucherClass: note.voucherClass, amounts };
|
|
2519
|
+
}
|
|
2520
|
+
function prepareCreditNote(original, input, now, kind) {
|
|
2521
|
+
assertOriginalExtensions(original);
|
|
2522
|
+
const family = voucherFamily(original.voucherType ?? 0);
|
|
2523
|
+
if (family.types[2] === original.voucherType) {
|
|
2524
|
+
invalid3("original must be an invoice or debit note");
|
|
2525
|
+
}
|
|
2526
|
+
const note = {
|
|
2527
|
+
voucherClass: family.voucherClass,
|
|
2528
|
+
voucherType: family.types[kind === "creditNote" ? 2 : 1]
|
|
2529
|
+
};
|
|
2530
|
+
if (!(["A", "O"].includes(original.result ?? "") && original.cae?.trim() && original.caeExpiry?.trim())) {
|
|
2531
|
+
invalid3("original is not authorized");
|
|
2532
|
+
}
|
|
2533
|
+
const voucherDate = normalizeWsfeDateInput(
|
|
2534
|
+
input.date ?? buenosAiresDate(now),
|
|
2535
|
+
"date"
|
|
2536
|
+
);
|
|
2537
|
+
const originalDate = normalizeWsfeDateInput(
|
|
2538
|
+
required(original.voucherDate, "voucherDate"),
|
|
2539
|
+
"original.voucherDate"
|
|
2540
|
+
);
|
|
2541
|
+
if (originalDate > voucherDate && originalDate.slice(0, 6) !== voucherDate.slice(0, 6)) {
|
|
2542
|
+
invalid3(
|
|
2543
|
+
"original date is later than the note and outside its month (10210)"
|
|
2544
|
+
);
|
|
2545
|
+
}
|
|
2546
|
+
const header = {
|
|
2547
|
+
salesPoint: input.salesPoint ?? required(original.salesPoint, "salesPoint"),
|
|
2548
|
+
voucherType: note.voucherType,
|
|
2549
|
+
concept: required(original.concept, "concept"),
|
|
2550
|
+
documentType: required(original.documentType, "documentType"),
|
|
2551
|
+
documentNumber: Number(required(original.documentNumber, "documentNumber")),
|
|
2552
|
+
receiverVatConditionId: required(
|
|
2553
|
+
original.receiverVatConditionId,
|
|
2554
|
+
"receiverVatConditionId"
|
|
2555
|
+
),
|
|
2556
|
+
currencyId: required(original.currencyId, "currencyId"),
|
|
2557
|
+
...original.sameCurrencyForeignCancellation === void 0 ? {} : {
|
|
2558
|
+
sameCurrencyForeignCancellation: original.sameCurrencyForeignCancellation
|
|
2559
|
+
},
|
|
2560
|
+
...original.buyers ? { buyers: structuredClone(original.buyers) } : {},
|
|
2561
|
+
...original.activities ? { activities: structuredClone(original.activities) } : {},
|
|
2562
|
+
...input.optionalFields ? { optionalFields: structuredClone(input.optionalFields) } : {},
|
|
2563
|
+
exchangeRate: required(original.exchangeRate, "exchangeRate"),
|
|
2564
|
+
voucherDate,
|
|
2565
|
+
associatedVouchers: [
|
|
2566
|
+
{
|
|
2567
|
+
type: required(original.voucherType, "voucherType"),
|
|
2568
|
+
salesPoint: required(original.salesPoint, "salesPoint"),
|
|
2569
|
+
number: original.voucherNumber,
|
|
2570
|
+
voucherDate: originalDate
|
|
2571
|
+
}
|
|
2572
|
+
]
|
|
2573
|
+
};
|
|
2574
|
+
applyFceFields(header, input.fce);
|
|
2575
|
+
copyServiceDates(original, header);
|
|
2576
|
+
return { note, header };
|
|
2577
|
+
}
|
|
2578
|
+
function copyServiceDates(original, header) {
|
|
2579
|
+
if (header.concept !== 2 && header.concept !== 3) {
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2582
|
+
header.serviceStartDate = required(
|
|
2583
|
+
original.serviceStartDate,
|
|
2584
|
+
"serviceStartDate"
|
|
2585
|
+
);
|
|
2586
|
+
header.serviceEndDate = required(
|
|
2587
|
+
original.serviceEndDate,
|
|
2588
|
+
"serviceEndDate"
|
|
2589
|
+
);
|
|
2590
|
+
const due = normalizeWsfeDateInput(
|
|
2591
|
+
required(original.paymentDueDate, "paymentDueDate"),
|
|
2592
|
+
"original.paymentDueDate"
|
|
2593
|
+
);
|
|
2594
|
+
header.paymentDueDate = due < header.voucherDate ? header.voucherDate : due;
|
|
2595
|
+
}
|
|
2596
|
+
var CREDIT_NOTE_KEYS = [
|
|
2597
|
+
"for",
|
|
2598
|
+
"salesPoint",
|
|
2599
|
+
"date",
|
|
2600
|
+
"items",
|
|
2601
|
+
"total",
|
|
2602
|
+
"all",
|
|
2603
|
+
"taxes",
|
|
2604
|
+
"amounts",
|
|
2605
|
+
"optionalFields",
|
|
2606
|
+
"details",
|
|
2607
|
+
"fce"
|
|
2608
|
+
];
|
|
2609
|
+
var TARGET_BOUNDS = [
|
|
2610
|
+
["salesPoint", 99999],
|
|
2611
|
+
["voucherType", 999],
|
|
2612
|
+
["number", 99999999]
|
|
2613
|
+
];
|
|
2614
|
+
function assertCreditNoteInput(input) {
|
|
2615
|
+
assertIssueObject(input, "input");
|
|
2616
|
+
validateIssuanceFields(input);
|
|
2617
|
+
if ("associatedPeriod" in input) {
|
|
2618
|
+
throw new ArcaInputError(
|
|
2619
|
+
"issueCreditNote does not support associatedPeriod; a note against a period is exact-layer work. Use the exact service API for manual control.",
|
|
2620
|
+
{
|
|
2621
|
+
code: "ARCA_INPUT_RESERVED_FIELD",
|
|
2622
|
+
field: "associatedPeriod",
|
|
2623
|
+
expected: "an associated invoice through for"
|
|
2624
|
+
}
|
|
2625
|
+
);
|
|
2626
|
+
}
|
|
2627
|
+
assertIssueKeys(input, CREDIT_NOTE_KEYS, "input", "issueCreditNote()");
|
|
2628
|
+
const target = assertCreditNoteTarget(input.for);
|
|
2629
|
+
if (input.salesPoint !== void 0) {
|
|
2630
|
+
assertCreditNoteBound(input.salesPoint, 99999, "salesPoint");
|
|
2631
|
+
}
|
|
2632
|
+
const date = input.date === void 0 ? void 0 : normalizeWsfeDateInput(input.date, "date");
|
|
2633
|
+
const common = {
|
|
2634
|
+
...input.fce === void 0 ? {} : { fce: structuredClone(input.fce) },
|
|
2635
|
+
...input.taxes === void 0 ? {} : { taxes: structuredClone(input.taxes) },
|
|
2636
|
+
...input.details === void 0 ? {} : { details: structuredClone(input.details) },
|
|
2637
|
+
...input.optionalFields === void 0 ? {} : { optionalFields: structuredClone(input.optionalFields) },
|
|
2638
|
+
for: target,
|
|
2639
|
+
...input.salesPoint === void 0 ? {} : { salesPoint: input.salesPoint },
|
|
2640
|
+
...date === void 0 ? {} : { date }
|
|
2641
|
+
};
|
|
2642
|
+
if (input.items !== void 0 && input.amounts !== void 0) {
|
|
2643
|
+
invalid3("use items or amounts, never both");
|
|
2644
|
+
}
|
|
2645
|
+
if ((input.items === void 0 && input.amounts === void 0) === (input.all === void 0)) {
|
|
2646
|
+
throw new ArcaInputError(
|
|
2647
|
+
"issueCreditNote needs exactly one mode: items or amounts for a partial note, or all: true for the whole original.",
|
|
2648
|
+
{
|
|
2649
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
2650
|
+
field: input.items === void 0 ? "input.items" : "input.all",
|
|
2651
|
+
expected: "exactly one of items, amounts or all: true"
|
|
2652
|
+
}
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2655
|
+
if (input.all !== void 0) {
|
|
2656
|
+
assertFullMode(input);
|
|
2657
|
+
return { ...common, all: true };
|
|
2658
|
+
}
|
|
2659
|
+
if (input.amounts !== void 0) {
|
|
2660
|
+
return {
|
|
2661
|
+
...common,
|
|
2662
|
+
amounts: structuredClone(input.amounts),
|
|
2663
|
+
...input.total === void 0 ? {} : { total: input.total }
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
return {
|
|
2667
|
+
...common,
|
|
2668
|
+
amounts: void 0,
|
|
2669
|
+
items: copyCreditNoteItems(input.items),
|
|
2670
|
+
...input.total === void 0 ? {} : { total: input.total }
|
|
2671
|
+
};
|
|
2672
|
+
}
|
|
2673
|
+
function assertCreditNoteTarget(value) {
|
|
2674
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
2675
|
+
throw new ArcaInputError(
|
|
2676
|
+
"issueCreditNote requires for: the coordinates of the authorized invoice the note corrects.",
|
|
2677
|
+
{
|
|
2678
|
+
code: "ARCA_INPUT_MISSING_FIELD",
|
|
2679
|
+
field: "input.for",
|
|
2680
|
+
expected: "{ salesPoint, voucherType, number }"
|
|
2681
|
+
}
|
|
2682
|
+
);
|
|
2683
|
+
}
|
|
2684
|
+
assertIssueKeys(
|
|
2685
|
+
value,
|
|
2686
|
+
["salesPoint", "voucherType", "number"],
|
|
2687
|
+
"input.for",
|
|
2688
|
+
"issueCreditNote()"
|
|
2689
|
+
);
|
|
2690
|
+
for (const [field, max] of TARGET_BOUNDS) {
|
|
2691
|
+
assertCreditNoteBound(value[field], max, `for.${field}`);
|
|
2692
|
+
}
|
|
2693
|
+
if (![1, 2, 6, 7, 11, 12, 51, 52, 201, 202, 206, 207, 211, 212].includes(
|
|
2694
|
+
value.voucherType
|
|
2695
|
+
)) {
|
|
2696
|
+
throw new ArcaInputError(
|
|
2697
|
+
"issueCreditNote requires an authorized invoice or debit note in a supported family in for.voucherType. Use the exact service API for manual control.",
|
|
2698
|
+
{
|
|
2699
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
2700
|
+
field: "input.for.voucherType",
|
|
2701
|
+
expected: "1, 6 or 11"
|
|
2702
|
+
}
|
|
2703
|
+
);
|
|
2704
|
+
}
|
|
2705
|
+
return {
|
|
2706
|
+
salesPoint: value.salesPoint,
|
|
2707
|
+
voucherType: value.voucherType,
|
|
2708
|
+
number: value.number
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
function assertCreditNoteBound(value, max, path) {
|
|
2712
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > max) {
|
|
2713
|
+
throw new ArcaInputError(
|
|
2714
|
+
`issueCreditNote requires input.${path} to be an integer from 1 through ${max}.`,
|
|
2715
|
+
{ code: "ARCA_INPUT_INVALID_VALUE", field: `input.${path}` }
|
|
2716
|
+
);
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
function copyCreditNoteItems(items) {
|
|
2720
|
+
if (!Array.isArray(items)) {
|
|
2721
|
+
throw new ArcaInputError(
|
|
2722
|
+
"issueCreditNote requires items to be a non-empty array of items.",
|
|
2723
|
+
{
|
|
2724
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
2725
|
+
field: "input.items",
|
|
2726
|
+
expected: "a non-empty array of items"
|
|
2727
|
+
}
|
|
2728
|
+
);
|
|
2729
|
+
}
|
|
2730
|
+
return items.map(
|
|
2731
|
+
(item) => item === null || typeof item !== "object" ? item : { ...item }
|
|
2732
|
+
);
|
|
2733
|
+
}
|
|
2734
|
+
function assertOriginalExtensions(original) {
|
|
2735
|
+
for (const [field, rawField] of [
|
|
2736
|
+
["taxes", "Tributos"],
|
|
2737
|
+
["optionalFields", "Opcionales"],
|
|
2738
|
+
["buyers", "Compradores"],
|
|
2739
|
+
["activities", "Actividades"],
|
|
2740
|
+
["associatedPeriod", "PeriodoAsoc"]
|
|
2741
|
+
]) {
|
|
2742
|
+
if (original.raw[rawField] && original[field] === void 0) {
|
|
2743
|
+
invalid3(`original ${field} could not be decoded`);
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
try {
|
|
2747
|
+
validateIssuanceFields({
|
|
2748
|
+
taxes: original.taxes?.map((t) => ({
|
|
2749
|
+
id: t.id,
|
|
2750
|
+
description: t.description,
|
|
2751
|
+
base: Number(
|
|
2752
|
+
normalizeArcaAmountToMinorUnits(t.baseAmount, "taxes.base")
|
|
2753
|
+
),
|
|
2754
|
+
rate: t.rate,
|
|
2755
|
+
amount: Number(
|
|
2756
|
+
normalizeArcaAmountToMinorUnits(t.amount, "taxes.amount")
|
|
2757
|
+
)
|
|
2758
|
+
})),
|
|
2759
|
+
optionalFields: original.optionalFields,
|
|
2760
|
+
buyers: original.buyers,
|
|
2761
|
+
activities: original.activities
|
|
2762
|
+
});
|
|
2763
|
+
if (original.associatedPeriod) {
|
|
2764
|
+
normalizeWsfeDateInput(
|
|
2765
|
+
original.associatedPeriod.startDate,
|
|
2766
|
+
"associatedPeriod.startDate"
|
|
2767
|
+
);
|
|
2768
|
+
normalizeWsfeDateInput(
|
|
2769
|
+
original.associatedPeriod.endDate,
|
|
2770
|
+
"associatedPeriod.endDate"
|
|
2771
|
+
);
|
|
2772
|
+
}
|
|
2773
|
+
} catch {
|
|
2774
|
+
invalid3("original extension fields are incomplete or malformed");
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
function assertFullMode(input) {
|
|
2778
|
+
if (input.all !== true) {
|
|
2779
|
+
throw new ArcaInputError(
|
|
2780
|
+
"issueCreditNote accepts only all: true; pass items to credit chosen lines.",
|
|
2781
|
+
{
|
|
2782
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
2783
|
+
field: "input.all",
|
|
2784
|
+
expected: "the literal true"
|
|
2785
|
+
}
|
|
2786
|
+
);
|
|
2787
|
+
}
|
|
2788
|
+
if (input.total !== void 0 || input.taxes !== void 0 || input.amounts !== void 0) {
|
|
2789
|
+
throw new ArcaInputError(
|
|
2790
|
+
"issueCreditNote takes total only with items; all: true credits the original's own total.",
|
|
2791
|
+
{
|
|
2792
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
2793
|
+
field: "input.total",
|
|
2794
|
+
expected: "no total when all is true"
|
|
2795
|
+
}
|
|
2796
|
+
);
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
// src/services/vouchers.ts
|
|
2801
|
+
function createVouchersService(wsfe, context, wsmtxca) {
|
|
2802
|
+
const select = (options = {}) => {
|
|
2803
|
+
validateOptions(options);
|
|
2804
|
+
if (options.service !== "wsmtxca") {
|
|
2805
|
+
return wsfe;
|
|
2806
|
+
}
|
|
2807
|
+
if (!wsmtxca) {
|
|
2808
|
+
throw new ArcaConfigurationError("WSMTXCA service is not configured");
|
|
2809
|
+
}
|
|
2810
|
+
return createWsmtxcaIssuanceService(wsmtxca);
|
|
2811
|
+
};
|
|
2812
|
+
return {
|
|
2813
|
+
recover: async (key, options) => recoverOperation(
|
|
2814
|
+
select,
|
|
2815
|
+
key,
|
|
2816
|
+
options === void 0 ? {} : options,
|
|
2817
|
+
context
|
|
2818
|
+
),
|
|
2819
|
+
issueDebitNote: async (input, options) => issueCreditNote(
|
|
2820
|
+
select(options),
|
|
2821
|
+
input,
|
|
2822
|
+
options ?? {},
|
|
2823
|
+
context,
|
|
2824
|
+
"debitNote"
|
|
2825
|
+
),
|
|
2826
|
+
previewCreditNote: async (input, options) => previewNote(
|
|
2827
|
+
select(options),
|
|
2828
|
+
input,
|
|
2829
|
+
options ?? {},
|
|
2830
|
+
context,
|
|
2831
|
+
"creditNote"
|
|
2832
|
+
),
|
|
2833
|
+
previewDebitNote: async (input, options) => previewNote(
|
|
2834
|
+
select(options),
|
|
2835
|
+
input,
|
|
2836
|
+
options ?? {},
|
|
2837
|
+
context,
|
|
2838
|
+
"debitNote"
|
|
2839
|
+
),
|
|
2840
|
+
issueCreditNote: async (input, options) => {
|
|
2841
|
+
const result = await issueCreditNote(
|
|
2842
|
+
select(options),
|
|
2843
|
+
input,
|
|
2844
|
+
options === void 0 ? {} : options,
|
|
2845
|
+
context
|
|
2846
|
+
);
|
|
2847
|
+
return result;
|
|
2848
|
+
},
|
|
2849
|
+
issue: async (input, options) => {
|
|
2850
|
+
const result = await issueInvoice(
|
|
2851
|
+
select(options),
|
|
2852
|
+
input,
|
|
2853
|
+
options === void 0 ? {} : options,
|
|
2854
|
+
context
|
|
2855
|
+
);
|
|
2856
|
+
return result;
|
|
2857
|
+
},
|
|
2858
|
+
preview: (input, options) => previewInvoice(
|
|
2859
|
+
input,
|
|
2860
|
+
options === void 0 ? {} : options
|
|
2861
|
+
)
|
|
2862
|
+
};
|
|
2863
|
+
}
|
|
2864
|
+
function previewInvoice(input, options) {
|
|
2865
|
+
assertIssueObject(options, "options");
|
|
2866
|
+
assertIssueKeys(options, ["representedTaxId", "service"], "options");
|
|
2867
|
+
validateOptions(options);
|
|
2868
|
+
if (options.representedTaxId !== void 0) {
|
|
2869
|
+
issueDocumentNumber(
|
|
2870
|
+
options.representedTaxId,
|
|
2871
|
+
"options.representedTaxId",
|
|
2872
|
+
11,
|
|
2873
|
+
11
|
|
2874
|
+
);
|
|
2875
|
+
}
|
|
2876
|
+
return toPreview(prepareInvoice(input, options), options);
|
|
2877
|
+
}
|
|
2878
|
+
function prepareInvoice(input, options) {
|
|
2879
|
+
const prepared = deriveWsfeInvoice(input);
|
|
2880
|
+
if (input.details !== void 0) {
|
|
2881
|
+
prepared.data.details = structuredClone(input.details);
|
|
2882
|
+
}
|
|
2883
|
+
validatePrepared(prepared, options);
|
|
2884
|
+
return prepared;
|
|
2885
|
+
}
|
|
2886
|
+
function validatePrepared(prepared, options) {
|
|
2887
|
+
validateFiscalHeader(prepared.data);
|
|
2888
|
+
if (options.service === "wsmtxca") {
|
|
2889
|
+
wsmtxcaRequest(prepared.data);
|
|
2890
|
+
} else if (prepared.data.details !== void 0) {
|
|
2891
|
+
throw new ArcaInputError("Detailed items require service: wsmtxca", {
|
|
2892
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
2893
|
+
field: "details"
|
|
2894
|
+
});
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
function toPreview(prepared, options) {
|
|
2898
|
+
const { data, voucherClass, amounts } = prepared;
|
|
2899
|
+
return {
|
|
2900
|
+
voucherClass,
|
|
2901
|
+
voucherType: data.voucherType,
|
|
2902
|
+
amounts,
|
|
2903
|
+
request: options.service === "wsmtxca" ? wsmtxcaRequest(data) : data,
|
|
2904
|
+
...options.service === "wsmtxca" ? { service: "wsmtxca" } : {}
|
|
2905
|
+
};
|
|
2906
|
+
}
|
|
2907
|
+
async function issueInvoice(wsfe, input, inputOptions, context) {
|
|
2908
|
+
const options = structuredClone(inputOptions);
|
|
2909
|
+
validateOptions(options);
|
|
2910
|
+
validateKeyStore(options, context);
|
|
2911
|
+
const prepared = prepareInvoice(input, options);
|
|
2912
|
+
return await runOperation(
|
|
2913
|
+
wsfe,
|
|
2914
|
+
"issue",
|
|
2915
|
+
input,
|
|
2916
|
+
() => Promise.resolve(prepared),
|
|
2917
|
+
options,
|
|
2918
|
+
context,
|
|
2919
|
+
prepared.amounts
|
|
2920
|
+
);
|
|
2921
|
+
}
|
|
2922
|
+
async function runOperation(wsfe, operation, input, prepare, options, context, replayAmounts) {
|
|
2923
|
+
const store = context?.store;
|
|
2924
|
+
if (options.idempotencyKey === void 0 || !store || !context) {
|
|
2925
|
+
return runAuthorization(wsfe, await prepare(), options);
|
|
2926
|
+
}
|
|
2927
|
+
const key = attemptKey(
|
|
2928
|
+
context.environment,
|
|
2929
|
+
context.taxId,
|
|
2930
|
+
options.idempotencyKey
|
|
2931
|
+
);
|
|
2932
|
+
const representedTaxId = options.representedTaxId === void 0 ? void 0 : String(options.representedTaxId);
|
|
2933
|
+
const inputHash = canonicalHash({
|
|
2934
|
+
input,
|
|
2935
|
+
representedTaxId,
|
|
2936
|
+
...options.service === "wsmtxca" ? { service: "wsmtxca" } : {},
|
|
2937
|
+
...options.number === void 0 ? {} : { number: options.number }
|
|
2938
|
+
});
|
|
2939
|
+
const existing = await storeCall(() => store.get(key));
|
|
2940
|
+
if (existing !== null) {
|
|
2941
|
+
return replay(existing);
|
|
2942
|
+
}
|
|
2943
|
+
const prepared = await prepare();
|
|
2944
|
+
const number = await nextNumber(wsfe, prepared.data, options);
|
|
2945
|
+
const service = options.service ?? "wsfe";
|
|
2946
|
+
const versioned = service === "wsmtxca" || prepared.data.details !== void 0;
|
|
2947
|
+
const record = {
|
|
2948
|
+
v: versioned ? 2 : 1,
|
|
2949
|
+
operation,
|
|
2950
|
+
...versioned ? { service } : {},
|
|
2951
|
+
representedTaxId,
|
|
2952
|
+
inputHash,
|
|
2953
|
+
number,
|
|
2954
|
+
salesPoint: prepared.data.salesPoint,
|
|
2955
|
+
voucherType: prepared.data.voucherType,
|
|
2956
|
+
sent: prepared.data,
|
|
2957
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2958
|
+
};
|
|
2959
|
+
if (await storeCall(() => store.add(key, JSON.stringify(record)))) {
|
|
2960
|
+
return runAuthorization(wsfe, prepared, options, number);
|
|
2961
|
+
}
|
|
2962
|
+
const winner = await storeCall(() => store.get(key));
|
|
2963
|
+
if (winner === null) {
|
|
2964
|
+
throw new ArcaConfigurationError(
|
|
2965
|
+
"ARCA reservation disappeared after atomic creation lost."
|
|
2966
|
+
);
|
|
2967
|
+
}
|
|
2968
|
+
return replay(winner);
|
|
2969
|
+
function replay(json) {
|
|
2970
|
+
const stored = readRecord(json);
|
|
2971
|
+
if ((stored.service ?? "wsfe") !== (options.service ?? "wsfe") || stored.operation !== operation || stored.inputHash !== inputHash || stored.representedTaxId !== representedTaxId) {
|
|
2972
|
+
throw new ArcaInputError(
|
|
2973
|
+
"The idempotency key was already used with different input or operation.",
|
|
2974
|
+
{
|
|
2975
|
+
code: "ARCA_INPUT_IDEMPOTENCY_MISMATCH",
|
|
2976
|
+
field: "options.idempotencyKey"
|
|
2977
|
+
}
|
|
2978
|
+
);
|
|
2979
|
+
}
|
|
2980
|
+
return runAuthorization(
|
|
2981
|
+
wsfe,
|
|
2982
|
+
{
|
|
2983
|
+
...preparedFromRecord(stored),
|
|
2984
|
+
...replayAmounts ? { amounts: replayAmounts } : {}
|
|
2985
|
+
},
|
|
2986
|
+
options,
|
|
2987
|
+
stored.number,
|
|
2988
|
+
true
|
|
2989
|
+
);
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
function validateKeyStore(options, context) {
|
|
2993
|
+
if (options.idempotencyKey === void 0) {
|
|
2994
|
+
return;
|
|
2995
|
+
}
|
|
2996
|
+
if (typeof options.idempotencyKey !== "string" || options.idempotencyKey.length < 1 || options.idempotencyKey.length > 255) {
|
|
2997
|
+
throw new ArcaInputError(
|
|
2998
|
+
"idempotencyKey must contain 1 to 255 characters.",
|
|
2999
|
+
{ code: "ARCA_INPUT_INVALID_VALUE", field: "options.idempotencyKey" }
|
|
3000
|
+
);
|
|
3001
|
+
}
|
|
3002
|
+
if (!context?.store) {
|
|
3003
|
+
throw new ArcaConfigurationError(
|
|
3004
|
+
'idempotencyKey requires a store. Add import { createPostgresStore } from "facturas"; and store: createPostgresStore({ query }) to createArcaClient().'
|
|
3005
|
+
);
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
function readRecord(json) {
|
|
3009
|
+
try {
|
|
3010
|
+
const record = JSON.parse(json);
|
|
3011
|
+
if (!record || record.v !== 1 && record.v !== 2 || record.v === 2 && record.service === void 0 || !["issue", "creditNote", "debitNote"].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) {
|
|
3012
|
+
throw new Error("Invalid reservation structure");
|
|
3013
|
+
}
|
|
3014
|
+
normalizeWsfeVoucherInput(record.sent);
|
|
3015
|
+
if (record.service !== void 0 && record.service !== "wsfe" && record.service !== "wsmtxca") {
|
|
3016
|
+
throw new Error("Invalid provider");
|
|
3017
|
+
}
|
|
3018
|
+
if (record.service === "wsmtxca") {
|
|
3019
|
+
wsmtxcaRequest(record.sent, record.number);
|
|
3020
|
+
}
|
|
3021
|
+
return record;
|
|
3022
|
+
} catch (cause) {
|
|
3023
|
+
throw new ArcaConfigurationError(
|
|
3024
|
+
"Invalid ARCA reservation record; preserve it for reconciliation.",
|
|
3025
|
+
{ cause }
|
|
3026
|
+
);
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
function preparedFromRecord(record) {
|
|
3030
|
+
const sentTotal = Number(
|
|
3031
|
+
normalizeArcaAmountToMinorUnits(record.sent.totalAmount, "totalAmount")
|
|
3032
|
+
);
|
|
3033
|
+
return {
|
|
3034
|
+
data: record.sent,
|
|
3035
|
+
voucherClass: voucherFamily(record.voucherType).voucherClass,
|
|
3036
|
+
amounts: { computedTotal: sentTotal, sentTotal, vatAdjustment: 0 }
|
|
3037
|
+
};
|
|
3038
|
+
}
|
|
3039
|
+
async function nextNumber(wsfe, data, options) {
|
|
3040
|
+
if (options.number !== void 0) {
|
|
3041
|
+
return options.number;
|
|
3042
|
+
}
|
|
3043
|
+
const number = await wsfe.getNextVoucherNumber({
|
|
3044
|
+
representedTaxId: options.representedTaxId,
|
|
3045
|
+
forceRefresh: options.forceRefresh,
|
|
3046
|
+
salesPoint: data.salesPoint,
|
|
3047
|
+
voucherType: data.voucherType
|
|
3048
|
+
});
|
|
3049
|
+
if (!Number.isSafeInteger(number) || number < 1 || number > 99999999) {
|
|
3050
|
+
throw new ArcaServiceError(
|
|
3051
|
+
"ARCA returned an invalid next voucher number.",
|
|
3052
|
+
{
|
|
3053
|
+
service: options.service ?? "wsfe",
|
|
3054
|
+
operation: options.service === "wsmtxca" ? "consultarUltimoComprobanteAutorizado" : "FECompUltimoAutorizado"
|
|
3055
|
+
}
|
|
3056
|
+
);
|
|
3057
|
+
}
|
|
3058
|
+
return number;
|
|
3059
|
+
}
|
|
3060
|
+
async function runAuthorization(wsfe, { data, voucherClass, amounts }, options, reservedNumber, replay = false) {
|
|
3061
|
+
const auth = {
|
|
3062
|
+
representedTaxId: options.representedTaxId,
|
|
3063
|
+
forceRefresh: options.forceRefresh
|
|
3064
|
+
};
|
|
3065
|
+
const includeRaw = options.include?.raw === true;
|
|
3066
|
+
const includeExact = options.include?.exactInput === true;
|
|
3067
|
+
const number = reservedNumber ?? await nextNumber(wsfe, data, options);
|
|
3068
|
+
const attempted = {
|
|
3069
|
+
salesPoint: data.salesPoint,
|
|
3070
|
+
voucherType: data.voucherType,
|
|
3071
|
+
number
|
|
3072
|
+
};
|
|
3073
|
+
const exact = exactEvidence(data, number, options, includeExact);
|
|
3074
|
+
const voucher = (cae, caeExpiry) => ({
|
|
3075
|
+
...attempted,
|
|
3076
|
+
voucherClass,
|
|
3077
|
+
date: data.voucherDate,
|
|
3078
|
+
cae,
|
|
3079
|
+
caeExpiry,
|
|
3080
|
+
amounts
|
|
3081
|
+
});
|
|
3082
|
+
const recovery = {
|
|
3083
|
+
wsfe,
|
|
3084
|
+
auth,
|
|
3085
|
+
data,
|
|
3086
|
+
attempted,
|
|
3087
|
+
includeRaw,
|
|
3088
|
+
exact,
|
|
3089
|
+
voucher,
|
|
3090
|
+
service: options.service
|
|
3091
|
+
};
|
|
3092
|
+
if (replay) {
|
|
3093
|
+
const attempt2 = replayEvidence(options.service);
|
|
3094
|
+
let lookup;
|
|
3095
|
+
try {
|
|
3096
|
+
lookup = await wsfe.lookupVoucher({ ...auth, ...attempted });
|
|
3097
|
+
} catch (error) {
|
|
3098
|
+
return {
|
|
3099
|
+
kind: "indeterminate",
|
|
3100
|
+
attempted,
|
|
3101
|
+
attempt: attempt2,
|
|
3102
|
+
lookup: { kind: "failed", error: toArcaSafeErrorMetadata(error) }
|
|
3103
|
+
};
|
|
3104
|
+
}
|
|
3105
|
+
if (lookup.kind === "found") {
|
|
3106
|
+
return recoverInvoice({ ...recovery, attempt: attempt2, lookup });
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
const authorization = await wsfe.issue({
|
|
3110
|
+
...auth,
|
|
3111
|
+
data,
|
|
3112
|
+
voucherNumber: number
|
|
3113
|
+
});
|
|
3114
|
+
if (authorization.kind === "authorized" && authorization.caeExpiry && authorization.voucherNumber === number) {
|
|
3115
|
+
return {
|
|
3116
|
+
kind: "authorized",
|
|
3117
|
+
recoveredByMatch: false,
|
|
3118
|
+
voucher: voucher(authorization.cae, authorization.caeExpiry),
|
|
3119
|
+
authorization: projectEvidence(authorization, includeRaw),
|
|
3120
|
+
...exact
|
|
3121
|
+
};
|
|
3122
|
+
}
|
|
3123
|
+
if (authorization.kind === "rejected") {
|
|
3124
|
+
if (options.idempotencyKey !== void 0 && (options.service === "wsmtxca" || [...authorization.errors, ...authorization.observations].some(
|
|
3125
|
+
(issue) => issue.code === "10016"
|
|
3126
|
+
))) {
|
|
3127
|
+
const recovered = await recoverInvoice({
|
|
3128
|
+
...recovery,
|
|
3129
|
+
attempt: projectEvidence(
|
|
3130
|
+
{
|
|
3131
|
+
...authorization,
|
|
3132
|
+
kind: "indeterminate",
|
|
3133
|
+
reason: "contradictory_response"
|
|
3134
|
+
},
|
|
3135
|
+
includeRaw
|
|
3136
|
+
)
|
|
3137
|
+
});
|
|
3138
|
+
if (recovered.kind === "authorized" || recovered.kind === "conflict") {
|
|
3139
|
+
return recovered;
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
return {
|
|
3143
|
+
kind: "rejected",
|
|
3144
|
+
attempted,
|
|
3145
|
+
issues: [...authorization.errors, ...authorization.observations].map(
|
|
3146
|
+
projectIssue
|
|
3147
|
+
),
|
|
3148
|
+
authorization: projectEvidence(authorization, includeRaw)
|
|
3149
|
+
};
|
|
3150
|
+
}
|
|
3151
|
+
const uncertain = authorization.kind === "indeterminate" ? authorization : {
|
|
3152
|
+
...authorization,
|
|
3153
|
+
kind: "indeterminate",
|
|
3154
|
+
reason: authorization.voucherNumber === number ? "incomplete_response" : "contradictory_response"
|
|
3155
|
+
};
|
|
3156
|
+
const attempt = projectEvidence(uncertain, includeRaw);
|
|
3157
|
+
return recoverInvoice({
|
|
3158
|
+
wsfe,
|
|
3159
|
+
service: options.service,
|
|
3160
|
+
auth,
|
|
3161
|
+
data,
|
|
3162
|
+
attempted,
|
|
3163
|
+
attempt,
|
|
3164
|
+
includeRaw,
|
|
3165
|
+
exact,
|
|
3166
|
+
voucher
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
async function recoverInvoice({
|
|
3170
|
+
wsfe,
|
|
3171
|
+
auth,
|
|
3172
|
+
data,
|
|
3173
|
+
attempted,
|
|
3174
|
+
attempt,
|
|
3175
|
+
includeRaw,
|
|
3176
|
+
exact,
|
|
3177
|
+
voucher,
|
|
3178
|
+
lookup: suppliedLookup,
|
|
3179
|
+
service
|
|
3180
|
+
}) {
|
|
3181
|
+
let lookup;
|
|
3182
|
+
try {
|
|
3183
|
+
lookup = suppliedLookup ?? await wsfe.lookupVoucher({ ...auth, ...attempted });
|
|
3184
|
+
} catch (error) {
|
|
3185
|
+
return {
|
|
3186
|
+
kind: "indeterminate",
|
|
3187
|
+
attempted,
|
|
3188
|
+
attempt,
|
|
3189
|
+
lookup: { kind: "failed", error: toArcaSafeErrorMetadata(error) }
|
|
3190
|
+
};
|
|
3191
|
+
}
|
|
3192
|
+
const raw = includeRaw ? { raw: lookup.raw } : {};
|
|
3193
|
+
if (lookup.kind === "not_found") {
|
|
3194
|
+
return {
|
|
3195
|
+
kind: "indeterminate",
|
|
3196
|
+
attempted,
|
|
3197
|
+
attempt,
|
|
3198
|
+
lookup: { kind: "not_found", ...raw }
|
|
3199
|
+
};
|
|
3200
|
+
}
|
|
3201
|
+
const detailsMatch = service === "wsmtxca" ? matchWsmtxcaDetails(data, attempted.number, lookup.voucher.raw) : void 0;
|
|
3202
|
+
const matched = detailsMatch === void 0 ? matchWsfeVoucherIdentity(data, attempted.number, lookup.voucher) : detailsMatch === "match" && lookup.voucher.cae && lookup.voucher.caeExpiry ? { matches: true } : {
|
|
3203
|
+
matches: false,
|
|
3204
|
+
evidence: detailsMatch === "conflict" ? "conflict" : "incomplete",
|
|
3205
|
+
reason: "WSMTXCA consultation does not match the complete reserved request"
|
|
3206
|
+
};
|
|
3207
|
+
if (!matched.matches) {
|
|
3208
|
+
if (matched.evidence === "conflict") {
|
|
3209
|
+
return {
|
|
3210
|
+
kind: "conflict",
|
|
3211
|
+
attempted,
|
|
3212
|
+
attempt,
|
|
3213
|
+
found: { ...toVoucherSummary(lookup.voucher), ...raw },
|
|
3214
|
+
reason: `${matched.reason}. Configure a store and pass idempotencyKey for retries.`
|
|
3215
|
+
};
|
|
3216
|
+
}
|
|
3217
|
+
return {
|
|
3218
|
+
kind: "indeterminate",
|
|
3219
|
+
attempted,
|
|
3220
|
+
attempt,
|
|
3221
|
+
lookup: { kind: "incomplete", reason: matched.reason, ...raw }
|
|
3222
|
+
};
|
|
3223
|
+
}
|
|
3224
|
+
return {
|
|
3225
|
+
kind: "authorized",
|
|
3226
|
+
recoveredByMatch: true,
|
|
3227
|
+
voucher: voucher(
|
|
3228
|
+
lookup.voucher.cae,
|
|
3229
|
+
lookup.voucher.caeExpiry
|
|
3230
|
+
),
|
|
3231
|
+
attempt,
|
|
3232
|
+
lookup: { ...toVoucherSummary(lookup.voucher), ...raw },
|
|
3233
|
+
...exact
|
|
3234
|
+
};
|
|
3235
|
+
}
|
|
3236
|
+
function projectIssue(issue) {
|
|
3237
|
+
return {
|
|
3238
|
+
service: issue.service,
|
|
3239
|
+
operation: issue.operation,
|
|
3240
|
+
source: issue.source,
|
|
3241
|
+
category: issue.category,
|
|
3242
|
+
message: issue.message,
|
|
3243
|
+
...issue.code === void 0 ? {} : { code: issue.code },
|
|
3244
|
+
...issue.resultLevel === void 0 ? {} : { resultLevel: issue.resultLevel }
|
|
3245
|
+
};
|
|
3246
|
+
}
|
|
3247
|
+
function projectEvidence(evidence, includeRaw) {
|
|
3248
|
+
const base = {
|
|
3249
|
+
kind: evidence.kind,
|
|
3250
|
+
service: evidence.service,
|
|
3251
|
+
operation: evidence.operation,
|
|
3252
|
+
results: {
|
|
3253
|
+
...evidence.results.header === void 0 ? {} : { header: evidence.results.header },
|
|
3254
|
+
...evidence.results.detail === void 0 ? {} : { detail: evidence.results.detail },
|
|
3255
|
+
...evidence.results.operation === void 0 ? {} : { operation: evidence.results.operation }
|
|
3256
|
+
},
|
|
3257
|
+
errors: evidence.errors.map(projectIssue),
|
|
3258
|
+
observations: evidence.observations.map(projectIssue),
|
|
3259
|
+
...includeRaw && evidence.raw !== void 0 ? { raw: evidence.raw } : {}
|
|
3260
|
+
};
|
|
3261
|
+
const projected = { ...base };
|
|
3262
|
+
for (const field of [
|
|
3263
|
+
"result",
|
|
3264
|
+
"resultLevel",
|
|
3265
|
+
"cae",
|
|
3266
|
+
"caeExpiry",
|
|
3267
|
+
"voucherNumber",
|
|
3268
|
+
"reason"
|
|
3269
|
+
]) {
|
|
3270
|
+
if (field in evidence && evidence[field] !== void 0) {
|
|
3271
|
+
projected[field] = evidence[field];
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
if (evidence.kind === "indeterminate" && evidence.authentication) {
|
|
3275
|
+
const { code, reason, providerCode } = evidence.authentication;
|
|
3276
|
+
projected.authentication = {
|
|
3277
|
+
code,
|
|
3278
|
+
reason,
|
|
3279
|
+
...providerCode === void 0 ? {} : { providerCode }
|
|
3280
|
+
};
|
|
3281
|
+
}
|
|
3282
|
+
return projected;
|
|
3283
|
+
}
|
|
3284
|
+
function validateOptions(options) {
|
|
3285
|
+
assertIssueObject(options, "options");
|
|
3286
|
+
assertIssueKeys(
|
|
3287
|
+
options,
|
|
3288
|
+
[
|
|
3289
|
+
"representedTaxId",
|
|
3290
|
+
"forceRefresh",
|
|
3291
|
+
"include",
|
|
3292
|
+
"idempotencyKey",
|
|
3293
|
+
"service",
|
|
3294
|
+
"number"
|
|
3295
|
+
],
|
|
3296
|
+
"options"
|
|
3297
|
+
);
|
|
3298
|
+
if (options.service !== void 0 && options.service !== "wsfe" && options.service !== "wsmtxca") {
|
|
3299
|
+
throw new ArcaInputError("Unknown fiscal service", {
|
|
3300
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
3301
|
+
field: "options.service"
|
|
3302
|
+
});
|
|
3303
|
+
}
|
|
3304
|
+
if (options.number !== void 0 && (!Number.isSafeInteger(options.number) || options.number < 1 || options.number > 99999999)) {
|
|
3305
|
+
throw new ArcaInputError("Invalid reserved number", {
|
|
3306
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
3307
|
+
field: "options.number"
|
|
3308
|
+
});
|
|
3309
|
+
}
|
|
3310
|
+
if (options.representedTaxId !== void 0) {
|
|
3311
|
+
issueDocumentNumber(
|
|
3312
|
+
options.representedTaxId,
|
|
3313
|
+
"options.representedTaxId",
|
|
3314
|
+
11,
|
|
3315
|
+
11
|
|
3316
|
+
);
|
|
3317
|
+
}
|
|
3318
|
+
if (options.forceRefresh !== void 0 && typeof options.forceRefresh !== "boolean") {
|
|
3319
|
+
throw new ArcaInputError("options.forceRefresh must be a boolean.", {
|
|
3320
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
3321
|
+
field: "options.forceRefresh"
|
|
3322
|
+
});
|
|
3323
|
+
}
|
|
3324
|
+
if (options.include !== void 0) {
|
|
3325
|
+
assertIssueObject(options.include, "options.include");
|
|
3326
|
+
assertIssueKeys(options.include, ["raw", "exactInput"], "options.include");
|
|
3327
|
+
for (const field of ["raw", "exactInput"]) {
|
|
3328
|
+
if (options.include[field] !== void 0 && typeof options.include[field] !== "boolean") {
|
|
3329
|
+
throw new ArcaInputError(
|
|
3330
|
+
`options.include.${field} must be a boolean.`,
|
|
3331
|
+
{
|
|
3332
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
3333
|
+
field: `options.include.${field}`
|
|
3334
|
+
}
|
|
3335
|
+
);
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
function replayEvidence(service = "wsfe") {
|
|
3341
|
+
return {
|
|
3342
|
+
kind: "indeterminate",
|
|
3343
|
+
service,
|
|
3344
|
+
operation: service === "wsfe" ? "FECAESolicitar" : "autorizarComprobante",
|
|
3345
|
+
reason: "incomplete_response",
|
|
3346
|
+
results: {},
|
|
3347
|
+
errors: [],
|
|
3348
|
+
observations: []
|
|
3349
|
+
};
|
|
3350
|
+
}
|
|
3351
|
+
async function issueCreditNote(wsfe, input, inputOptions, context, kind = "creditNote") {
|
|
3352
|
+
const options = structuredClone(inputOptions);
|
|
3353
|
+
validateOptions(options);
|
|
3354
|
+
validateKeyStore(options, context);
|
|
3355
|
+
assertIssueObject(input, "input");
|
|
3356
|
+
const note = "associatedPeriod" in input && !("for" in input) ? structuredClone(input) : assertCreditNoteInput(input);
|
|
3357
|
+
if (kind === "debitNote" && "all" in note && note.all) {
|
|
3358
|
+
throw new ArcaInputError("Debit notes require explicit items", {
|
|
3359
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
3360
|
+
field: "items"
|
|
3361
|
+
});
|
|
3362
|
+
}
|
|
3363
|
+
return await runOperation(
|
|
3364
|
+
wsfe,
|
|
3365
|
+
kind,
|
|
3366
|
+
note,
|
|
3367
|
+
() => prepareNote(wsfe, note, options, context, kind),
|
|
3368
|
+
options,
|
|
3369
|
+
context
|
|
3370
|
+
);
|
|
3371
|
+
}
|
|
3372
|
+
async function previewNote(wsfe, input, inputOptions, context, kind) {
|
|
3373
|
+
const options = structuredClone(inputOptions);
|
|
3374
|
+
assertIssueKeys(
|
|
3375
|
+
options,
|
|
3376
|
+
["representedTaxId", "service", "forceRefresh"],
|
|
3377
|
+
"options"
|
|
3378
|
+
);
|
|
3379
|
+
return toPreview(
|
|
3380
|
+
await prepareNote(wsfe, input, options, context, kind),
|
|
3381
|
+
options
|
|
3382
|
+
);
|
|
3383
|
+
}
|
|
3384
|
+
async function prepareNote(wsfe, input, options, context, kind) {
|
|
3385
|
+
validateOptions(options);
|
|
3386
|
+
assertIssueObject(input, "input");
|
|
3387
|
+
if ("associatedPeriod" in input && !("for" in input)) {
|
|
3388
|
+
return preparePeriodNote(input, options, kind);
|
|
3389
|
+
}
|
|
3390
|
+
const note = assertCreditNoteInput(input);
|
|
3391
|
+
if (kind === "debitNote" && note.all) {
|
|
3392
|
+
throw new ArcaInputError("Debit notes require explicit items", {
|
|
3393
|
+
code: "ARCA_INPUT_INVALID_VALUE"
|
|
3394
|
+
});
|
|
3395
|
+
}
|
|
3396
|
+
const target = note.for;
|
|
3397
|
+
const original = await wsfe.lookupVoucher({
|
|
3398
|
+
representedTaxId: options.representedTaxId,
|
|
3399
|
+
forceRefresh: options.forceRefresh,
|
|
3400
|
+
...target
|
|
3401
|
+
});
|
|
3402
|
+
if (original.kind !== "found") {
|
|
3403
|
+
throw new ArcaInputError(
|
|
3404
|
+
"issueCreditNote failed: original voucher not found",
|
|
3405
|
+
{ code: "ARCA_INPUT_INVALID_VALUE", field: "input.for" }
|
|
3406
|
+
);
|
|
3407
|
+
}
|
|
3408
|
+
if (original.voucher.salesPoint !== target.salesPoint || original.voucher.voucherType !== target.voucherType || original.voucher.voucherNumber !== target.number) {
|
|
3409
|
+
throw new ArcaInputError(
|
|
3410
|
+
"Original lookup coordinates do not match input.for",
|
|
3411
|
+
{ code: "ARCA_INPUT_INVALID_VALUE", field: "input.for" }
|
|
3412
|
+
);
|
|
3413
|
+
}
|
|
3414
|
+
const prepared = note.all === true ? deriveWsfeFullCreditNote(original.voucher, note) : deriveWsfePartialCreditNote(original.voucher, note, /* @__PURE__ */ new Date(), kind);
|
|
3415
|
+
if (note.details !== void 0) {
|
|
3416
|
+
prepared.data.details = structuredClone(note.details);
|
|
3417
|
+
} else if (note.all && "details" in original.voucher) {
|
|
3418
|
+
prepared.data.details = structuredClone(
|
|
3419
|
+
original.voucher.details
|
|
3420
|
+
);
|
|
3421
|
+
}
|
|
3422
|
+
if (voucherFamily(prepared.data.voucherType).family === "fce") {
|
|
3423
|
+
const taxId = options.representedTaxId ?? context?.taxId;
|
|
3424
|
+
if (!taxId) {
|
|
3425
|
+
throw new ArcaInputError("FCE association requires the issuer tax ID", {
|
|
3426
|
+
code: "ARCA_INPUT_MISSING_FIELD",
|
|
3427
|
+
field: "representedTaxId"
|
|
3428
|
+
});
|
|
3429
|
+
}
|
|
3430
|
+
for (const associated of prepared.data.associatedVouchers ?? []) {
|
|
3431
|
+
associated.taxId = String(taxId);
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
validatePrepared(prepared, options);
|
|
3435
|
+
return prepared;
|
|
3436
|
+
}
|
|
3437
|
+
function preparePeriodNote(input, options, kind) {
|
|
3438
|
+
const { associatedPeriod, ...invoice } = input;
|
|
3439
|
+
assertIssueObject(associatedPeriod, "associatedPeriod");
|
|
3440
|
+
assertIssueKeys(associatedPeriod, ["from", "to"], "associatedPeriod");
|
|
3441
|
+
if (normalizeWsfeDateInput(associatedPeriod.from, "associatedPeriod.from") > normalizeWsfeDateInput(associatedPeriod.to, "associatedPeriod.to")) {
|
|
3442
|
+
throw new ArcaInputError("Associated period starts after its end", {
|
|
3443
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
3444
|
+
field: "associatedPeriod"
|
|
3445
|
+
});
|
|
3446
|
+
}
|
|
3447
|
+
const prepared = prepareInvoice(invoice, options);
|
|
3448
|
+
const family = voucherFamily(prepared.data.voucherType);
|
|
3449
|
+
if (family.family === "fce") {
|
|
3450
|
+
throw new ArcaInputError("FCE notes require an associated invoice", {
|
|
3451
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
3452
|
+
field: "associatedPeriod"
|
|
3453
|
+
});
|
|
3454
|
+
}
|
|
3455
|
+
prepared.data.voucherType = family.types[kind === "creditNote" ? 2 : 1];
|
|
3456
|
+
prepared.data.associatedPeriod = {
|
|
3457
|
+
startDate: associatedPeriod.from,
|
|
3458
|
+
endDate: associatedPeriod.to
|
|
3459
|
+
};
|
|
3460
|
+
normalizeWsfeVoucherInput(prepared.data);
|
|
3461
|
+
validatePrepared(prepared, options);
|
|
3462
|
+
return prepared;
|
|
3463
|
+
}
|
|
3464
|
+
function exactEvidence(data, number, options, include) {
|
|
3465
|
+
return include ? {
|
|
3466
|
+
sent: options.service === "wsmtxca" ? wsmtxcaRequest(data, number) : data
|
|
3467
|
+
} : {};
|
|
3468
|
+
}
|
|
3469
|
+
async function recoverOperation(select, key, inputOptions, context) {
|
|
3470
|
+
const options = structuredClone(inputOptions);
|
|
3471
|
+
assertIssueObject(options, "options");
|
|
3472
|
+
assertIssueKeys(
|
|
3473
|
+
options,
|
|
3474
|
+
["representedTaxId", "forceRefresh", "include"],
|
|
3475
|
+
"options"
|
|
3476
|
+
);
|
|
3477
|
+
validateOptions(options);
|
|
3478
|
+
validateKeyStore({ ...options, idempotencyKey: key }, context);
|
|
3479
|
+
const json = await storeCall(
|
|
3480
|
+
() => context.store.get(
|
|
3481
|
+
attemptKey(
|
|
3482
|
+
context.environment,
|
|
3483
|
+
context.taxId,
|
|
3484
|
+
key
|
|
3485
|
+
)
|
|
3486
|
+
)
|
|
3487
|
+
);
|
|
3488
|
+
if (json === null) {
|
|
3489
|
+
throw new ArcaInputError("No reservation exists for this idempotency key", {
|
|
3490
|
+
code: "ARCA_INPUT_INVALID_VALUE",
|
|
3491
|
+
field: "idempotencyKey"
|
|
3492
|
+
});
|
|
3493
|
+
}
|
|
3494
|
+
const record = readRecord(json);
|
|
3495
|
+
if (options.representedTaxId !== void 0 && String(options.representedTaxId) !== (record.representedTaxId ?? context?.taxId)) {
|
|
3496
|
+
throw new ArcaInputError(
|
|
3497
|
+
"Reservation belongs to another represented taxpayer",
|
|
3498
|
+
{ code: "ARCA_INPUT_IDEMPOTENCY_MISMATCH" }
|
|
3499
|
+
);
|
|
3500
|
+
}
|
|
3501
|
+
const storedOptions = {
|
|
3502
|
+
...options,
|
|
3503
|
+
service: record.service ?? "wsfe",
|
|
3504
|
+
representedTaxId: record.representedTaxId
|
|
3505
|
+
};
|
|
3506
|
+
const prepared = preparedFromRecord(record);
|
|
3507
|
+
return recoverInvoice({
|
|
3508
|
+
wsfe: select(storedOptions),
|
|
3509
|
+
service: storedOptions.service,
|
|
3510
|
+
auth: storedOptions,
|
|
3511
|
+
data: prepared.data,
|
|
3512
|
+
attempted: {
|
|
3513
|
+
salesPoint: record.salesPoint,
|
|
3514
|
+
voucherType: record.voucherType,
|
|
3515
|
+
number: record.number
|
|
3516
|
+
},
|
|
3517
|
+
attempt: replayEvidence(storedOptions.service),
|
|
3518
|
+
includeRaw: options.include?.raw === true,
|
|
3519
|
+
exact: exactEvidence(
|
|
3520
|
+
prepared.data,
|
|
3521
|
+
record.number,
|
|
3522
|
+
storedOptions,
|
|
3523
|
+
options.include?.exactInput === true
|
|
3524
|
+
),
|
|
3525
|
+
voucher: (cae, caeExpiry) => ({
|
|
3526
|
+
salesPoint: record.salesPoint,
|
|
3527
|
+
voucherType: record.voucherType,
|
|
3528
|
+
number: record.number,
|
|
3529
|
+
voucherClass: prepared.voucherClass,
|
|
3530
|
+
date: prepared.data.voucherDate,
|
|
3531
|
+
amounts: prepared.amounts,
|
|
3532
|
+
cae,
|
|
3533
|
+
caeExpiry
|
|
3534
|
+
})
|
|
3535
|
+
});
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3538
|
+
// src/soap/index.ts
|
|
3539
|
+
function createSoapTransport(options) {
|
|
3540
|
+
return {
|
|
3541
|
+
async execute(request) {
|
|
3542
|
+
const serviceConfig = getArcaServiceConfig(request.service);
|
|
3543
|
+
const url = serviceConfig.endpoint[options.config.environment];
|
|
3544
|
+
const soapActionOperation = request.operation;
|
|
3545
|
+
const bodyElementName = request.bodyElementName ?? request.operation;
|
|
3546
|
+
const soapAction = serviceConfig.usesEmptySoapAction ? "" : `${serviceConfig.soapActionBase}${soapActionOperation}`;
|
|
3547
|
+
const contentType = serviceConfig.soapVersion === "1.2" ? `application/soap+xml; charset=utf-8; action="${soapAction}"` : 'text/xml; charset="utf-8"';
|
|
3548
|
+
const xml = buildSoapEnvelope(
|
|
3549
|
+
serviceConfig.soapVersion,
|
|
3550
|
+
bodyElementName,
|
|
3551
|
+
serviceConfig.namespace,
|
|
3552
|
+
request.body,
|
|
3553
|
+
{
|
|
3554
|
+
namespaceMode: request.bodyElementNamespaceMode
|
|
3555
|
+
}
|
|
3556
|
+
);
|
|
3557
|
+
const startedAt = Date.now();
|
|
3558
|
+
options.logger?.debug("Sending ARCA SOAP request", {
|
|
3559
|
+
service: request.service,
|
|
3560
|
+
operation: request.operation,
|
|
3561
|
+
url
|
|
3562
|
+
});
|
|
3563
|
+
try {
|
|
3564
|
+
const response = await postXmlWithMetadata({
|
|
3565
|
+
url,
|
|
3566
|
+
body: xml,
|
|
3567
|
+
contentType,
|
|
3568
|
+
soapAction: serviceConfig.soapVersion === "1.1" ? soapAction : void 0,
|
|
3569
|
+
useLegacyTlsSecurityLevel0: options.config.environment === "production" && serviceConfig.useLegacyTlsSecurityLevel0 === true,
|
|
3570
|
+
timeout: options.config.timeout,
|
|
3571
|
+
retries: request.retries ?? options.config.retries,
|
|
3572
|
+
retryDelay: options.config.retryDelay,
|
|
3573
|
+
logger: options.logger,
|
|
3574
|
+
service: request.service,
|
|
3575
|
+
operation: request.operation
|
|
3576
|
+
});
|
|
3577
|
+
options.logger?.debug("Received ARCA SOAP response", {
|
|
3578
|
+
service: request.service,
|
|
3579
|
+
operation: request.operation,
|
|
3580
|
+
durationMs: Date.now() - startedAt
|
|
3581
|
+
});
|
|
3582
|
+
const parseContext = {
|
|
3583
|
+
service: request.service,
|
|
3584
|
+
operation: request.operation,
|
|
3585
|
+
endpointUrl: url,
|
|
3586
|
+
statusCode: response.statusCode,
|
|
3587
|
+
contentType: response.contentType,
|
|
3588
|
+
responseBody: response.body
|
|
3589
|
+
};
|
|
3590
|
+
const soapBody = parseSoapBody(response.body, parseContext);
|
|
3591
|
+
const [, result] = getSingleBodyEntry(
|
|
3592
|
+
soapBody,
|
|
3593
|
+
parseContext
|
|
3594
|
+
);
|
|
3595
|
+
return {
|
|
3596
|
+
service: request.service,
|
|
3597
|
+
operation: request.operation,
|
|
3598
|
+
raw: response.body,
|
|
3599
|
+
result
|
|
3600
|
+
};
|
|
3601
|
+
} catch (error) {
|
|
3602
|
+
if (error instanceof ArcaSoapFaultError) {
|
|
3603
|
+
options.logger?.error("ARCA SOAP fault response", {
|
|
3604
|
+
service: request.service,
|
|
3605
|
+
operation: request.operation,
|
|
3606
|
+
url,
|
|
3607
|
+
...createSafeErrorDiagnostic(error)
|
|
3608
|
+
});
|
|
3609
|
+
}
|
|
3610
|
+
if (error instanceof ArcaInvalidSoapResponseError) {
|
|
3611
|
+
options.logger?.error("ARCA invalid SOAP response", {
|
|
3612
|
+
service: request.service,
|
|
3613
|
+
operation: request.operation,
|
|
3614
|
+
url,
|
|
3615
|
+
...createSafeErrorDiagnostic(error)
|
|
3616
|
+
});
|
|
3617
|
+
}
|
|
3618
|
+
throw error;
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
};
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
// src/wsaa/store-adapter.ts
|
|
3625
|
+
function createWsaaStoreAdapter(store) {
|
|
3626
|
+
const key = (value) => `arca:v1:wsaa:${serializeWsaaSessionKey(value)}`;
|
|
3627
|
+
const remove = store.delete?.bind(store);
|
|
3628
|
+
const lock = store.withLock?.bind(store);
|
|
3629
|
+
return {
|
|
3630
|
+
get: (value) => storeCall(async () => {
|
|
3631
|
+
const json = await store.get(key(value));
|
|
3632
|
+
if (json === null) {
|
|
3633
|
+
return null;
|
|
3634
|
+
}
|
|
3635
|
+
const credentials = JSON.parse(json);
|
|
3636
|
+
return credentials && typeof credentials.token === "string" && typeof credentials.sign === "string" && isWsaaCredentialValid(credentials) ? credentials : null;
|
|
3637
|
+
}),
|
|
3638
|
+
set: (value, credentials) => storeCall(() => store.set(key(value), JSON.stringify(credentials))),
|
|
3639
|
+
...remove ? {
|
|
3640
|
+
delete: (value) => storeCall(() => remove(key(value)))
|
|
3641
|
+
} : {},
|
|
3642
|
+
...lock ? {
|
|
3643
|
+
withLock: (value, fn) => lock(key(value), fn)
|
|
3644
|
+
} : {}
|
|
3645
|
+
};
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
// src/client.ts
|
|
3649
|
+
function createArcaClient(config = {}) {
|
|
3650
|
+
const discovered = discoverArcaClientConfig(config);
|
|
3651
|
+
assertArcaClientConfig(discovered);
|
|
3652
|
+
const normalizedConfig = normalizeArcaClientConfig(discovered);
|
|
3653
|
+
if (normalizedConfig.store && !normalizedConfig.wsaaSessionStore) {
|
|
3654
|
+
normalizedConfig.wsaaSessionStore = createWsaaStoreAdapter(
|
|
3655
|
+
normalizedConfig.store
|
|
3656
|
+
);
|
|
3657
|
+
}
|
|
3658
|
+
const logger = createArcaLogger(normalizedConfig.logger);
|
|
3659
|
+
const auth = createWsaaAuthModule({ config: normalizedConfig, logger });
|
|
3660
|
+
const soap = createSoapTransport({ config: normalizedConfig, logger });
|
|
3661
|
+
const publicConfig = Object.freeze({
|
|
3662
|
+
taxId: normalizedConfig.taxId,
|
|
3663
|
+
environment: normalizedConfig.environment,
|
|
3664
|
+
timeout: normalizedConfig.timeout,
|
|
3665
|
+
retries: normalizedConfig.retries,
|
|
3666
|
+
retryDelay: normalizedConfig.retryDelay
|
|
3667
|
+
});
|
|
3668
|
+
const wsfe = createWsfeService({ config: normalizedConfig, auth, soap });
|
|
3669
|
+
const wsmtxca = createWsmtxcaService({
|
|
3670
|
+
config: normalizedConfig,
|
|
3671
|
+
auth,
|
|
3672
|
+
soap
|
|
3673
|
+
});
|
|
3674
|
+
const vouchers = createVouchersService(wsfe, normalizedConfig, wsmtxca);
|
|
3675
|
+
return {
|
|
3676
|
+
config: publicConfig,
|
|
3677
|
+
issue: vouchers.issue,
|
|
3678
|
+
preview: vouchers.preview,
|
|
3679
|
+
issueCreditNote: vouchers.issueCreditNote,
|
|
3680
|
+
recover: vouchers.recover,
|
|
3681
|
+
issueDebitNote: vouchers.issueDebitNote,
|
|
3682
|
+
previewCreditNote: vouchers.previewCreditNote,
|
|
3683
|
+
previewDebitNote: vouchers.previewDebitNote,
|
|
3684
|
+
wsfe,
|
|
3685
|
+
wsmtxca,
|
|
3686
|
+
padron: createPadronService({ config: normalizedConfig, auth, soap })
|
|
3687
|
+
};
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3690
|
+
// src/store/file.ts
|
|
3691
|
+
import { createHash as createHash3, randomUUID } from "crypto";
|
|
3692
|
+
import { mkdir, readFile, rename, unlink, writeFile } from "fs/promises";
|
|
3693
|
+
import { join } from "path";
|
|
3694
|
+
function createFileStore(directory) {
|
|
3695
|
+
const path = (key) => join(directory, createHash3("sha256").update(key).digest("hex"));
|
|
3696
|
+
const ensure = () => mkdir(directory, { recursive: true, mode: 448 });
|
|
3697
|
+
return {
|
|
3698
|
+
get: (key) => storeCall(async () => {
|
|
3699
|
+
try {
|
|
3700
|
+
return await readFile(path(key), "utf8");
|
|
3701
|
+
} catch (error) {
|
|
3702
|
+
if (error.code === "ENOENT") {
|
|
3703
|
+
return null;
|
|
3704
|
+
}
|
|
3705
|
+
throw error;
|
|
3706
|
+
}
|
|
3707
|
+
}),
|
|
3708
|
+
set: (key, value) => storeCall(async () => {
|
|
3709
|
+
await ensure();
|
|
3710
|
+
const temporary = `${path(key)}.${randomUUID()}.tmp`;
|
|
3711
|
+
try {
|
|
3712
|
+
await writeFile(temporary, value, { mode: 384, flag: "wx" });
|
|
3713
|
+
await rename(temporary, path(key));
|
|
3714
|
+
} finally {
|
|
3715
|
+
await unlink(temporary).catch(() => void 0);
|
|
3716
|
+
}
|
|
3717
|
+
}),
|
|
3718
|
+
add: (key, value) => storeCall(async () => {
|
|
3719
|
+
await ensure();
|
|
3720
|
+
try {
|
|
3721
|
+
await writeFile(path(key), value, { flag: "wx", mode: 384 });
|
|
3722
|
+
return true;
|
|
3723
|
+
} catch (error) {
|
|
3724
|
+
if (error.code === "EEXIST") {
|
|
3725
|
+
return false;
|
|
3726
|
+
}
|
|
3727
|
+
throw error;
|
|
3728
|
+
}
|
|
3729
|
+
}),
|
|
3730
|
+
delete: (key) => storeCall(async () => {
|
|
3731
|
+
try {
|
|
3732
|
+
await unlink(path(key));
|
|
3733
|
+
} catch (error) {
|
|
3734
|
+
if (error.code !== "ENOENT") {
|
|
3735
|
+
throw error;
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
})
|
|
3739
|
+
};
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3742
|
+
export {
|
|
3743
|
+
ARCA_ENVIRONMENTS,
|
|
3744
|
+
ARCA_ENV_VARIABLES,
|
|
3745
|
+
resolveArcaEnvironment,
|
|
3746
|
+
createArcaClientConfigFromEnv,
|
|
3747
|
+
assertArcaClientConfig,
|
|
3748
|
+
ARCA_WSAA_CONFIG,
|
|
3749
|
+
discoverArcaClientConfig,
|
|
3750
|
+
storeCall,
|
|
3751
|
+
matchWsfeVoucherIdentity,
|
|
3752
|
+
createMemoryWsaaSessionStore,
|
|
3753
|
+
createWsaaAuthModule,
|
|
3754
|
+
createWsaaStoreAdapter,
|
|
3755
|
+
createArcaClient,
|
|
3756
|
+
createFileStore
|
|
3757
|
+
};
|
|
3758
|
+
//# sourceMappingURL=chunk-LOWV7IF6.mjs.map
|