@deuriib/dgii-ecf 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +107 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -3
- package/dist/index.d.ts +38 -3
- package/dist/index.js +107 -63
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -370,16 +370,51 @@ var createResiliencePolicies = (options = {}) => {
|
|
|
370
370
|
};
|
|
371
371
|
|
|
372
372
|
// src/http.ts
|
|
373
|
+
var DEFAULT_IDEMPOTENCY_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
374
|
+
var IDEMPOTENCY_SWEEP_THRESHOLD = 1e3;
|
|
373
375
|
var InMemoryIdempotencyStore = class {
|
|
374
|
-
|
|
376
|
+
entries = /* @__PURE__ */ new Map();
|
|
377
|
+
ttlMs;
|
|
378
|
+
now;
|
|
379
|
+
constructor(options = {}) {
|
|
380
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_IDEMPOTENCY_TTL_MS;
|
|
381
|
+
if (!Number.isFinite(this.ttlMs) || this.ttlMs <= 0) {
|
|
382
|
+
throw new RangeError("ttlMs must be a positive finite number");
|
|
383
|
+
}
|
|
384
|
+
this.now = options.now ?? Date.now;
|
|
385
|
+
}
|
|
386
|
+
/** Number of live entries; expired ones are swept first. */
|
|
387
|
+
get size() {
|
|
388
|
+
this.sweepExpired();
|
|
389
|
+
return this.entries.size;
|
|
390
|
+
}
|
|
375
391
|
async get(key) {
|
|
376
|
-
|
|
392
|
+
const entry = this.entries.get(key);
|
|
393
|
+
if (entry === void 0) {
|
|
394
|
+
return void 0;
|
|
395
|
+
}
|
|
396
|
+
if (entry.expiresAt <= this.now()) {
|
|
397
|
+
this.entries.delete(key);
|
|
398
|
+
return void 0;
|
|
399
|
+
}
|
|
400
|
+
return entry.value;
|
|
377
401
|
}
|
|
378
402
|
async set(key, value) {
|
|
379
|
-
this.
|
|
403
|
+
this.entries.set(key, { value, expiresAt: this.now() + this.ttlMs });
|
|
404
|
+
if (this.entries.size > IDEMPOTENCY_SWEEP_THRESHOLD) {
|
|
405
|
+
this.sweepExpired();
|
|
406
|
+
}
|
|
380
407
|
}
|
|
381
408
|
async delete(key) {
|
|
382
|
-
this.
|
|
409
|
+
this.entries.delete(key);
|
|
410
|
+
}
|
|
411
|
+
sweepExpired() {
|
|
412
|
+
const now = this.now();
|
|
413
|
+
for (const [key, entry] of this.entries) {
|
|
414
|
+
if (entry.expiresAt <= now) {
|
|
415
|
+
this.entries.delete(key);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
383
418
|
}
|
|
384
419
|
};
|
|
385
420
|
var HttpClient = class {
|
|
@@ -1051,8 +1086,39 @@ var ECF = class {
|
|
|
1051
1086
|
}
|
|
1052
1087
|
buildIdempotencyKey(signedXml) {
|
|
1053
1088
|
const rnc = extractElementText(signedXml, "RNCEmisor") ?? "unknown-rnc";
|
|
1054
|
-
const
|
|
1055
|
-
return `${rnc}:${
|
|
1089
|
+
const ncf = extractElementText(signedXml, "eNCF") ?? extractElementText(signedXml, "RangoNCF") ?? "unknown-ncf";
|
|
1090
|
+
return `${rnc}:${ncf}:${sha256Hex(signedXml)}`;
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Run a submission through the persist-before-transmit idempotency flow.
|
|
1094
|
+
*
|
|
1095
|
+
* The pending marker is written first so an interrupted send can be
|
|
1096
|
+
* reconciled instead of re-issued; a successful response replaces it, and
|
|
1097
|
+
* a failure clears it so the caller can retry.
|
|
1098
|
+
*/
|
|
1099
|
+
async submitIdempotent(key, send) {
|
|
1100
|
+
const cached = await this.idempotency.get(key);
|
|
1101
|
+
if (cached !== void 0) {
|
|
1102
|
+
if (isPendingSubmission(cached)) {
|
|
1103
|
+
return (0, import_neverthrow.err)(
|
|
1104
|
+
new DgiiError(
|
|
1105
|
+
"A submission with this key is already pending; reconcile via a status lookup before retrying",
|
|
1106
|
+
{ code: "IDEMPOTENCY" }
|
|
1107
|
+
)
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
return (0, import_neverthrow.ok)(cached);
|
|
1111
|
+
}
|
|
1112
|
+
return this.singleFlight(key, async () => {
|
|
1113
|
+
await this.idempotency.set(key, PENDING_SUBMISSION);
|
|
1114
|
+
const result = await send();
|
|
1115
|
+
if (result.isOk()) {
|
|
1116
|
+
await this.idempotency.set(key, result.value);
|
|
1117
|
+
} else {
|
|
1118
|
+
await this.idempotency.delete(key);
|
|
1119
|
+
}
|
|
1120
|
+
return result;
|
|
1121
|
+
});
|
|
1056
1122
|
}
|
|
1057
1123
|
async singleFlight(key, task) {
|
|
1058
1124
|
const existing = this.inFlight.get(key);
|
|
@@ -1125,66 +1191,28 @@ var ECF = class {
|
|
|
1125
1191
|
*/
|
|
1126
1192
|
async sendElectronicDocument(signedXml, fileName, buyerHost) {
|
|
1127
1193
|
const key = this.buildIdempotencyKey(signedXml);
|
|
1128
|
-
|
|
1129
|
-
if (cached !== void 0) {
|
|
1130
|
-
if (isPendingSubmission(cached)) {
|
|
1131
|
-
return (0, import_neverthrow.err)(
|
|
1132
|
-
new DgiiError(
|
|
1133
|
-
"A submission with this key is already pending; reconcile via a status lookup before retrying",
|
|
1134
|
-
{ code: "IDEMPOTENCY" }
|
|
1135
|
-
)
|
|
1136
|
-
);
|
|
1137
|
-
}
|
|
1138
|
-
return (0, import_neverthrow.ok)(cached);
|
|
1139
|
-
}
|
|
1140
|
-
return this.singleFlight(key, async () => {
|
|
1141
|
-
await this.idempotency.set(key, PENDING_SUBMISSION);
|
|
1194
|
+
return this.submitIdempotent(key, () => {
|
|
1142
1195
|
const url = buyerHost ? buildSenderReceiverUrl(buyerHost, "fe/recepcion/api/ecf" /* SendInvoice */) : buildUrl(this.environment, "recepcion/api/FacturasElectronicas" /* SendInvoice */);
|
|
1143
|
-
|
|
1196
|
+
return this.http.postMultipart(
|
|
1144
1197
|
url,
|
|
1145
1198
|
fileName,
|
|
1146
1199
|
signedXml,
|
|
1147
1200
|
{ absoluteUrl: true, resource: "recepcion/api/FacturasElectronicas" /* SendInvoice */ }
|
|
1148
1201
|
);
|
|
1149
|
-
if (result.isOk()) {
|
|
1150
|
-
await this.idempotency.set(key, result.value);
|
|
1151
|
-
} else {
|
|
1152
|
-
await this.idempotency.delete(key);
|
|
1153
|
-
}
|
|
1154
|
-
return result;
|
|
1155
1202
|
});
|
|
1156
1203
|
}
|
|
1157
1204
|
/** Submit a signed summary invoice (RFCE). Idempotent like submissions. */
|
|
1158
1205
|
async sendSummary(signedXml, fileName) {
|
|
1159
1206
|
const key = this.buildIdempotencyKey(signedXml);
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
new DgiiError(
|
|
1165
|
-
"A submission with this key is already pending; reconcile via a status lookup before retrying",
|
|
1166
|
-
{ code: "IDEMPOTENCY" }
|
|
1167
|
-
)
|
|
1168
|
-
);
|
|
1169
|
-
}
|
|
1170
|
-
return (0, import_neverthrow.ok)(cached);
|
|
1171
|
-
}
|
|
1172
|
-
return this.singleFlight(key, async () => {
|
|
1173
|
-
await this.idempotency.set(key, PENDING_SUBMISSION);
|
|
1174
|
-
const url = buildUrl(this.environment, "recepcionfc/api/recepcion/ecf" /* SendSummary */, BaseUrl.CF);
|
|
1175
|
-
const result = await this.http.postMultipart(
|
|
1176
|
-
url,
|
|
1207
|
+
return this.submitIdempotent(
|
|
1208
|
+
key,
|
|
1209
|
+
() => this.http.postMultipart(
|
|
1210
|
+
buildUrl(this.environment, "recepcionfc/api/recepcion/ecf" /* SendSummary */, BaseUrl.CF),
|
|
1177
1211
|
fileName,
|
|
1178
1212
|
signedXml,
|
|
1179
1213
|
{ absoluteUrl: true, resource: "recepcionfc/api/recepcion/ecf" /* SendSummary */ }
|
|
1180
|
-
)
|
|
1181
|
-
|
|
1182
|
-
await this.idempotency.set(key, result.value);
|
|
1183
|
-
} else {
|
|
1184
|
-
await this.idempotency.delete(key);
|
|
1185
|
-
}
|
|
1186
|
-
return result;
|
|
1187
|
-
});
|
|
1214
|
+
)
|
|
1215
|
+
);
|
|
1188
1216
|
}
|
|
1189
1217
|
statusTrackId(trackId) {
|
|
1190
1218
|
const url = buildUrl(this.environment, "consultaresultado/api/Consultas/Estado" /* TrackResultStatus */);
|
|
@@ -1244,21 +1272,37 @@ var ECF = class {
|
|
|
1244
1272
|
{ absoluteUrl: true, resource: "consultarfce/api/Consultas/Consulta" /* InquiryInvoiceSummary */ }
|
|
1245
1273
|
);
|
|
1246
1274
|
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Submit a signed commercial approval (ACECF). Idempotent like submissions,
|
|
1277
|
+
* keyed on `RNC + eNCF + SHA-256(signed XML)`.
|
|
1278
|
+
*/
|
|
1247
1279
|
sendCommercialApproval(signedXml, fileName) {
|
|
1248
|
-
const
|
|
1249
|
-
return this.
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1280
|
+
const key = this.buildIdempotencyKey(signedXml);
|
|
1281
|
+
return this.submitIdempotent(
|
|
1282
|
+
key,
|
|
1283
|
+
() => this.http.postMultipart(
|
|
1284
|
+
buildUrl(this.environment, "aprobacionComercial/api/AprobacionComercial" /* CommercialApproval */),
|
|
1285
|
+
fileName,
|
|
1286
|
+
signedXml,
|
|
1287
|
+
{ absoluteUrl: true, resource: "aprobacionComercial/api/AprobacionComercial" /* CommercialApproval */ }
|
|
1288
|
+
)
|
|
1254
1289
|
);
|
|
1255
1290
|
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Submit a signed range-voiding request (ANECF). Idempotent like
|
|
1293
|
+
* submissions, keyed on `RNC + RangoNCF + SHA-256(signed XML)`.
|
|
1294
|
+
*/
|
|
1256
1295
|
voidENCF(signedXml, fileName) {
|
|
1257
|
-
const
|
|
1258
|
-
return this.
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1296
|
+
const key = this.buildIdempotencyKey(signedXml);
|
|
1297
|
+
return this.submitIdempotent(
|
|
1298
|
+
key,
|
|
1299
|
+
() => this.http.postMultipart(
|
|
1300
|
+
buildUrl(this.environment, "anulacionrangos/api/operaciones/anularrango" /* Void */),
|
|
1301
|
+
fileName,
|
|
1302
|
+
signedXml,
|
|
1303
|
+
{ absoluteUrl: true, resource: "anulacionrangos/api/operaciones/anularrango" /* Void */ }
|
|
1304
|
+
)
|
|
1305
|
+
);
|
|
1262
1306
|
}
|
|
1263
1307
|
};
|
|
1264
1308
|
// Annotate the CommonJS export names for ESM import in node:
|