@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.js
CHANGED
|
@@ -318,16 +318,51 @@ var createResiliencePolicies = (options = {}) => {
|
|
|
318
318
|
};
|
|
319
319
|
|
|
320
320
|
// src/http.ts
|
|
321
|
+
var DEFAULT_IDEMPOTENCY_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
322
|
+
var IDEMPOTENCY_SWEEP_THRESHOLD = 1e3;
|
|
321
323
|
var InMemoryIdempotencyStore = class {
|
|
322
|
-
|
|
324
|
+
entries = /* @__PURE__ */ new Map();
|
|
325
|
+
ttlMs;
|
|
326
|
+
now;
|
|
327
|
+
constructor(options = {}) {
|
|
328
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_IDEMPOTENCY_TTL_MS;
|
|
329
|
+
if (!Number.isFinite(this.ttlMs) || this.ttlMs <= 0) {
|
|
330
|
+
throw new RangeError("ttlMs must be a positive finite number");
|
|
331
|
+
}
|
|
332
|
+
this.now = options.now ?? Date.now;
|
|
333
|
+
}
|
|
334
|
+
/** Number of live entries; expired ones are swept first. */
|
|
335
|
+
get size() {
|
|
336
|
+
this.sweepExpired();
|
|
337
|
+
return this.entries.size;
|
|
338
|
+
}
|
|
323
339
|
async get(key) {
|
|
324
|
-
|
|
340
|
+
const entry = this.entries.get(key);
|
|
341
|
+
if (entry === void 0) {
|
|
342
|
+
return void 0;
|
|
343
|
+
}
|
|
344
|
+
if (entry.expiresAt <= this.now()) {
|
|
345
|
+
this.entries.delete(key);
|
|
346
|
+
return void 0;
|
|
347
|
+
}
|
|
348
|
+
return entry.value;
|
|
325
349
|
}
|
|
326
350
|
async set(key, value) {
|
|
327
|
-
this.
|
|
351
|
+
this.entries.set(key, { value, expiresAt: this.now() + this.ttlMs });
|
|
352
|
+
if (this.entries.size > IDEMPOTENCY_SWEEP_THRESHOLD) {
|
|
353
|
+
this.sweepExpired();
|
|
354
|
+
}
|
|
328
355
|
}
|
|
329
356
|
async delete(key) {
|
|
330
|
-
this.
|
|
357
|
+
this.entries.delete(key);
|
|
358
|
+
}
|
|
359
|
+
sweepExpired() {
|
|
360
|
+
const now = this.now();
|
|
361
|
+
for (const [key, entry] of this.entries) {
|
|
362
|
+
if (entry.expiresAt <= now) {
|
|
363
|
+
this.entries.delete(key);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
331
366
|
}
|
|
332
367
|
};
|
|
333
368
|
var HttpClient = class {
|
|
@@ -999,8 +1034,39 @@ var ECF = class {
|
|
|
999
1034
|
}
|
|
1000
1035
|
buildIdempotencyKey(signedXml) {
|
|
1001
1036
|
const rnc = extractElementText(signedXml, "RNCEmisor") ?? "unknown-rnc";
|
|
1002
|
-
const
|
|
1003
|
-
return `${rnc}:${
|
|
1037
|
+
const ncf = extractElementText(signedXml, "eNCF") ?? extractElementText(signedXml, "RangoNCF") ?? "unknown-ncf";
|
|
1038
|
+
return `${rnc}:${ncf}:${sha256Hex(signedXml)}`;
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Run a submission through the persist-before-transmit idempotency flow.
|
|
1042
|
+
*
|
|
1043
|
+
* The pending marker is written first so an interrupted send can be
|
|
1044
|
+
* reconciled instead of re-issued; a successful response replaces it, and
|
|
1045
|
+
* a failure clears it so the caller can retry.
|
|
1046
|
+
*/
|
|
1047
|
+
async submitIdempotent(key, send) {
|
|
1048
|
+
const cached = await this.idempotency.get(key);
|
|
1049
|
+
if (cached !== void 0) {
|
|
1050
|
+
if (isPendingSubmission(cached)) {
|
|
1051
|
+
return err(
|
|
1052
|
+
new DgiiError(
|
|
1053
|
+
"A submission with this key is already pending; reconcile via a status lookup before retrying",
|
|
1054
|
+
{ code: "IDEMPOTENCY" }
|
|
1055
|
+
)
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
return ok(cached);
|
|
1059
|
+
}
|
|
1060
|
+
return this.singleFlight(key, async () => {
|
|
1061
|
+
await this.idempotency.set(key, PENDING_SUBMISSION);
|
|
1062
|
+
const result = await send();
|
|
1063
|
+
if (result.isOk()) {
|
|
1064
|
+
await this.idempotency.set(key, result.value);
|
|
1065
|
+
} else {
|
|
1066
|
+
await this.idempotency.delete(key);
|
|
1067
|
+
}
|
|
1068
|
+
return result;
|
|
1069
|
+
});
|
|
1004
1070
|
}
|
|
1005
1071
|
async singleFlight(key, task) {
|
|
1006
1072
|
const existing = this.inFlight.get(key);
|
|
@@ -1073,66 +1139,28 @@ var ECF = class {
|
|
|
1073
1139
|
*/
|
|
1074
1140
|
async sendElectronicDocument(signedXml, fileName, buyerHost) {
|
|
1075
1141
|
const key = this.buildIdempotencyKey(signedXml);
|
|
1076
|
-
|
|
1077
|
-
if (cached !== void 0) {
|
|
1078
|
-
if (isPendingSubmission(cached)) {
|
|
1079
|
-
return err(
|
|
1080
|
-
new DgiiError(
|
|
1081
|
-
"A submission with this key is already pending; reconcile via a status lookup before retrying",
|
|
1082
|
-
{ code: "IDEMPOTENCY" }
|
|
1083
|
-
)
|
|
1084
|
-
);
|
|
1085
|
-
}
|
|
1086
|
-
return ok(cached);
|
|
1087
|
-
}
|
|
1088
|
-
return this.singleFlight(key, async () => {
|
|
1089
|
-
await this.idempotency.set(key, PENDING_SUBMISSION);
|
|
1142
|
+
return this.submitIdempotent(key, () => {
|
|
1090
1143
|
const url = buyerHost ? buildSenderReceiverUrl(buyerHost, "fe/recepcion/api/ecf" /* SendInvoice */) : buildUrl(this.environment, "recepcion/api/FacturasElectronicas" /* SendInvoice */);
|
|
1091
|
-
|
|
1144
|
+
return this.http.postMultipart(
|
|
1092
1145
|
url,
|
|
1093
1146
|
fileName,
|
|
1094
1147
|
signedXml,
|
|
1095
1148
|
{ absoluteUrl: true, resource: "recepcion/api/FacturasElectronicas" /* SendInvoice */ }
|
|
1096
1149
|
);
|
|
1097
|
-
if (result.isOk()) {
|
|
1098
|
-
await this.idempotency.set(key, result.value);
|
|
1099
|
-
} else {
|
|
1100
|
-
await this.idempotency.delete(key);
|
|
1101
|
-
}
|
|
1102
|
-
return result;
|
|
1103
1150
|
});
|
|
1104
1151
|
}
|
|
1105
1152
|
/** Submit a signed summary invoice (RFCE). Idempotent like submissions. */
|
|
1106
1153
|
async sendSummary(signedXml, fileName) {
|
|
1107
1154
|
const key = this.buildIdempotencyKey(signedXml);
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
new DgiiError(
|
|
1113
|
-
"A submission with this key is already pending; reconcile via a status lookup before retrying",
|
|
1114
|
-
{ code: "IDEMPOTENCY" }
|
|
1115
|
-
)
|
|
1116
|
-
);
|
|
1117
|
-
}
|
|
1118
|
-
return ok(cached);
|
|
1119
|
-
}
|
|
1120
|
-
return this.singleFlight(key, async () => {
|
|
1121
|
-
await this.idempotency.set(key, PENDING_SUBMISSION);
|
|
1122
|
-
const url = buildUrl(this.environment, "recepcionfc/api/recepcion/ecf" /* SendSummary */, BaseUrl.CF);
|
|
1123
|
-
const result = await this.http.postMultipart(
|
|
1124
|
-
url,
|
|
1155
|
+
return this.submitIdempotent(
|
|
1156
|
+
key,
|
|
1157
|
+
() => this.http.postMultipart(
|
|
1158
|
+
buildUrl(this.environment, "recepcionfc/api/recepcion/ecf" /* SendSummary */, BaseUrl.CF),
|
|
1125
1159
|
fileName,
|
|
1126
1160
|
signedXml,
|
|
1127
1161
|
{ absoluteUrl: true, resource: "recepcionfc/api/recepcion/ecf" /* SendSummary */ }
|
|
1128
|
-
)
|
|
1129
|
-
|
|
1130
|
-
await this.idempotency.set(key, result.value);
|
|
1131
|
-
} else {
|
|
1132
|
-
await this.idempotency.delete(key);
|
|
1133
|
-
}
|
|
1134
|
-
return result;
|
|
1135
|
-
});
|
|
1162
|
+
)
|
|
1163
|
+
);
|
|
1136
1164
|
}
|
|
1137
1165
|
statusTrackId(trackId) {
|
|
1138
1166
|
const url = buildUrl(this.environment, "consultaresultado/api/Consultas/Estado" /* TrackResultStatus */);
|
|
@@ -1192,21 +1220,37 @@ var ECF = class {
|
|
|
1192
1220
|
{ absoluteUrl: true, resource: "consultarfce/api/Consultas/Consulta" /* InquiryInvoiceSummary */ }
|
|
1193
1221
|
);
|
|
1194
1222
|
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Submit a signed commercial approval (ACECF). Idempotent like submissions,
|
|
1225
|
+
* keyed on `RNC + eNCF + SHA-256(signed XML)`.
|
|
1226
|
+
*/
|
|
1195
1227
|
sendCommercialApproval(signedXml, fileName) {
|
|
1196
|
-
const
|
|
1197
|
-
return this.
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1228
|
+
const key = this.buildIdempotencyKey(signedXml);
|
|
1229
|
+
return this.submitIdempotent(
|
|
1230
|
+
key,
|
|
1231
|
+
() => this.http.postMultipart(
|
|
1232
|
+
buildUrl(this.environment, "aprobacionComercial/api/AprobacionComercial" /* CommercialApproval */),
|
|
1233
|
+
fileName,
|
|
1234
|
+
signedXml,
|
|
1235
|
+
{ absoluteUrl: true, resource: "aprobacionComercial/api/AprobacionComercial" /* CommercialApproval */ }
|
|
1236
|
+
)
|
|
1202
1237
|
);
|
|
1203
1238
|
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Submit a signed range-voiding request (ANECF). Idempotent like
|
|
1241
|
+
* submissions, keyed on `RNC + RangoNCF + SHA-256(signed XML)`.
|
|
1242
|
+
*/
|
|
1204
1243
|
voidENCF(signedXml, fileName) {
|
|
1205
|
-
const
|
|
1206
|
-
return this.
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1244
|
+
const key = this.buildIdempotencyKey(signedXml);
|
|
1245
|
+
return this.submitIdempotent(
|
|
1246
|
+
key,
|
|
1247
|
+
() => this.http.postMultipart(
|
|
1248
|
+
buildUrl(this.environment, "anulacionrangos/api/operaciones/anularrango" /* Void */),
|
|
1249
|
+
fileName,
|
|
1250
|
+
signedXml,
|
|
1251
|
+
{ absoluteUrl: true, resource: "anulacionrangos/api/operaciones/anularrango" /* Void */ }
|
|
1252
|
+
)
|
|
1253
|
+
);
|
|
1210
1254
|
}
|
|
1211
1255
|
};
|
|
1212
1256
|
export {
|