@deuriib/dgii-ecf 0.5.1 → 0.6.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
@@ -24,6 +24,7 @@ var Endpoint = /* @__PURE__ */ ((Endpoint2) => {
24
24
  Endpoint2["InquiryStatus"] = "consultaestado/api/Consultas/Estado";
25
25
  Endpoint2["AllTrackingEcf"] = "ConsultaTrackIds/api/TrackIds/Consulta";
26
26
  Endpoint2["Directory"] = "consultadirectorio/api/consultas/obtenerdirectorioporrnc";
27
+ Endpoint2["DirectoryList"] = "consultadirectorio/api/consultas/listado";
27
28
  Endpoint2["Void"] = "anulacionrangos/api/operaciones/anularrango";
28
29
  return Endpoint2;
29
30
  })(Endpoint || {});
@@ -34,6 +35,19 @@ var SenderReceiverEndpoint = /* @__PURE__ */ ((SenderReceiverEndpoint2) => {
34
35
  SenderReceiverEndpoint2["CommercialApproval"] = "fe/aprobacioncomercial/api/ecf";
35
36
  return SenderReceiverEndpoint2;
36
37
  })(SenderReceiverEndpoint || {});
38
+ var StatusEndpoint = /* @__PURE__ */ ((StatusEndpoint2) => {
39
+ StatusEndpoint2["Services"] = "api/estatusservicios/obtenerestatus";
40
+ StatusEndpoint2["MaintenanceWindows"] = "api/estatusservicios/obtenerventanasmantenimiento";
41
+ StatusEndpoint2["Check"] = "api/estatusservicios/verificarestado";
42
+ return StatusEndpoint2;
43
+ })(StatusEndpoint || {});
44
+ var StatusEnvironment = /* @__PURE__ */ ((StatusEnvironment3) => {
45
+ StatusEnvironment3[StatusEnvironment3["PreCertification"] = 1] = "PreCertification";
46
+ StatusEnvironment3[StatusEnvironment3["Production"] = 2] = "Production";
47
+ StatusEnvironment3[StatusEnvironment3["Certification"] = 3] = "Certification";
48
+ return StatusEnvironment3;
49
+ })(StatusEnvironment || {});
50
+ var buildStatusUrl = (endpoint, baseUrl = BaseUrl.STATUS) => `${baseUrl.replace(/\/+$/, "")}/${endpoint}`;
37
51
  var buildUrl = (environment, endpoint, baseUrl = BaseUrl.ECF) => `${baseUrl.replace(/\/+$/, "")}/${environment}/${endpoint}`;
38
52
  var buildSenderReceiverUrl = (host, endpoint) => `${host.replace(/\/+$/, "")}/${endpoint}`;
39
53
 
@@ -46,6 +60,9 @@ var TrackStatusEnum = /* @__PURE__ */ ((TrackStatusEnum2) => {
46
60
  TrackStatusEnum2["ConditionalAccepted"] = "Aceptado Condicional";
47
61
  return TrackStatusEnum2;
48
62
  })(TrackStatusEnum || {});
63
+ var SUMMARY_ACCEPTED = "Aceptado";
64
+ var SUMMARY_CONDITIONAL_ACCEPTED = "Aceptado condicional";
65
+ var SUMMARY_REJECTED = "Rechazado";
49
66
 
50
67
  // src/errors.ts
51
68
  var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
@@ -409,9 +426,9 @@ var HttpClient = class {
409
426
  }
410
427
  return url;
411
428
  }
