@deuriib/dgii-ecf 0.4.1 → 0.5.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 +211 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +83 -1
- package/dist/index.d.ts +83 -1
- package/dist/index.js +206 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -31,8 +31,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
BaseUrl: () => BaseUrl,
|
|
34
|
+
CONSUMPTION_SUMMARY_THRESHOLD: () => CONSUMPTION_SUMMARY_THRESHOLD,
|
|
34
35
|
DgiiError: () => DgiiError,
|
|
35
36
|
ECF: () => ECF,
|
|
37
|
+
ECF_TYPE_CODES: () => ECF_TYPE_CODES,
|
|
36
38
|
Endpoint: () => Endpoint,
|
|
37
39
|
Environment: () => Environment,
|
|
38
40
|
Err: () => import_neverthrow.Err,
|
|
@@ -47,6 +49,7 @@ __export(index_exports, {
|
|
|
47
49
|
Transformer: () => Transformer,
|
|
48
50
|
buildSenderReceiverUrl: () => buildSenderReceiverUrl,
|
|
49
51
|
buildUrl: () => buildUrl,
|
|
52
|
+
compareDecimal: () => compareDecimal,
|
|
50
53
|
convertECF32ToRFCE: () => convertECF32ToRFCE,
|
|
51
54
|
createResiliencePolicies: () => createResiliencePolicies,
|
|
52
55
|
defaultResilienceOptions: () => defaultResilienceOptions,
|
|
@@ -62,10 +65,12 @@ __export(index_exports, {
|
|
|
62
65
|
getCurrentFormattedDate: () => getCurrentFormattedDate,
|
|
63
66
|
getCurrentFormattedDateTime: () => getCurrentFormattedDateTime,
|
|
64
67
|
getXmlFromBodyResponse: () => getXmlFromBodyResponse,
|
|
68
|
+
isEcfTypeCode: () => isEcfTypeCode,
|
|
65
69
|
isRetryableError: () => isRetryableError,
|
|
66
70
|
isRetryableStatus: () => isRetryableStatus,
|
|
67
71
|
ok: () => import_neverthrow.ok,
|
|
68
72
|
okAsync: () => import_neverthrow.okAsync,
|
|
73
|
+
resolveReceptionRoute: () => resolveReceptionRoute,
|
|
69
74
|
roundHalfUpToCents: () => roundHalfUpToCents,
|
|
70
75
|
toDgiiError: () => toDgiiError,
|
|
71
76
|
validateXMLCertificate: () => validateXMLCertificate
|
|
@@ -422,9 +427,13 @@ var HttpClient = class {
|
|
|
422
427
|
policies;
|
|
423
428
|
fetchImpl;
|
|
424
429
|
baseUrl;
|
|
430
|
+
beforeRequest;
|
|
431
|
+
onUnauthorized;
|
|
425
432
|
constructor(options) {
|
|
426
433
|
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
427
434
|
this.token = options.token;
|
|
435
|
+
this.beforeRequest = options.beforeRequest;
|
|
436
|
+
this.onUnauthorized = options.onUnauthorized;
|
|
428
437
|
this.policies = options.policies ?? createResiliencePolicies({
|
|
429
438
|
...options.resilience,
|
|
430
439
|
...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {}
|
|
@@ -434,6 +443,9 @@ var HttpClient = class {
|
|
|
434
443
|
setToken(token) {
|
|
435
444
|
this.token = token;
|
|
436
445
|
}
|
|
446
|
+
getToken() {
|
|
447
|
+
return this.token;
|
|
448
|
+
}
|
|
437
449
|
clearToken() {
|
|
438
450
|
this.token = void 0;
|
|
439
451
|
}
|
|
@@ -493,6 +505,19 @@ var HttpClient = class {
|
|
|
493
505
|
return (0, import_neverthrow.err)(error);
|
|
494
506
|
}
|
|
495
507
|
async request(options) {
|
|
508
|
+
if (!options.skipAuthHooks) {
|
|
509
|
+
await this.beforeRequest?.();
|
|
510
|
+
}
|
|
511
|
+
const first = await this.execute(options);
|
|
512
|
+
if (!options.skipAuthHooks && first.isErr() && first.error.status === 401 && this.onUnauthorized !== void 0) {
|
|
513
|
+
const replayed = await this.onUnauthorized();
|
|
514
|
+
if (replayed) {
|
|
515
|
+
return this.execute(options);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return first;
|
|
519
|
+
}
|
|
520
|
+
async execute(options) {
|
|
496
521
|
const url = this.buildUrl(options.path, options.absoluteUrl, options.query);
|
|
497
522
|
const method = options.method ?? "GET";
|
|
498
523
|
const headers = this.authHeaders(options.headers);
|
|
@@ -521,7 +546,8 @@ var HttpClient = class {
|
|
|
521
546
|
path,
|
|
522
547
|
query,
|
|
523
548
|
absoluteUrl: opts?.absoluteUrl,
|
|
524
|
-
resource: opts?.resource
|
|
549
|
+
resource: opts?.resource,
|
|
550
|
+
skipAuthHooks: opts?.skipAuthHooks
|
|
525
551
|
});
|
|
526
552
|
}
|
|
527
553
|
/**
|
|
@@ -537,7 +563,8 @@ var HttpClient = class {
|
|
|
537
563
|
body: form,
|
|
538
564
|
headers: { Accept: "application/json" },
|
|
539
565
|
absoluteUrl: opts?.absoluteUrl,
|
|
540
|
-
resource: opts?.resource
|
|
566
|
+
resource: opts?.resource,
|
|
567
|
+
skipAuthHooks: opts?.skipAuthHooks
|
|
541
568
|
});
|
|
542
569
|
}
|
|
543
570
|
};
|
|
@@ -885,6 +912,98 @@ var extractElementText = (xml, tag) => {
|
|
|
885
912
|
const text = match?.[1]?.trim();
|
|
886
913
|
return text === void 0 || text === "" ? void 0 : text;
|
|
887
914
|
};
|
|
915
|
+
var ROOT_PATTERN = /<\s*(?:[A-Za-z_][\w.-]*:)?([A-Za-z_][\w.-]*)[\s/>]/;
|
|
916
|
+
var extractRootElementName = (xml) => {
|
|
917
|
+
const stripped = xml.replace(/<\?[\s\S]*?\?>/g, "").replace(/<!--[\s\S]*?-->/g, "").trimStart();
|
|
918
|
+
return ROOT_PATTERN.exec(stripped)?.[1];
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
// src/reception.ts
|
|
922
|
+
var ECF_TYPE_CODES = [
|
|
923
|
+
"31",
|
|
924
|
+
"32",
|
|
925
|
+
"33",
|
|
926
|
+
"34",
|
|
927
|
+
"41",
|
|
928
|
+
"43",
|
|
929
|
+
"44",
|
|
930
|
+
"45",
|
|
931
|
+
"46",
|
|
932
|
+
"47"
|
|
933
|
+
];
|
|
934
|
+
var CONSUMPTION_SUMMARY_THRESHOLD = "250000";
|
|
935
|
+
var CONSUMPTION_TYPE = "32";
|
|
936
|
+
var GENERAL = {
|
|
937
|
+
kind: "general",
|
|
938
|
+
endpoint: "recepcion/api/FacturasElectronicas" /* SendInvoice */
|
|
939
|
+
};
|
|
940
|
+
var SUMMARY = {
|
|
941
|
+
kind: "summary",
|
|
942
|
+
endpoint: "recepcionfc/api/recepcion/ecf" /* SendSummary */
|
|
943
|
+
};
|
|
944
|
+
var isEcfTypeCode = (value) => ECF_TYPE_CODES.includes(value);
|
|
945
|
+
var DECIMAL_PATTERN = /^\d+(?:\.\d+)?$/;
|
|
946
|
+
var compareDecimal = (a, b) => {
|
|
947
|
+
const [aInt = "", aFrac = ""] = a.split(".");
|
|
948
|
+
const [bInt = "", bFrac = ""] = b.split(".");
|
|
949
|
+
const aWhole = aInt.replace(/^0+(?=\d)/, "");
|
|
950
|
+
const bWhole = bInt.replace(/^0+(?=\d)/, "");
|
|
951
|
+
if (aWhole.length !== bWhole.length) {
|
|
952
|
+
return aWhole.length - bWhole.length;
|
|
953
|
+
}
|
|
954
|
+
if (aWhole !== bWhole) {
|
|
955
|
+
return aWhole < bWhole ? -1 : 1;
|
|
956
|
+
}
|
|
957
|
+
const width = Math.max(aFrac.length, bFrac.length);
|
|
958
|
+
const aPadded = aFrac.padEnd(width, "0");
|
|
959
|
+
const bPadded = bFrac.padEnd(width, "0");
|
|
960
|
+
if (aPadded === bPadded) {
|
|
961
|
+
return 0;
|
|
962
|
+
}
|
|
963
|
+
return aPadded < bPadded ? -1 : 1;
|
|
964
|
+
};
|
|
965
|
+
var validationError = (message) => new DgiiError(message, { code: "VALIDATION" });
|
|
966
|
+
var resolveReceptionRoute = (signedXml) => {
|
|
967
|
+
const root = extractRootElementName(signedXml);
|
|
968
|
+
if (root === "RFCE") {
|
|
969
|
+
return (0, import_neverthrow.ok)(SUMMARY);
|
|
970
|
+
}
|
|
971
|
+
if (root !== "ECF") {
|
|
972
|
+
return (0, import_neverthrow.err)(
|
|
973
|
+
validationError(
|
|
974
|
+
`Unsupported document root "${root ?? "unknown"}"; expected "ECF" or "RFCE"`
|
|
975
|
+
)
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
const tipo = extractElementText(signedXml, "TipoeCF");
|
|
979
|
+
if (tipo === void 0) {
|
|
980
|
+
return (0, import_neverthrow.err)(validationError("TipoeCF is missing from the document"));
|
|
981
|
+
}
|
|
982
|
+
if (!isEcfTypeCode(tipo)) {
|
|
983
|
+
return (0, import_neverthrow.err)(validationError(`Unknown TipoeCF "${tipo}"`));
|
|
984
|
+
}
|
|
985
|
+
if (tipo !== CONSUMPTION_TYPE) {
|
|
986
|
+
return (0, import_neverthrow.ok)(GENERAL);
|
|
987
|
+
}
|
|
988
|
+
const total = extractElementText(signedXml, "MontoTotal");
|
|
989
|
+
if (total === void 0) {
|
|
990
|
+
return (0, import_neverthrow.err)(
|
|
991
|
+
validationError(
|
|
992
|
+
"MontoTotal is required to route an E32 consumption invoice"
|
|
993
|
+
)
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
if (!DECIMAL_PATTERN.test(total)) {
|
|
997
|
+
return (0, import_neverthrow.err)(
|
|
998
|
+
validationError(`MontoTotal "${total}" is not a decimal amount`)
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
return compareDecimal(total, CONSUMPTION_SUMMARY_THRESHOLD) < 0 ? (0, import_neverthrow.err)(
|
|
1002
|
+
validationError(
|
|
1003
|
+
"An E32 below RD$250,000 must be sent as an RFCE summary; convert it with convertECF32ToRFCE first"
|
|
1004
|
+
)
|
|
1005
|
+
) : (0, import_neverthrow.ok)(GENERAL);
|
|
1006
|
+
};
|
|
888
1007
|
|
|
889
1008
|
// src/utils.ts
|
|
890
1009
|
var getCodeSixDigitfromSignature = (signedXml) => {
|
|
@@ -1048,22 +1167,39 @@ var import_node_crypto2 = require("crypto");
|
|
|
1048
1167
|
var PENDING_SUBMISSION = { pending: true };
|
|
1049
1168
|
var isPendingSubmission = (value) => typeof value === "object" && value !== null && value.pending === true;
|
|
1050
1169
|
var sha256Hex = (value) => (0, import_node_crypto2.createHash)("sha256").update(value, "utf8").digest("hex");
|
|
1170
|
+
var parseExpiry = (value) => {
|
|
1171
|
+
if (value === void 0) {
|
|
1172
|
+
return void 0;
|
|
1173
|
+
}
|
|
1174
|
+
const parsed = Date.parse(value);
|
|
1175
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
1176
|
+
};
|
|
1051
1177
|
var ECF = class {
|
|
1052
1178
|
environment;
|
|
1053
1179
|
http;
|
|
1054
1180
|
idempotency;
|
|
1055
1181
|
credentials;
|
|
1056
1182
|
inFlight = /* @__PURE__ */ new Map();
|
|
1183
|
+
tokenRefreshSkewMs;
|
|
1184
|
+
now;
|
|
1185
|
+
tokenExpiresAt;
|
|
1186
|
+
refreshPromise;
|
|
1057
1187
|
constructor(credentials, environment = "TesteCF" /* Dev */, options = {}) {
|
|
1058
1188
|
this.credentials = credentials;
|
|
1059
1189
|
this.environment = environment;
|
|
1190
|
+
this.tokenRefreshSkewMs = options.tokenRefreshSkewMs ?? 5 * 60 * 1e3;
|
|
1191
|
+
this.now = options.now ?? Date.now;
|
|
1060
1192
|
this.idempotency = options.idempotencyStore ?? new InMemoryIdempotencyStore();
|
|
1061
1193
|
this.http = new HttpClient({
|
|
1062
1194
|
baseUrl: BaseUrl.ECF,
|
|
1063
1195
|
...options.token !== void 0 ? { token: options.token } : {},
|
|
1064
1196
|
...options.resilience !== void 0 ? { resilience: options.resilience } : {},
|
|
1065
1197
|
...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
|
|
1066
|
-
...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {}
|
|
1198
|
+
...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {},
|
|
1199
|
+
beforeRequest: async () => {
|
|
1200
|
+
await this.ensureToken();
|
|
1201
|
+
},
|
|
1202
|
+
onUnauthorized: () => this.reauthenticate()
|
|
1067
1203
|
});
|
|
1068
1204
|
}
|
|
1069
1205
|
buildIdempotencyKey(signedXml) {
|
|
@@ -1115,6 +1251,40 @@ var ECF = class {
|
|
|
1115
1251
|
this.inFlight.delete(key);
|
|
1116
1252
|
}
|
|
1117
1253
|
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Refresh the token when it is missing or about to expire.
|
|
1256
|
+
*
|
|
1257
|
+
* A pre-supplied `token` has no known expiry, so it is never refreshed
|
|
1258
|
+
* proactively — only a 401 triggers a re-authentication for it.
|
|
1259
|
+
*/
|
|
1260
|
+
async ensureToken() {
|
|
1261
|
+
if (this.tokenExpiresAt === void 0) {
|
|
1262
|
+
return (0, import_neverthrow.ok)(void 0);
|
|
1263
|
+
}
|
|
1264
|
+
if (this.now() < this.tokenExpiresAt - this.tokenRefreshSkewMs) {
|
|
1265
|
+
return (0, import_neverthrow.ok)(void 0);
|
|
1266
|
+
}
|
|
1267
|
+
const refreshed = await this.refreshToken();
|
|
1268
|
+
return refreshed.isOk() ? (0, import_neverthrow.ok)(void 0) : (0, import_neverthrow.err)(refreshed.error);
|
|
1269
|
+
}
|
|
1270
|
+
/** Authenticate, coalescing concurrent refreshes into a single round-trip. */
|
|
1271
|
+
refreshToken() {
|
|
1272
|
+
if (this.refreshPromise !== void 0) {
|
|
1273
|
+
return this.refreshPromise;
|
|
1274
|
+
}
|
|
1275
|
+
const promise = this.authenticate().finally(() => {
|
|
1276
|
+
this.refreshPromise = void 0;
|
|
1277
|
+
});
|
|
1278
|
+
this.refreshPromise = promise;
|
|
1279
|
+
return promise;
|
|
1280
|
+
}
|
|
1281
|
+
/** Drop the rejected token and authenticate once. Returns `true` on success. */
|
|
1282
|
+
async reauthenticate() {
|
|
1283
|
+
this.http.clearToken();
|
|
1284
|
+
this.tokenExpiresAt = void 0;
|
|
1285
|
+
const refreshed = await this.refreshToken();
|
|
1286
|
+
return refreshed.isOk();
|
|
1287
|
+
}
|
|
1118
1288
|
/** Authenticate against DGII (or a buyer host) and store the bearer token. */
|
|
1119
1289
|
async authenticate(buyerHost) {
|
|
1120
1290
|
const seed = await this.getSeed(buyerHost);
|
|
@@ -1141,6 +1311,7 @@ var ECF = class {
|
|
|
1141
1311
|
return (0, import_neverthrow.err)(token.error);
|
|
1142
1312
|
}
|
|
1143
1313
|
this.http.setToken(token.value.token);
|
|
1314
|
+
this.tokenExpiresAt = parseExpiry(token.value.expira);
|
|
1144
1315
|
return (0, import_neverthrow.ok)(token.value);
|
|
1145
1316
|
}
|
|
1146
1317
|
getSeed(buyerHost) {
|
|
@@ -1151,20 +1322,23 @@ var ECF = class {
|
|
|
1151
1322
|
);
|
|
1152
1323
|
return this.http.get(url2, void 0, {
|
|
1153
1324
|
absoluteUrl: true,
|
|
1154
|
-
resource: "fe/autenticacion/api/semilla" /* Seed
|
|
1325
|
+
resource: "fe/autenticacion/api/semilla" /* Seed */,
|
|
1326
|
+
skipAuthHooks: true
|
|
1155
1327
|
});
|
|
1156
1328
|
}
|
|
1157
1329
|
const url = buildUrl(this.environment, "Autenticacion/api/Autenticacion/Semilla" /* Seed */);
|
|
1158
1330
|
return this.http.get(url, void 0, {
|
|
1159
1331
|
absoluteUrl: true,
|
|
1160
|
-
resource: "Autenticacion/api/Autenticacion/Semilla" /* Seed
|
|
1332
|
+
resource: "Autenticacion/api/Autenticacion/Semilla" /* Seed */,
|
|
1333
|
+
skipAuthHooks: true
|
|
1161
1334
|
});
|
|
1162
1335
|
}
|
|
1163
1336
|
getAuthToken(signedSeed, buyerHost) {
|
|
1164
1337
|
const url = buyerHost ? buildSenderReceiverUrl(buyerHost, "fe/autenticacion/api/validacioncertificado" /* ValidateSeed */) : buildUrl(this.environment, "autenticacion/api/Autenticacion/ValidarSemilla" /* ValidateSeed */);
|
|
1165
1338
|
return this.http.postMultipart(url, "signed.xml", signedSeed, {
|
|
1166
1339
|
absoluteUrl: true,
|
|
1167
|
-
resource: "autenticacion/api/Autenticacion/ValidarSemilla" /* ValidateSeed
|
|
1340
|
+
resource: "autenticacion/api/Autenticacion/ValidarSemilla" /* ValidateSeed */,
|
|
1341
|
+
skipAuthHooks: true
|
|
1168
1342
|
});
|
|
1169
1343
|
}
|
|
1170
1344
|
/**
|
|
@@ -1196,6 +1370,32 @@ var ECF = class {
|
|
|
1196
1370
|
)
|
|
1197
1371
|
);
|
|
1198
1372
|
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Submit a signed document to the endpoint its own content belongs to.
|
|
1375
|
+
*
|
|
1376
|
+
* Reads the root element, `TipoeCF` and `MontoTotal` from the signed XML and
|
|
1377
|
+
* dispatches to {@link sendSummary} for an RFCE or to
|
|
1378
|
+
* {@link sendElectronicDocument} for an ECF. An E32 below RD$250,000 is
|
|
1379
|
+
* rejected because DGII only accepts the summary form for those.
|
|
1380
|
+
*/
|
|
1381
|
+
async sendDocument(signedXml, fileName, buyerHost) {
|
|
1382
|
+
const route = resolveReceptionRoute(signedXml);
|
|
1383
|
+
if (route.isErr()) {
|
|
1384
|
+
return (0, import_neverthrow.err)(route.error);
|
|
1385
|
+
}
|
|
1386
|
+
if (route.value.kind === "summary") {
|
|
1387
|
+
if (buyerHost !== void 0) {
|
|
1388
|
+
return (0, import_neverthrow.err)(
|
|
1389
|
+
new DgiiError(
|
|
1390
|
+
"An RFCE summary cannot be delivered directly to a buyer host",
|
|
1391
|
+
{ code: "VALIDATION" }
|
|
1392
|
+
)
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
return this.sendSummary(signedXml, fileName);
|
|
1396
|
+
}
|
|
1397
|
+
return this.sendElectronicDocument(signedXml, fileName, buyerHost);
|
|
1398
|
+
}
|
|
1199
1399
|
statusTrackId(trackId) {
|
|
1200
1400
|
const url = buildUrl(this.environment, "consultaresultado/api/Consultas/Estado" /* TrackResultStatus */);
|
|
1201
1401
|
return this.http.get(
|
|
@@ -1290,8 +1490,10 @@ var ECF = class {
|
|
|
1290
1490
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1291
1491
|
0 && (module.exports = {
|
|
1292
1492
|
BaseUrl,
|
|
1493
|
+
CONSUMPTION_SUMMARY_THRESHOLD,
|
|
1293
1494
|
DgiiError,
|
|
1294
1495
|
ECF,
|
|
1496
|
+
ECF_TYPE_CODES,
|
|
1295
1497
|
Endpoint,
|
|
1296
1498
|
Environment,
|
|
1297
1499
|
Err,
|
|
@@ -1306,6 +1508,7 @@ var ECF = class {
|
|
|
1306
1508
|
Transformer,
|
|
1307
1509
|
buildSenderReceiverUrl,
|
|
1308
1510
|
buildUrl,
|
|
1511
|
+
compareDecimal,
|
|
1309
1512
|
convertECF32ToRFCE,
|
|
1310
1513
|
createResiliencePolicies,
|
|
1311
1514
|
defaultResilienceOptions,
|
|
@@ -1321,10 +1524,12 @@ var ECF = class {
|
|
|
1321
1524
|
getCurrentFormattedDate,
|
|
1322
1525
|
getCurrentFormattedDateTime,
|
|
1323
1526
|
getXmlFromBodyResponse,
|
|
1527
|
+
isEcfTypeCode,
|
|
1324
1528
|
isRetryableError,
|
|
1325
1529
|
isRetryableStatus,
|
|
1326
1530
|
ok,
|
|
1327
1531
|
okAsync,
|
|
1532
|
+
resolveReceptionRoute,
|
|
1328
1533
|
roundHalfUpToCents,
|
|
1329
1534
|
toDgiiError,
|
|
1330
1535
|
validateXMLCertificate
|