@deuriib/dgii-ecf 0.5.0 → 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]);
@@ -67,9 +84,10 @@ var readMensajes = (value) => {
67
84
  if (typeof item === "string" && item.trim()) {
68
85
  out.push({ valor: item.trim(), codigo: 0 });
69
86
  } else if (isObject(item) && typeof item.valor === "string") {
87
+ const codigo = item.codigo;
70
88
  out.push({
71
89
  valor: item.valor,
72
- codigo: typeof item.codigo === "number" ? item.codigo : 0
90
+ codigo: typeof codigo === "number" || typeof codigo === "string" ? codigo : 0
73
91
  });
74
92
  }
75
93
  }
@@ -408,9 +426,9 @@ var HttpClient = class {
408
426
  }
409
427
  return url;
410
428
  }
411
- authHeaders(extra) {
429
+ authHeaders(extra, omitAuth = false) {
412
430
  const headers = { Accept: "application/json" };
413
- if (this.token) {
431
+ if (!omitAuth && this.token) {
414
432
  headers.Authorization = `Bearer ${this.token}`;
415
433
  }
416
434
  return { ...headers, ...extra };
@@ -463,7 +481,7 @@ var HttpClient = class {
463
481
  async execute(options) {
464
482
  const url = this.buildUrl(options.path, options.absoluteUrl, options.query);
465
483
  const method = options.method ?? "GET";
466
- const headers = this.authHeaders(options.headers);
484
+ const headers = this.authHeaders(options.headers, options.omitAuth);
467
485
  const ctx = {
468
486
  method,
469
487
  resource: options.resource ?? options.path,
@@ -785,18 +803,43 @@ var roundHalfUpToCents = (value) => {
785
803
  const padded = rounded.padStart(3, "0");
786
804
  return `${sign}${padded.slice(0, -2)}.${padded.slice(-2)}`;
787
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
+ };
788
831
  var Transformer = class {
789
832
  json2xml(dataObject, round = false) {
790
833
  try {
834
+ const pruned = pruneEmptyValues(dataObject);
791
835
  const data = {
792
836
  _declaration: { _attributes: { version: "1.0", encoding: "utf-8" } },
793
- ...dataObject
837
+ ...pruned !== null && pruned !== void 0 && typeof pruned === "object" && !Array.isArray(pruned) ? pruned : {}
794
838
  };
795
839
  const xml = convert.js2xml(data, {
796
840
  compact: true,
797
841
  ignoreComment: true,
798
842
  spaces: 2,
799
- fullTagEmptyElement: true,
800
843
  textFn: (value, label) => {
801
844
  if (round && label.toLowerCase() !== "version") {
802
845
  return roundHalfUpToCents(value);
@@ -947,6 +990,190 @@ var resolveReceptionRoute = (signedXml) => {
947
990
  )
948
991
  ) : ok(GENERAL);
949
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
+ };
950
1177
 
951
1178
  // src/utils.ts
952
1179
  var getCodeSixDigitfromSignature = (signedXml) => {
@@ -1006,31 +1233,6 @@ var toArray = (value) => {
1006
1233
  }
1007
1234
  return Array.isArray(value) ? value : [value];
1008
1235
  };
1009
- var isEmptyObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0;
1010
- var removeEmptyValues = (input) => {
1011
- if (input === null || input === void 0 || input === "") {
1012
- return void 0;
1013
- }
1014
- if (Array.isArray(input)) {
1015
- const filtered = input.map(removeEmptyValues).filter(
1016
- (item) => item !== null && item !== void 0 && item !== "" && !isEmptyObject(item)
1017
- );
1018
- return filtered.length ? filtered : void 0;
1019
- }
1020
- if (typeof input === "object") {
1021
- const output = {};
1022
- for (const [key, value] of Object.entries(
1023
- input
1024
- )) {
1025
- const cleaned = removeEmptyValues(value);
1026
- if (cleaned !== void 0 && cleaned !== "" && !isEmptyObject(cleaned)) {
1027
- output[key] = cleaned;
1028
- }
1029
- }
1030
- return Object.keys(output).length ? output : void 0;
1031
- }
1032
- return input;
1033
- };
1034
1236
  var convertECF32ToRFCE = (ecf32Xml) => {
1035
1237
  const securityCode = getCodeSixDigitfromSignature(ecf32Xml);
1036
1238
  if (securityCode.isErr()) {
@@ -1045,7 +1247,7 @@ var convertECF32ToRFCE = (ecf32Xml) => {
1045
1247
  const impuestos = toArray(
1046
1248
  encabezado?.Totales?.ImpuestosAdicionales?.ImpuestoAdicional
1047
1249
  );
1048
- const rfce = removeEmptyValues({
1250
+ const rfce = pruneEmptyValues({
1049
1251
  RFCE: {
1050
1252
  Encabezado: {
1051
1253
  Version: encabezado?.Version,
@@ -1125,6 +1327,10 @@ var ECF = class {
1125
1327
  inFlight = /* @__PURE__ */ new Map();
1126
1328
  tokenRefreshSkewMs;
1127
1329
  now;
1330
+ statusApiKey;
1331
+ enforceB2BRules;
1332
+ validateBeforeSend;
1333
+ xmllintBin;
1128
1334
  tokenExpiresAt;
1129
1335
  refreshPromise;
1130
1336
  constructor(credentials, environment = "TesteCF" /* Dev */, options = {}) {
@@ -1132,6 +1338,10 @@ var ECF = class {
1132
1338
  this.environment = environment;
1133
1339
  this.tokenRefreshSkewMs = options.tokenRefreshSkewMs ?? 5 * 60 * 1e3;
1134
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;
1135
1345
  this.idempotency = options.idempotencyStore ?? new InMemoryIdempotencyStore();
1136
1346
  this.http = new HttpClient({
1137
1347
  baseUrl: BaseUrl.ECF,
@@ -1146,8 +1356,8 @@ var ECF = class {
1146
1356
  });
1147
1357
  }
1148
1358
  buildIdempotencyKey(signedXml) {
1149
- const rnc = extractElementText(signedXml, "RNCEmisor") ?? "unknown-rnc";
1150
- 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";
1151
1361
  return `${rnc}:${ncf}:${sha256Hex(signedXml)}`;
1152
1362
  }
1153
1363
  /**
@@ -1162,10 +1372,7 @@ var ECF = class {
1162
1372
  if (cached !== void 0) {
1163
1373
  if (isPendingSubmission(cached)) {
1164
1374
  return err(
1165
- new DgiiError(
1166
- "A submission with this key is already pending; reconcile via a status lookup before retrying",
1167
- { code: "IDEMPOTENCY" }
1168
- )
1375
+ new DgiiError(PENDING_SUBMISSION_MESSAGE, { code: "IDEMPOTENCY" })
1169
1376
  );
1170
1377
  }
1171
1378
  return ok(cached);
@@ -1257,6 +1464,90 @@ var ECF = class {
1257
1464
  this.tokenExpiresAt = parseExpiry(token.value.expira);
1258
1465
  return ok(token.value);
1259
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
+ }
1260
1551
  getSeed(buyerHost) {
1261
1552
  if (buyerHost) {
1262
1553
  const url2 = buildSenderReceiverUrl(
@@ -1289,6 +1580,29 @@ var ECF = class {
1289
1580
  * a repeated call returns the cached response instead of double-issuing.
1290
1581
  */
1291
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
+ }
1292
1606
  const key = this.buildIdempotencyKey(signedXml);
1293
1607
  return this.submitIdempotent(key, () => {
1294
1608
  const url = buyerHost ? buildSenderReceiverUrl(buyerHost, "fe/recepcion/api/ecf" /* SendInvoice */) : buildUrl(this.environment, "recepcion/api/FacturasElectronicas" /* SendInvoice */);
@@ -1376,6 +1690,22 @@ var ECF = class {
1376
1690
  }
1377
1691
  return ok(Array.isArray(data) ? data : [data]);
1378
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
+ }
1379
1709
  /** RFCE inquiry — DGII production only. */
1380
1710
  async getSummaryInvoiceInquiry(rnc, encf, codSeguridad) {
1381
1711
  if (this.environment !== "eCF" /* Prod */) {
@@ -1397,12 +1727,32 @@ var ECF = class {
1397
1727
  { absoluteUrl: true, resource: "consultarfce/api/Consultas/Consulta" /* InquiryInvoiceSummary */ }
1398
1728
  );
1399
1729
  }
1400
- /**
1401
- * Submit a signed commercial approval (ACECF). Idempotent like submissions,
1402
- * keyed on `RNC + eNCF + SHA-256(signed XML)`.
1403
- */
1404
- sendCommercialApproval(signedXml, fileName) {
1730
+ sendCommercialApproval(signedXml, fileName, buyerHost) {
1405
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
+ }
1406
1756
  return this.submitIdempotent(
1407
1757
  key,
1408
1758
  () => this.http.postMultipart(
@@ -1443,13 +1793,24 @@ export {
1443
1793
  InMemoryIdempotencyStore,
1444
1794
  Ok,
1445
1795
  P12Reader,
1796
+ PENDING_SUBMISSION_MESSAGE,
1446
1797
  ResultAsync,
1798
+ SCHEMA_DIR,
1799
+ SUMMARY_ACCEPTED,
1800
+ SUMMARY_CONDITIONAL_ACCEPTED,
1801
+ SUMMARY_REJECTED,
1447
1802
  SenderReceiverEndpoint,
1448
1803
  Signature,
1804
+ StatusEndpoint,
1805
+ StatusEnvironment,
1449
1806
  TrackStatusEnum,
1450
1807
  Transformer,
1808
+ UNAVAILABLE_SCHEMAS,
1451
1809
  buildSenderReceiverUrl,
1810
+ buildStatusUrl,
1452
1811
  buildUrl,
1812
+ canReuseSequence,
1813
+ checkB2BApplicability,
1453
1814
  compareDecimal,
1454
1815
  convertECF32ToRFCE,
1455
1816
  createResiliencePolicies,
@@ -1457,6 +1818,7 @@ export {
1457
1818
  err,
1458
1819
  errAsync,
1459
1820
  extractDgiiErrorMessage,
1821
+ formatViolations,
1460
1822
  fromPromise,
1461
1823
  fromSafePromise,
1462
1824
  fromThrowable,
@@ -1471,9 +1833,11 @@ export {
1471
1833
  isRetryableStatus,
1472
1834
  ok,
1473
1835
  okAsync,
1836
+ pruneEmptyValues,
1474
1837
  resolveReceptionRoute,
1475
1838
  roundHalfUpToCents,
1476
1839
  toDgiiError,
1840
+ validateDocument,
1477
1841
  validateXMLCertificate
1478
1842
  };
1479
1843
  //# sourceMappingURL=index.js.map