@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.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
- store = /* @__PURE__ */ new Map();
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
- return this.store.get(key);
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.store.set(key, value);
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.store.delete(key);
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 encf = extractElementText(signedXml, "eNCF") ?? "unknown-encf";
1003
- return `${rnc}:${encf}:${sha256Hex(signedXml)}`;
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
- const cached = await this.idempotency.get(key);
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
- const result = await this.http.postMultipart(
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
- const cached = await this.idempotency.get(key);
1109
- if (cached !== void 0) {
1110
- if (isPendingSubmission(cached)) {
1111
- return err(
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
- if (result.isOk()) {
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 url = buildUrl(this.environment, "aprobacionComercial/api/AprobacionComercial" /* CommercialApproval */);
1197
- return this.http.postMultipart(
1198
- url,
1199
- fileName,
1200
- signedXml,
1201
- { absoluteUrl: true, resource: "aprobacionComercial/api/AprobacionComercial" /* CommercialApproval */ }
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 url = buildUrl(this.environment, "anulacionrangos/api/operaciones/anularrango" /* Void */);
1206
- return this.http.postMultipart(url, fileName, signedXml, {
1207
- absoluteUrl: true,
1208
- resource: "anulacionrangos/api/operaciones/anularrango" /* Void */
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 {