412
- authHeaders(extra) {
429
+ authHeaders(extra, omitAuth = false) {
413
430
  const headers = { Accept: "application/json" };
414
- if (this.token) {
431
+ if (!omitAuth && this.token) {
415
432
  headers.Authorization = `Bearer ${this.token}`;
416
433
  }
417
434
  return { ...headers, ...extra };
@@ -464,7 +481,7 @@ var HttpClient = class {
464
481
  async execute(options) {
465
482
  const url = this.buildUrl(options.path, options.absoluteUrl, options.query);
466
483
  const method = options.method ?? "GET";
467
- const headers = this.authHeaders(options.headers);
484
+ const headers = this.authHeaders(options.headers, options.omitAuth);
468
485
  const ctx = {
469
486
  method,
470
487
  resource: options.resource ?? options.path,
@@ -786,18 +803,43 @@ var roundHalfUpToCents = (value) => {
786
803
  const padded = rounded.padStart(3, "0");
787
804
  return `${sign}${padded.slice(0, -2)}.${padded.slice(-2)}`;
788
805
  };
806
+ var isEmptyObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0;
807
+ var pruneEmptyValues = (input) => {
808
+ if (input === null || input === void 0 || input === "") {
809
+ return void 0;
810
+ }
811
+ if (Array.isArray(input)) {
812
+ const filtered = input.map(pruneEmptyValues).filter(
813
+ (item) => item !== null && item !== void 0 && item !== "" && !isEmptyObject(item)
814
+ );
815
+ return filtered.length ? filtered : void 0;
816
+ }
817
+ if (typeof input === "object") {
818
+ const output = {};
819
+ for (const [key, value] of Object.entries(
820
+ input
821
+ )) {
822
+ const cleaned = pruneEmptyValues(value);
823
+ if (cleaned !== void 0 && cleaned !== "" && !isEmptyObject(cleaned)) {
824
+ output[key] = cleaned;
825
+ }
826
+ }
827
+ return Object.keys(output).length ? output : void 0;
828
+ }
829
+ return input;
830
+ };
789
831
  var Transformer = class {
790
832
  json2xml(dataObject, round = false) {
791
833
  try {
834
+ const pruned = pruneEmptyValues(dataObject);
792
835
  const data = {
793
836
  _declaration: { _attributes: { version: "1.0", encoding: "utf-8" } },
794
- ...dataObject
837
+ ...pruned !== null && pruned !== void 0 && typeof pruned === "object" && !Array.isArray(pruned) ? pruned : {}
795
838
  };
796
839
  const xml = convert.js2xml(data, {
797
840
  compact: true,
798
841
  ignoreComment: true,
799
842
  spaces: 2,
800
- fullTagEmptyElement: true,
801
843
  textFn: (value, label) => {
802
844
  if (round && label.toLowerCase() !== "version") {
803
845
  return roundHalfUpToCents(value);
@@ -948,6 +990,190 @@ var resolveReceptionRoute = (signedXml) => {
948
990
  )
949
991
  ) : ok(GENERAL);
950
992
  };
993
+ var B2B_EXCLUDED_TYPES = [
994
+ "41",
995
+ "43",
996
+ "45",
997
+ "46",
998
+ "47"
999
+ ];
1000
+ var checkB2BApplicability = (signedXml) => {
1001
+ const root = extractRootElementName(signedXml);
1002
+ if (root === "RFCE") {
1003
+ return err(
1004
+ validationError(
1005
+ "An RFCE summary cannot be delivered directly to a buyer host"
1006
+ )
1007
+ );
1008
+ }
1009
+ if (root !== "ECF") {
1010
+ return err(
1011
+ validationError(
1012
+ `Unsupported document root "${root ?? "unknown"}"; expected "ECF"`
1013
+ )
1014
+ );
1015
+ }
1016
+ const tipo = extractElementText(signedXml, "TipoeCF");
1017
+ if (tipo === void 0) {
1018
+ return err(validationError("TipoeCF is missing from the document"));
1019
+ }
1020
+ if (!isEcfTypeCode(tipo)) {
1021
+ return err(validationError(`Unknown TipoeCF "${tipo}"`));
1022
+ }
1023
+ if (B2B_EXCLUDED_TYPES.includes(tipo)) {
1024
+ return err(
1025
+ validationError(
1026
+ `An e-CF of type ${tipo} cannot be delivered directly to a buyer host`
1027
+ )
1028
+ );
1029
+ }
1030
+ if (tipo !== CONSUMPTION_TYPE) {
1031
+ return ok(void 0);
1032
+ }
1033
+ const total = extractElementText(signedXml, "MontoTotal");
1034
+ if (total === void 0) {
1035
+ return err(
1036
+ validationError(
1037
+ "MontoTotal is required to check B2B applicability of an E32"
1038
+ )
1039
+ );
1040
+ }
1041
+ if (!DECIMAL_PATTERN.test(total)) {
1042
+ return err(
1043
+ validationError(`MontoTotal "${total}" is not a decimal amount`)
1044
+ );
1045
+ }
1046
+ if (compareDecimal(total, CONSUMPTION_SUMMARY_THRESHOLD) < 0) {
1047
+ return err(
1048
+ validationError(
1049
+ "An E32 below RD$250,000 cannot be delivered directly to a buyer host; send the RFCE summary to DGII instead"
1050
+ )
1051
+ );
1052
+ }
1053
+ return ok(void 0);
1054
+ };
1055
+
1056
+ // src/reconciliation.ts
1057
+ var PENDING_SUBMISSION_MESSAGE = "A submission with this key is already pending; call reconcileSubmission() before retrying";
1058
+ var canReuseSequence = (response) => response.secuenciaUtilizada === false;
1059
+
1060
+ // src/validation.ts
1061
+ import { spawnSync } from "child_process";
1062
+ import { existsSync } from "fs";
1063
+ import { dirname, resolve } from "path";
1064
+ import { fileURLToPath } from "url";
1065
+ var moduleDir = (() => {
1066
+ if (typeof __dirname !== "undefined") {
1067
+ return __dirname;
1068
+ }
1069
+ return dirname(fileURLToPath(import.meta.url));
1070
+ })();
1071
+ var SCHEMA_DIR = [moduleDir, resolve(moduleDir, "..", "schemas")].find(
1072
+ (candidate) => existsSync(resolve(candidate, "e-cf-32.xsd"))
1073
+ ) ?? resolve(moduleDir, "..", "schemas");
1074
+ var SCHEMA_FILES = {
1075
+ "ECF|32": "e-cf-32.xsd",
1076
+ "ECF|33": "e-cf-33.xsd",
1077
+ "ECF|34": "e-cf-34.xsd",
1078
+ "ECF|41": "e-cf-41.xsd",
1079
+ "ECF|43": "e-cf-43.xsd",
1080
+ "ECF|44": "e-cf-44.xsd",
1081
+ "ECF|45": "e-cf-45.xsd",
1082
+ "ECF|46": "e-cf-46.xsd",
1083
+ "ECF|47": "e-cf-47.xsd",
1084
+ "ANECF|": "anecf.xsd",
1085
+ "ARECF|": "arecf.xsd",
1086
+ "SemillaModel|": "semilla.xsd"
1087
+ };
1088
+ var UNAVAILABLE_SCHEMAS = {
1089
+ "ECF|31": "the published schema references IndicadorServicioTodoIncluidoType, which does not resolve",
1090
+ "RFCE|": "the published schema has an invalid pattern facet: [$0-9] and the \\- escape are not valid XML Schema regex",
1091
+ "ACECF|": "the published schema has an invalid pattern facet: the \\- escape is not valid XML Schema regex"
1092
+ };
1093
+ var schemaKey = (xml) => {
1094
+ const root = extractRootElementName(xml);
1095
+ if (root === void 0) {
1096
+ return void 0;
1097
+ }
1098
+ if (root !== "ECF") {
1099
+ return `${root}|`;
1100
+ }
1101
+ const tipo = extractElementText(xml, "TipoeCF");
1102
+ return tipo === void 0 ? void 0 : `ECF|${tipo}`;
1103
+ };
1104
+ var validationError2 = (message, cause) => new DgiiError(message, {
1105
+ code: "VALIDATION",
1106
+ ...cause !== void 0 ? { cause } : {}
1107
+ });
1108
+ var VIOLATION_LINE = /^.*?:(\d+):\s*element\s+([^:]+):\s*(.*)$/;
1109
+ var parseViolations = (stderr) => {
1110
+ const violations = [];
1111
+ for (const raw of stderr.split("\n")) {
1112
+ const line = raw.trim();
1113
+ if (line === "" || line.endsWith("fails to validate") || line.endsWith("validates")) {
1114
+ continue;
1115
+ }
1116
+ const match = VIOLATION_LINE.exec(line);
1117
+ if (match !== null) {
1118
+ violations.push({
1119
+ path: (match[2] ?? "").trim(),
1120
+ message: (match[3] ?? "").trim()
1121
+ });
1122
+ } else {
1123
+ violations.push({ path: "", message: line });
1124
+ }
1125
+ }
1126
+ return violations;
1127
+ };
1128
+ var validateDocument = (xml, options = {}) => {
1129
+ const key = schemaKey(xml);
1130
+ if (key === void 0) {
1131
+ return err(
1132
+ validationError2("Cannot determine the document type to validate")
1133
+ );
1134
+ }
1135
+ const file = SCHEMA_FILES[key];
1136
+ if (file === void 0) {
1137
+ const reason = UNAVAILABLE_SCHEMAS[key] ?? "no bundled XML Schema for this document type";
1138
+ const [root, tipo] = key.split("|");
1139
+ const label = tipo ? `${root} ${tipo}` : root ?? key;
1140
+ return err(validationError2(`Schema unavailable for ${label}: ${reason}`));
1141
+ }
1142
+ const bin = options.bin ?? process.env["DGII_XMLLINT_BIN"] ?? "xmllint";
1143
+ const result = spawnSync(
1144
+ bin,
1145
+ ["--noout", "--schema", resolve(SCHEMA_DIR, file), "-"],
1146
+ { input: xml, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }
1147
+ );
1148
+ if (result.error !== void 0 && result.error !== null) {
1149
+ const code = result.error.code;
1150
+ return err(
1151
+ validationError2(
1152
+ code === "ENOENT" ? `xmllint was not found ("${bin}"). Install libxml2-utils or set DGII_XMLLINT_BIN; schema validation cannot run without it` : `Failed to run xmllint: ${result.error.message}`,
1153
+ result.error
1154
+ )
1155
+ );
1156
+ }
1157
+ if (result.status === 0) {
1158
+ return ok([]);
1159
+ }
1160
+ const violations = parseViolations(result.stderr ?? "");
1161
+ return ok(
1162
+ violations.length > 0 ? violations : [
1163
+ {
1164
+ path: "",
1165
+ message: (result.stderr ?? "").trim() || "Validation failed"
1166
+ }
1167
+ ]
1168
+ );
1169
+ };
1170
+ var formatViolations = (violations) => {
1171
+ const shown = violations.slice(0, 5).map(
1172
+ (violation) => violation.path ? `${violation.path}: ${violation.message}` : violation.message
1173
+ ).join("; ");
1174
+ const remaining = violations.length - 5;
1175
+ return remaining > 0 ? `${shown} (+${remaining} more)` : shown;
1176
+ };
951
1177
 
952
1178
  // src/utils.ts
953
1179
  var getCodeSixDigitfromSignature = (signedXml) => {
@@ -1007,31 +1233,6 @@ var toArray = (value) => {
1007
1233
  }
1008
1234
  return Array.isArray(value) ? value : [value];
1009
1235
  };
1010
- var isEmptyObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0;
1011
- var removeEmptyValues = (input) => {
1012
- if (input === null || input === void 0 || input === "") {
1013
- return void 0;
1014
- }
1015
- if (Array.isArray(input)) {
1016
- const filtered = input.map(removeEmptyValues).filter(
1017
- (item) => item !== null && item !== void 0 && item !== "" && !isEmptyObject(item)
1018
- );
1019
- return filtered.length ? filtered : void 0;
1020
- }
1021
- if (typeof input === "object") {
1022
- const output = {};
1023
- for (const [key, value] of Object.entries(
1024
- input
1025
- )) {
1026
- const cleaned = removeEmptyValues(value);
1027
- if (cleaned !== void 0 && cleaned !== "" && !isEmptyObject(cleaned)) {
1028
- output[key] = cleaned;
1029
- }
1030
- }
1031
- return Object.keys(output).length ? output : void 0;
1032
- }
1033
- return input;
1034
- };
1035
1236
  var convertECF32ToRFCE = (ecf32Xml) => {
1036
1237
  const securityCode = getCodeSixDigitfromSignature(ecf32Xml);
1037
1238
  if (securityCode.isErr()) {
@@ -1046,7 +1247,7 @@ var convertECF32ToRFCE = (ecf32Xml) => {
1046
1247
  const impuestos = toArray(
1047
1248
  encabezado?.Totales?.ImpuestosAdicionales?.ImpuestoAdicional
1048
1249
  );
1049
- const rfce = removeEmptyValues({
1250
+ const rfce = pruneEmptyValues({
1050
1251
  RFCE: {
1051
1252
  Encabezado: {
1052
1253
  Version: encabezado?.Version,
@@ -1126,6 +1327,10 @@ var ECF = class {
1126
1327
  inFlight = /* @__PURE__ */ new Map();
1127
1328
  tokenRefreshSkewMs;
1128
1329
  now;
1330
+ statusApiKey;
1331
+ enforceB2BRules;
1332
+ validateBeforeSend;
1333
+ xmllintBin;
1129
1334
  tokenExpiresAt;
1130
1335
  refreshPromise;
1131
1336
  constructor(credentials, environment = "TesteCF" /* Dev */, options = {}) {
@@ -1133,6 +1338,10 @@ var ECF = class {
1133
1338
  this.environment = environment;
1134
1339
  this.tokenRefreshSkewMs = options.tokenRefreshSkewMs ?? 5 * 60 * 1e3;
1135
1340
  this.now = options.now ?? Date.now;
1341
+ this.statusApiKey = options.statusApiKey;
1342
+ this.enforceB2BRules = options.enforceB2BRules ?? true;
1343
+ this.validateBeforeSend = options.validateBeforeSend ?? false;
1344
+ this.xmllintBin = options.xmllintBin;
1136
1345
  this.idempotency = options.idempotencyStore ?? new InMemoryIdempotencyStore();
1137
1346
  this.http = new HttpClient({
1138
1347
  baseUrl: BaseUrl.ECF,
@@ -1147,8 +1356,8 @@ var ECF = class {
1147
1356
  });
1148
1357
  }
1149
1358
  buildIdempotencyKey(signedXml) {
1150
- const rnc = extractElementText(signedXml, "RNCEmisor") ?? "unknown-rnc";
1151
- const ncf = extractElementText(signedXml, "eNCF") ?? extractElementText(signedXml, "RangoNCF") ?? "unknown-ncf";
1359
+ const rnc = extractElementText(signedXml, "RNCEmisor") ?? extractElementText(signedXml, "RncEmisor") ?? "unknown-rnc";
1360
+ const ncf = extractElementText(signedXml, "eNCF") ?? extractElementText(signedXml, "SecuenciaeNCFDesde") ?? extractElementText(signedXml, "RangoNCF") ?? "unknown-ncf";
1152
1361
  return `${rnc}:${ncf}:${sha256Hex(signedXml)}`;
1153
1362
  }
1154
1363
  /**
@@ -1163,10 +1372,7 @@ var ECF = class {
1163
1372
  if (cached !== void 0) {
1164
1373
  if (isPendingSubmission(cached)) {
1165
1374
  return err(
1166
- new DgiiError(
1167
- "A submission with this key is already pending; reconcile via a status lookup before retrying",
1168
- { code: "IDEMPOTENCY" }
1169
- )
1375
+ new DgiiError(PENDING_SUBMISSION_MESSAGE, { code: "IDEMPOTENCY" })
1170
1376
  );
1171
1377
  }
1172
1378
  return ok(cached);
@@ -1258,6 +1464,90 @@ var ECF = class {
1258
1464
  this.tokenExpiresAt = parseExpiry(token.value.expira);
1259
1465
  return ok(token.value);
1260
1466
  }
1467
+ /**
1468
+ * Query the operational status of every DGII service.
1469
+ *
1470
+ * The status host takes an API key instead of a bearer token, so this call
1471
+ * neither needs `authenticate()` nor triggers a token refresh.
1472
+ */
1473
+ async getServiceStatus() {
1474
+ const result = await this.statusRequest("api/estatusservicios/obtenerestatus" /* Services */);
1475
+ if (result.isErr()) {
1476
+ return err(result.error);
1477
+ }
1478
+ const data = result.value;
1479
+ if (data === void 0 || data === null) {
1480
+ return ok([]);
1481
+ }
1482
+ return ok(Array.isArray(data) ? data : [data]);
1483
+ }
1484
+ /** Scheduled maintenance windows, one entry per environment. */
1485
+ async getMaintenanceWindows() {
1486
+ const result = await this.statusRequest("api/estatusservicios/obtenerventanasmantenimiento" /* MaintenanceWindows */);
1487
+ if (result.isErr()) {
1488
+ return err(result.error);
1489
+ }
1490
+ const data = result.value;
1491
+ if (Array.isArray(data)) {
1492
+ return ok(data);
1493
+ }
1494
+ return ok(data?.ventanaMantenimientos ?? []);
1495
+ }
1496
+ /** Whether an environment is currently inside a maintenance window. */
1497
+ checkServiceAvailability(ambiente) {
1498
+ return this.statusRequest(
1499
+ "api/estatusservicios/verificarestado" /* Check */,
1500
+ { ambiente }
1501
+ );
1502
+ }
1503
+ statusRequest(endpoint, query) {
1504
+ const headers = this.statusApiKey === void 0 ? {} : { Authorization: `Apikey ${this.statusApiKey}` };
1505
+ return this.http.request({
1506
+ method: "GET",
1507
+ path: buildStatusUrl(endpoint),
1508
+ absoluteUrl: true,
1509
+ resource: `status:${endpoint}`,
1510
+ headers,
1511
+ ...query !== void 0 ? { query } : {},
1512
+ skipAuthHooks: true,
1513
+ omitAuth: true
1514
+ });
1515
+ }
1516
+ /**
1517
+ * Resolve what happened to a submission whose idempotency marker is pending.
1518
+ *
1519
+ * Queries DGII by e-NCF. When DGII has no record of the document the marker
1520
+ * is cleared, because re-submitting cannot double-issue. When DGII does have
1521
+ * a record the marker stays in place — it is the lock that prevents a second
1522
+ * submission of a document that already exists — and `delivered` tells the
1523
+ * caller to stop retrying.
1524
+ */
1525
+ async reconcileSubmission(signedXml) {
1526
+ const rnc = extractElementText(signedXml, "RNCEmisor") ?? extractElementText(signedXml, "RncEmisor");
1527
+ const encf = extractElementText(signedXml, "eNCF") ?? extractElementText(signedXml, "SecuenciaeNCFDesde");
1528
+ if (rnc === void 0 || encf === void 0) {
1529
+ return err(
1530
+ new DgiiError("The document has no emitter RNC or e-NCF to reconcile", {
1531
+ code: "VALIDATION"
1532
+ })
1533
+ );
1534
+ }
1535
+ const inquiry = await this.inquiryStatus(rnc, encf);
1536
+ if (inquiry.isErr()) {
1537
+ return err(inquiry.error);
1538
+ }
1539
+ const delivered = inquiry.value.estado !== "No encontrado";
1540
+ const cleared = !delivered;
1541
+ if (cleared) {
1542
+ await this.idempotency.delete(this.buildIdempotencyKey(signedXml));
1543
+ }
1544
+ return ok({
1545
+ inquiry: inquiry.value,
1546
+ delivered,
1547
+ canReuseSequence: canReuseSequence(inquiry.value),
1548
+ cleared
1549
+ });
1550
+ }
1261
1551
  getSeed(buyerHost) {
1262
1552
  if (buyerHost) {
1263
1553
  const url2 = buildSenderReceiverUrl(
@@ -1290,6 +1580,29 @@ var ECF = class {
1290
1580
  * a repeated call returns the cached response instead of double-issuing.
1291
1581
  */
1292
1582
  async sendElectronicDocument(signedXml, fileName, buyerHost) {
1583
+ if (buyerHost !== void 0 && this.enforceB2BRules) {
1584
+ const applicable = checkB2BApplicability(signedXml);
1585
+ if (applicable.isErr()) {
1586
+ return err(applicable.error);
1587
+ }
1588
+ }
1589
+ if (this.validateBeforeSend) {
1590
+ const validation = validateDocument(
1591
+ signedXml,
1592
+ this.xmllintBin !== void 0 ? { bin: this.xmllintBin } : {}
1593
+ );
1594
+ if (validation.isErr()) {
1595
+ return err(validation.error);
1596
+ }
1597
+ if (validation.value.length > 0) {
1598
+ return err(
1599
+ new DgiiError(
1600
+ `Document does not match the DGII XML Schema: ${formatViolations(validation.value)}`,
1601
+ { code: "VALIDATION", data: validation.value }
1602
+ )
1603
+ );
1604
+ }
1605
+ }
1293
1606
  const key = this.buildIdempotencyKey(signedXml);
1294
1607
  return this.submitIdempotent(key, () => {
1295
1608
  const url = buyerHost ? buildSenderReceiverUrl(buyerHost, "fe/recepcion/api/ecf" /* SendInvoice */) : buildUrl(this.environment, "recepcion/api/FacturasElectronicas" /* SendInvoice */);
@@ -1377,6 +1690,22 @@ var ECF = class {
1377
1690
  }
1378
1691
  return ok(Array.isArray(data) ? data : [data]);
1379
1692
  }
1693
+ /** General directory listing — every receiver that publishes service URLs. */
1694
+ async getDirectoryListing() {
1695
+ const url = buildUrl(this.environment, "consultadirectorio/api/consultas/listado" /* DirectoryList */);
1696
+ const result = await this.http.get(url, void 0, {
1697
+ absoluteUrl: true,
1698
+ resource: "consultadirectorio/api/consultas/listado" /* DirectoryList */
1699
+ });
1700
+ if (result.isErr()) {
1701
+ return err(result.error);
1702
+ }
1703
+ const data = result.value;
1704
+ if (data === void 0 || data === null) {
1705
+ return ok([]);
1706
+ }
1707
+ return ok(Array.isArray(data) ? data : [data]);
1708
+ }
1380
1709
  /** RFCE inquiry — DGII production only. */
1381
1710
  async getSummaryInvoiceInquiry(rnc, encf, codSeguridad) {
1382
1711
  if (this.environment !== "eCF" /* Prod */) {
@@ -1398,12 +1727,32 @@ var ECF = class {
1398
1727
  { absoluteUrl: true, resource: "consultarfce/api/Consultas/Consulta" /* InquiryInvoiceSummary */ }
1399
1728
  );
1400
1729
  }
1401
- /**
1402
- * Submit a signed commercial approval (ACECF). Idempotent like submissions,
1403
- * keyed on `RNC + eNCF + SHA-256(signed XML)`.
1404
- */
1405
- sendCommercialApproval(signedXml, fileName) {
1730
+ sendCommercialApproval(signedXml, fileName, buyerHost) {
1406
1731
  const key = this.buildIdempotencyKey(signedXml);
1732
+ if (buyerHost !== void 0) {
1733
+ return this.submitIdempotent(
1734
+ key,
1735
+ async () => {
1736
+ const url = buildSenderReceiverUrl(
1737
+ buyerHost,
1738
+ "fe/aprobacioncomercial/api/ecf" /* CommercialApproval */
1739
+ );
1740
+ const result = await this.http.postMultipart(
1741
+ url,
1742
+ fileName,
1743
+ signedXml,
1744
+ {
1745
+ absoluteUrl: true,
1746
+ resource: "fe/aprobacioncomercial/api/ecf" /* CommercialApproval */
1747
+ }
1748
+ );
1749
+ if (result.isErr()) {
1750
+ return result.error.status === 400 ? ok({ status: 400, accepted: false }) : err(result.error);
1751
+ }
1752
+ return ok({ status: 200, accepted: true });
1753
+ }
1754
+ );
1755
+ }
1407
1756
  return this.submitIdempotent(
1408
1757
  key,
1409
1758
  () => this.http.postMultipart(
@@ -1444,13 +1793,24 @@ export {
1444
1793
  InMemoryIdempotencyStore,
1445
1794
  Ok,
1446
1795
  P12Reader,
1796
+ PENDING_SUBMISSION_MESSAGE,
1447
1797
  ResultAsync,
1798
+ SCHEMA_DIR,
1799
+ SUMMARY_ACCEPTED,
1800
+ SUMMARY_CONDITIONAL_ACCEPTED,
1801
+ SUMMARY_REJECTED,
1448
1802
  SenderReceiverEndpoint,
1449
1803
  Signature,
1804
+ StatusEndpoint,
1805
+ StatusEnvironment,
1450
1806
  TrackStatusEnum,
1451
1807
  Transformer,
1808
+ UNAVAILABLE_SCHEMAS,
1452
1809
  buildSenderReceiverUrl,
1810
+ buildStatusUrl,
1453
1811
  buildUrl,
1812
+ canReuseSequence,
1813
+ checkB2BApplicability,
1454
1814
  compareDecimal,
1455
1815
  convertECF32ToRFCE,
1456
1816
  createResiliencePolicies,
@@ -1458,6 +1818,7 @@ export {
1458
1818
  err,
1459
1819
  errAsync,
1460
1820
  extractDgiiErrorMessage,
1821
+ formatViolations,
1461
1822
  fromPromise,
1462
1823
  fromSafePromise,
1463
1824
  fromThrowable,
@@ -1472,9 +1833,11 @@ export {
1472
1833
  isRetryableStatus,
1473
1834
  ok,
1474
1835
  okAsync,
1836
+ pruneEmptyValues,
1475
1837
  resolveReceptionRoute,
1476
1838
  roundHalfUpToCents,
1477
1839
  toDgiiError,
1840
+ validateDocument,
1478
1841
  validateXMLCertificate
1479
1842
  };
1480
1843
  //# sourceMappingURL=index.js.map