@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.cjs CHANGED
@@ -42,13 +42,24 @@ __export(index_exports, {
42
42
  InMemoryIdempotencyStore: () => InMemoryIdempotencyStore,
43
43
  Ok: () => import_neverthrow.Ok,
44
44
  P12Reader: () => P12Reader,
45
+ PENDING_SUBMISSION_MESSAGE: () => PENDING_SUBMISSION_MESSAGE,
45
46
  ResultAsync: () => import_neverthrow.ResultAsync,
47
+ SCHEMA_DIR: () => SCHEMA_DIR,
48
+ SUMMARY_ACCEPTED: () => SUMMARY_ACCEPTED,
49
+ SUMMARY_CONDITIONAL_ACCEPTED: () => SUMMARY_CONDITIONAL_ACCEPTED,
50
+ SUMMARY_REJECTED: () => SUMMARY_REJECTED,
46
51
  SenderReceiverEndpoint: () => SenderReceiverEndpoint,
47
52
  Signature: () => Signature,
53
+ StatusEndpoint: () => StatusEndpoint,
54
+ StatusEnvironment: () => StatusEnvironment,
48
55
  TrackStatusEnum: () => TrackStatusEnum,
49
56
  Transformer: () => Transformer,
57
+ UNAVAILABLE_SCHEMAS: () => UNAVAILABLE_SCHEMAS,
50
58
  buildSenderReceiverUrl: () => buildSenderReceiverUrl,
59
+ buildStatusUrl: () => buildStatusUrl,
51
60
  buildUrl: () => buildUrl,
61
+ canReuseSequence: () => canReuseSequence,
62
+ checkB2BApplicability: () => checkB2BApplicability,
52
63
  compareDecimal: () => compareDecimal,
53
64
  convertECF32ToRFCE: () => convertECF32ToRFCE,
54
65
  createResiliencePolicies: () => createResiliencePolicies,
@@ -56,6 +67,7 @@ __export(index_exports, {
56
67
  err: () => import_neverthrow.err,
57
68
  errAsync: () => import_neverthrow.errAsync,
58
69
  extractDgiiErrorMessage: () => extractDgiiErrorMessage,
70
+ formatViolations: () => formatViolations,
59
71
  fromPromise: () => import_neverthrow.fromPromise,
60
72
  fromSafePromise: () => import_neverthrow.fromSafePromise,
61
73
  fromThrowable: () => import_neverthrow.fromThrowable,
@@ -70,9 +82,11 @@ __export(index_exports, {
70
82
  isRetryableStatus: () => isRetryableStatus,
71
83
  ok: () => import_neverthrow.ok,
72
84
  okAsync: () => import_neverthrow.okAsync,
85
+ pruneEmptyValues: () => pruneEmptyValues,
73
86
  resolveReceptionRoute: () => resolveReceptionRoute,
74
87
  roundHalfUpToCents: () => roundHalfUpToCents,
75
88
  toDgiiError: () => toDgiiError,
89
+ validateDocument: () => validateDocument,
76
90
  validateXMLCertificate: () => validateXMLCertificate
77
91
  });
78
92
  module.exports = __toCommonJS(index_exports);
@@ -103,6 +117,7 @@ var Endpoint = /* @__PURE__ */ ((Endpoint2) => {
103
117
  Endpoint2["InquiryStatus"] = "consultaestado/api/Consultas/Estado";
104
118
  Endpoint2["AllTrackingEcf"] = "ConsultaTrackIds/api/TrackIds/Consulta";
105
119
  Endpoint2["Directory"] = "consultadirectorio/api/consultas/obtenerdirectorioporrnc";
120
+ Endpoint2["DirectoryList"] = "consultadirectorio/api/consultas/listado";
106
121
  Endpoint2["Void"] = "anulacionrangos/api/operaciones/anularrango";
107
122
  return Endpoint2;
108
123
  })(Endpoint || {});
@@ -113,6 +128,19 @@ var SenderReceiverEndpoint = /* @__PURE__ */ ((SenderReceiverEndpoint2) => {
113
128
  SenderReceiverEndpoint2["CommercialApproval"] = "fe/aprobacioncomercial/api/ecf";
114
129
  return SenderReceiverEndpoint2;
115
130
  })(SenderReceiverEndpoint || {});
131
+ var StatusEndpoint = /* @__PURE__ */ ((StatusEndpoint2) => {
132
+ StatusEndpoint2["Services"] = "api/estatusservicios/obtenerestatus";
133
+ StatusEndpoint2["MaintenanceWindows"] = "api/estatusservicios/obtenerventanasmantenimiento";
134
+ StatusEndpoint2["Check"] = "api/estatusservicios/verificarestado";
135
+ return StatusEndpoint2;
136
+ })(StatusEndpoint || {});
137
+ var StatusEnvironment = /* @__PURE__ */ ((StatusEnvironment3) => {
138
+ StatusEnvironment3[StatusEnvironment3["PreCertification"] = 1] = "PreCertification";
139
+ StatusEnvironment3[StatusEnvironment3["Production"] = 2] = "Production";
140
+ StatusEnvironment3[StatusEnvironment3["Certification"] = 3] = "Certification";
141
+ return StatusEnvironment3;
142
+ })(StatusEnvironment || {});
143
+ var buildStatusUrl = (endpoint, baseUrl = BaseUrl.STATUS) => `${baseUrl.replace(/\/+$/, "")}/${endpoint}`;
116
144
  var buildUrl = (environment, endpoint, baseUrl = BaseUrl.ECF) => `${baseUrl.replace(/\/+$/, "")}/${environment}/${endpoint}`;
117
145
  var buildSenderReceiverUrl = (host, endpoint) => `${host.replace(/\/+$/, "")}/${endpoint}`;
118
146
 
@@ -125,6 +153,9 @@ var TrackStatusEnum = /* @__PURE__ */ ((TrackStatusEnum2) => {
125
153
  TrackStatusEnum2["ConditionalAccepted"] = "Aceptado Condicional";
126
154
  return TrackStatusEnum2;
127
155
  })(TrackStatusEnum || {});
156
+ var SUMMARY_ACCEPTED = "Aceptado";
157
+ var SUMMARY_CONDITIONAL_ACCEPTED = "Aceptado condicional";
158
+ var SUMMARY_REJECTED = "Rechazado";
128
159
 
129
160
  // src/errors.ts
130
161
  var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
@@ -466,9 +497,9 @@ var HttpClient = class {
466
497
  }
467
498
  return url;
468
499
  }
469
- authHeaders(extra) {
500
+ authHeaders(extra, omitAuth = false) {
470
501
  const headers = { Accept: "application/json" };
471
- if (this.token) {
502
+ if (!omitAuth && this.token) {
472
503
  headers.Authorization = `Bearer ${this.token}`;
473
504
  }
474
505
  return { ...headers, ...extra };
@@ -521,7 +552,7 @@ var HttpClient = class {
521
552
  async execute(options) {
522
553
  const url = this.buildUrl(options.path, options.absoluteUrl, options.query);
523
554
  const method = options.method ?? "GET";
524
- const headers = this.authHeaders(options.headers);
555
+ const headers = this.authHeaders(options.headers, options.omitAuth);
525
556
  const ctx = {
526
557
  method,
527
558
  resource: options.resource ?? options.path,
@@ -843,18 +874,43 @@ var roundHalfUpToCents = (value) => {
843
874
  const padded = rounded.padStart(3, "0");
844
875
  return `${sign}${padded.slice(0, -2)}.${padded.slice(-2)}`;
845
876
  };
877
+ var isEmptyObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0;
878
+ var pruneEmptyValues = (input) => {
879
+ if (input === null || input === void 0 || input === "") {
880
+ return void 0;
881
+ }
882
+ if (Array.isArray(input)) {
883
+ const filtered = input.map(pruneEmptyValues).filter(
884
+ (item) => item !== null && item !== void 0 && item !== "" && !isEmptyObject(item)
885
+ );
886
+ return filtered.length ? filtered : void 0;
887
+ }
888
+ if (typeof input === "object") {
889
+ const output = {};
890
+ for (const [key, value] of Object.entries(
891
+ input
892
+ )) {
893
+ const cleaned = pruneEmptyValues(value);
894
+ if (cleaned !== void 0 && cleaned !== "" && !isEmptyObject(cleaned)) {
895
+ output[key] = cleaned;
896
+ }
897
+ }
898
+ return Object.keys(output).length ? output : void 0;
899
+ }
900
+ return input;
901
+ };
846
902
  var Transformer = class {
847
903
  json2xml(dataObject, round = false) {
848
904
  try {
905
+ const pruned = pruneEmptyValues(dataObject);
849
906
  const data = {
850
907
  _declaration: { _attributes: { version: "1.0", encoding: "utf-8" } },
851
- ...dataObject
908
+ ...pruned !== null && pruned !== void 0 && typeof pruned === "object" && !Array.isArray(pruned) ? pruned : {}
852
909
  };
853
910
  const xml = import_xml_js.default.js2xml(data, {
854
911
  compact: true,
855
912
  ignoreComment: true,
856
913
  spaces: 2,
857
- fullTagEmptyElement: true,
858
914
  textFn: (value, label) => {
859
915
  if (round && label.toLowerCase() !== "version") {
860
916
  return roundHalfUpToCents(value);
@@ -1005,6 +1061,191 @@ var resolveReceptionRoute = (signedXml) => {
1005
1061
  )
1006
1062
  ) : (0, import_neverthrow.ok)(GENERAL);
1007
1063
  };
1064
+ var B2B_EXCLUDED_TYPES = [
1065
+ "41",
1066
+ "43",
1067
+ "45",
1068
+ "46",
1069
+ "47"
1070
+ ];
1071
+ var checkB2BApplicability = (signedXml) => {
1072
+ const root = extractRootElementName(signedXml);
1073
+ if (root === "RFCE") {
1074
+ return (0, import_neverthrow.err)(
1075
+ validationError(
1076
+ "An RFCE summary cannot be delivered directly to a buyer host"
1077
+ )
1078
+ );
1079
+ }
1080
+ if (root !== "ECF") {
1081
+ return (0, import_neverthrow.err)(
1082
+ validationError(
1083
+ `Unsupported document root "${root ?? "unknown"}"; expected "ECF"`
1084
+ )
1085
+ );
1086
+ }
1087
+ const tipo = extractElementText(signedXml, "TipoeCF");
1088
+ if (tipo === void 0) {
1089
+ return (0, import_neverthrow.err)(validationError("TipoeCF is missing from the document"));
1090
+ }
1091
+ if (!isEcfTypeCode(tipo)) {
1092
+ return (0, import_neverthrow.err)(validationError(`Unknown TipoeCF "${tipo}"`));
1093
+ }
1094
+ if (B2B_EXCLUDED_TYPES.includes(tipo)) {
1095
+ return (0, import_neverthrow.err)(
1096
+ validationError(
1097
+ `An e-CF of type ${tipo} cannot be delivered directly to a buyer host`
1098
+ )
1099
+ );
1100
+ }
1101
+ if (tipo !== CONSUMPTION_TYPE) {
1102
+ return (0, import_neverthrow.ok)(void 0);
1103
+ }
1104
+ const total = extractElementText(signedXml, "MontoTotal");
1105
+ if (total === void 0) {
1106
+ return (0, import_neverthrow.err)(
1107
+ validationError(
1108
+ "MontoTotal is required to check B2B applicability of an E32"
1109
+ )
1110
+ );
1111
+ }
1112
+ if (!DECIMAL_PATTERN.test(total)) {
1113
+ return (0, import_neverthrow.err)(
1114
+ validationError(`MontoTotal "${total}" is not a decimal amount`)
1115
+ );
1116
+ }
1117
+ if (compareDecimal(total, CONSUMPTION_SUMMARY_THRESHOLD) < 0) {
1118
+ return (0, import_neverthrow.err)(
1119
+ validationError(
1120
+ "An E32 below RD$250,000 cannot be delivered directly to a buyer host; send the RFCE summary to DGII instead"
1121
+ )
1122
+ );
1123
+ }
1124
+ return (0, import_neverthrow.ok)(void 0);
1125
+ };
1126
+
1127
+ // src/reconciliation.ts
1128
+ var PENDING_SUBMISSION_MESSAGE = "A submission with this key is already pending; call reconcileSubmission() before retrying";
1129
+ var canReuseSequence = (response) => response.secuenciaUtilizada === false;
1130
+
1131
+ // src/validation.ts
1132
+ var import_node_child_process = require("child_process");
1133
+ var import_node_fs2 = require("fs");
1134
+ var import_node_path = require("path");
1135
+ var import_node_url = require("url");
1136
+ var import_meta = {};
1137
+ var moduleDir = (() => {
1138
+ if (typeof __dirname !== "undefined") {
1139
+ return __dirname;
1140
+ }
1141
+ return (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
1142
+ })();
1143
+ var SCHEMA_DIR = [moduleDir, (0, import_node_path.resolve)(moduleDir, "..", "schemas")].find(
1144
+ (candidate) => (0, import_node_fs2.existsSync)((0, import_node_path.resolve)(candidate, "e-cf-32.xsd"))
1145
+ ) ?? (0, import_node_path.resolve)(moduleDir, "..", "schemas");
1146
+ var SCHEMA_FILES = {
1147
+ "ECF|32": "e-cf-32.xsd",
1148
+ "ECF|33": "e-cf-33.xsd",
1149
+ "ECF|34": "e-cf-34.xsd",
1150
+ "ECF|41": "e-cf-41.xsd",
1151
+ "ECF|43": "e-cf-43.xsd",
1152
+ "ECF|44": "e-cf-44.xsd",
1153
+ "ECF|45": "e-cf-45.xsd",
1154
+ "ECF|46": "e-cf-46.xsd",
1155
+ "ECF|47": "e-cf-47.xsd",
1156
+ "ANECF|": "anecf.xsd",
1157
+ "ARECF|": "arecf.xsd",
1158
+ "SemillaModel|": "semilla.xsd"
1159
+ };
1160
+ var UNAVAILABLE_SCHEMAS = {
1161
+ "ECF|31": "the published schema references IndicadorServicioTodoIncluidoType, which does not resolve",
1162
+ "RFCE|": "the published schema has an invalid pattern facet: [$0-9] and the \\- escape are not valid XML Schema regex",
1163
+ "ACECF|": "the published schema has an invalid pattern facet: the \\- escape is not valid XML Schema regex"
1164
+ };
1165
+ var schemaKey = (xml) => {
1166
+ const root = extractRootElementName(xml);
1167
+ if (root === void 0) {
1168
+ return void 0;
1169
+ }
1170
+ if (root !== "ECF") {
1171
+ return `${root}|`;
1172
+ }
1173
+ const tipo = extractElementText(xml, "TipoeCF");
1174
+ return tipo === void 0 ? void 0 : `ECF|${tipo}`;
1175
+ };
1176
+ var validationError2 = (message, cause) => new DgiiError(message, {
1177
+ code: "VALIDATION",
1178
+ ...cause !== void 0 ? { cause } : {}
1179
+ });
1180
+ var VIOLATION_LINE = /^.*?:(\d+):\s*element\s+([^:]+):\s*(.*)$/;
1181
+ var parseViolations = (stderr) => {
1182
+ const violations = [];
1183
+ for (const raw of stderr.split("\n")) {
1184
+ const line = raw.trim();
1185
+ if (line === "" || line.endsWith("fails to validate") || line.endsWith("validates")) {
1186
+ continue;
1187
+ }
1188
+ const match = VIOLATION_LINE.exec(line);
1189
+ if (match !== null) {
1190
+ violations.push({
1191
+ path: (match[2] ?? "").trim(),
1192
+ message: (match[3] ?? "").trim()
1193
+ });
1194
+ } else {
1195
+ violations.push({ path: "", message: line });
1196
+ }
1197
+ }
1198
+ return violations;
1199
+ };
1200
+ var validateDocument = (xml, options = {}) => {
1201
+ const key = schemaKey(xml);
1202
+ if (key === void 0) {
1203
+ return (0, import_neverthrow.err)(
1204
+ validationError2("Cannot determine the document type to validate")
1205
+ );
1206
+ }
1207
+ const file = SCHEMA_FILES[key];
1208
+ if (file === void 0) {
1209
+ const reason = UNAVAILABLE_SCHEMAS[key] ?? "no bundled XML Schema for this document type";
1210
+ const [root, tipo] = key.split("|");
1211
+ const label = tipo ? `${root} ${tipo}` : root ?? key;
1212
+ return (0, import_neverthrow.err)(validationError2(`Schema unavailable for ${label}: ${reason}`));
1213
+ }
1214
+ const bin = options.bin ?? process.env["DGII_XMLLINT_BIN"] ?? "xmllint";
1215
+ const result = (0, import_node_child_process.spawnSync)(
1216
+ bin,
1217
+ ["--noout", "--schema", (0, import_node_path.resolve)(SCHEMA_DIR, file), "-"],
1218
+ { input: xml, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }
1219
+ );
1220
+ if (result.error !== void 0 && result.error !== null) {
1221
+ const code = result.error.code;
1222
+ return (0, import_neverthrow.err)(
1223
+ validationError2(
1224
+ 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}`,
1225
+ result.error
1226
+ )
1227
+ );
1228
+ }
1229
+ if (result.status === 0) {
1230
+ return (0, import_neverthrow.ok)([]);
1231
+ }
1232
+ const violations = parseViolations(result.stderr ?? "");
1233
+ return (0, import_neverthrow.ok)(
1234
+ violations.length > 0 ? violations : [
1235
+ {
1236
+ path: "",
1237
+ message: (result.stderr ?? "").trim() || "Validation failed"
1238
+ }
1239
+ ]
1240
+ );
1241
+ };
1242
+ var formatViolations = (violations) => {
1243
+ const shown = violations.slice(0, 5).map(
1244
+ (violation) => violation.path ? `${violation.path}: ${violation.message}` : violation.message
1245
+ ).join("; ");
1246
+ const remaining = violations.length - 5;
1247
+ return remaining > 0 ? `${shown} (+${remaining} more)` : shown;
1248
+ };
1008
1249
 
1009
1250
  // src/utils.ts
1010
1251
  var getCodeSixDigitfromSignature = (signedXml) => {
@@ -1064,31 +1305,6 @@ var toArray = (value) => {
1064
1305
  }
1065
1306
  return Array.isArray(value) ? value : [value];
1066
1307
  };
1067
- var isEmptyObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0;
1068
- var removeEmptyValues = (input) => {
1069
- if (input === null || input === void 0 || input === "") {
1070
- return void 0;
1071
- }
1072
- if (Array.isArray(input)) {
1073
- const filtered = input.map(removeEmptyValues).filter(
1074
- (item) => item !== null && item !== void 0 && item !== "" && !isEmptyObject(item)
1075
- );
1076
- return filtered.length ? filtered : void 0;
1077
- }
1078
- if (typeof input === "object") {
1079
- const output = {};
1080
- for (const [key, value] of Object.entries(
1081
- input
1082
- )) {
1083
- const cleaned = removeEmptyValues(value);
1084
- if (cleaned !== void 0 && cleaned !== "" && !isEmptyObject(cleaned)) {
1085
- output[key] = cleaned;
1086
- }
1087
- }
1088
- return Object.keys(output).length ? output : void 0;
1089
- }
1090
- return input;
1091
- };
1092
1308
  var convertECF32ToRFCE = (ecf32Xml) => {
1093
1309
  const securityCode = getCodeSixDigitfromSignature(ecf32Xml);
1094
1310
  if (securityCode.isErr()) {
@@ -1103,7 +1319,7 @@ var convertECF32ToRFCE = (ecf32Xml) => {
1103
1319
  const impuestos = toArray(
1104
1320
  encabezado?.Totales?.ImpuestosAdicionales?.ImpuestoAdicional
1105
1321
  );
1106
- const rfce = removeEmptyValues({
1322
+ const rfce = pruneEmptyValues({
1107
1323
  RFCE: {
1108
1324
  Encabezado: {
1109
1325
  Version: encabezado?.Version,
@@ -1183,6 +1399,10 @@ var ECF = class {
1183
1399
  inFlight = /* @__PURE__ */ new Map();
1184
1400
  tokenRefreshSkewMs;
1185
1401
  now;
1402
+ statusApiKey;
1403
+ enforceB2BRules;
1404
+ validateBeforeSend;
1405
+ xmllintBin;
1186
1406
  tokenExpiresAt;
1187
1407
  refreshPromise;
1188
1408
  constructor(credentials, environment = "TesteCF" /* Dev */, options = {}) {
@@ -1190,6 +1410,10 @@ var ECF = class {
1190
1410
  this.environment = environment;
1191
1411
  this.tokenRefreshSkewMs = options.tokenRefreshSkewMs ?? 5 * 60 * 1e3;
1192
1412
  this.now = options.now ?? Date.now;
1413
+ this.statusApiKey = options.statusApiKey;
1414
+ this.enforceB2BRules = options.enforceB2BRules ?? true;
1415
+ this.validateBeforeSend = options.validateBeforeSend ?? false;
1416
+ this.xmllintBin = options.xmllintBin;
1193
1417
  this.idempotency = options.idempotencyStore ?? new InMemoryIdempotencyStore();
1194
1418
  this.http = new HttpClient({
1195
1419
  baseUrl: BaseUrl.ECF,
@@ -1204,8 +1428,8 @@ var ECF = class {
1204
1428
  });
1205
1429
  }
1206
1430
  buildIdempotencyKey(signedXml) {
1207
- const rnc = extractElementText(signedXml, "RNCEmisor") ?? "unknown-rnc";
1208
- const ncf = extractElementText(signedXml, "eNCF") ?? extractElementText(signedXml, "RangoNCF") ?? "unknown-ncf";
1431
+ const rnc = extractElementText(signedXml, "RNCEmisor") ?? extractElementText(signedXml, "RncEmisor") ?? "unknown-rnc";
1432
+ const ncf = extractElementText(signedXml, "eNCF") ?? extractElementText(signedXml, "SecuenciaeNCFDesde") ?? extractElementText(signedXml, "RangoNCF") ?? "unknown-ncf";
1209
1433
  return `${rnc}:${ncf}:${sha256Hex(signedXml)}`;
1210
1434
  }
1211
1435
  /**
@@ -1220,10 +1444,7 @@ var ECF = class {
1220
1444
  if (cached !== void 0) {
1221
1445
  if (isPendingSubmission(cached)) {
1222
1446
  return (0, import_neverthrow.err)(
1223
- new DgiiError(
1224
- "A submission with this key is already pending; reconcile via a status lookup before retrying",
1225
- { code: "IDEMPOTENCY" }
1226
- )
1447
+ new DgiiError(PENDING_SUBMISSION_MESSAGE, { code: "IDEMPOTENCY" })
1227
1448
  );
1228
1449
  }
1229
1450
  return (0, import_neverthrow.ok)(cached);
@@ -1315,6 +1536,90 @@ var ECF = class {
1315
1536
  this.tokenExpiresAt = parseExpiry(token.value.expira);
1316
1537
  return (0, import_neverthrow.ok)(token.value);
1317
1538
  }
1539
+ /**
1540
+ * Query the operational status of every DGII service.
1541
+ *
1542
+ * The status host takes an API key instead of a bearer token, so this call
1543
+ * neither needs `authenticate()` nor triggers a token refresh.
1544
+ */
1545
+ async getServiceStatus() {
1546
+ const result = await this.statusRequest("api/estatusservicios/obtenerestatus" /* Services */);
1547
+ if (result.isErr()) {
1548
+ return (0, import_neverthrow.err)(result.error);
1549
+ }
1550
+ const data = result.value;
1551
+ if (data === void 0 || data === null) {
1552
+ return (0, import_neverthrow.ok)([]);
1553
+ }
1554
+ return (0, import_neverthrow.ok)(Array.isArray(data) ? data : [data]);
1555
+ }
1556
+ /** Scheduled maintenance windows, one entry per environment. */
1557
+ async getMaintenanceWindows() {
1558
+ const result = await this.statusRequest("api/estatusservicios/obtenerventanasmantenimiento" /* MaintenanceWindows */);
1559
+ if (result.isErr()) {
1560
+ return (0, import_neverthrow.err)(result.error);
1561
+ }
1562
+ const data = result.value;
1563
+ if (Array.isArray(data)) {
1564
+ return (0, import_neverthrow.ok)(data);
1565
+ }
1566
+ return (0, import_neverthrow.ok)(data?.ventanaMantenimientos ?? []);
1567
+ }
1568
+ /** Whether an environment is currently inside a maintenance window. */
1569
+ checkServiceAvailability(ambiente) {
1570
+ return this.statusRequest(
1571
+ "api/estatusservicios/verificarestado" /* Check */,
1572
+ { ambiente }
1573
+ );
1574
+ }
1575
+ statusRequest(endpoint, query) {
1576
+ const headers = this.statusApiKey === void 0 ? {} : { Authorization: `Apikey ${this.statusApiKey}` };
1577
+ return this.http.request({
1578
+ method: "GET",
1579
+ path: buildStatusUrl(endpoint),
1580
+ absoluteUrl: true,
1581
+ resource: `status:${endpoint}`,
1582
+ headers,
1583
+ ...query !== void 0 ? { query } : {},
1584
+ skipAuthHooks: true,
1585
+ omitAuth: true
1586
+ });
1587
+ }
1588
+ /**
1589
+ * Resolve what happened to a submission whose idempotency marker is pending.
1590
+ *
1591
+ * Queries DGII by e-NCF. When DGII has no record of the document the marker
1592
+ * is cleared, because re-submitting cannot double-issue. When DGII does have
1593
+ * a record the marker stays in place — it is the lock that prevents a second
1594
+ * submission of a document that already exists — and `delivered` tells the
1595
+ * caller to stop retrying.
1596
+ */
1597
+ async reconcileSubmission(signedXml) {
1598
+ const rnc = extractElementText(signedXml, "RNCEmisor") ?? extractElementText(signedXml, "RncEmisor");
1599
+ const encf = extractElementText(signedXml, "eNCF") ?? extractElementText(signedXml, "SecuenciaeNCFDesde");
1600
+ if (rnc === void 0 || encf === void 0) {
1601
+ return (0, import_neverthrow.err)(
1602
+ new DgiiError("The document has no emitter RNC or e-NCF to reconcile", {
1603
+ code: "VALIDATION"
1604
+ })
1605
+ );
1606
+ }
1607
+ const inquiry = await this.inquiryStatus(rnc, encf);
1608
+ if (inquiry.isErr()) {
1609
+ return (0, import_neverthrow.err)(inquiry.error);
1610
+ }
1611
+ const delivered = inquiry.value.estado !== "No encontrado";
1612
+ const cleared = !delivered;
1613
+ if (cleared) {
1614
+ await this.idempotency.delete(this.buildIdempotencyKey(signedXml));
1615
+ }
1616
+ return (0, import_neverthrow.ok)({
1617
+ inquiry: inquiry.value,
1618
+ delivered,
1619
+ canReuseSequence: canReuseSequence(inquiry.value),
1620
+ cleared
1621
+ });
1622
+ }
1318
1623
  getSeed(buyerHost) {
1319
1624
  if (buyerHost) {
1320
1625
  const url2 = buildSenderReceiverUrl(
@@ -1347,6 +1652,29 @@ var ECF = class {
1347
1652
  * a repeated call returns the cached response instead of double-issuing.
1348
1653
  */
1349
1654
  async sendElectronicDocument(signedXml, fileName, buyerHost) {
1655
+ if (buyerHost !== void 0 && this.enforceB2BRules) {
1656
+ const applicable = checkB2BApplicability(signedXml);
1657
+ if (applicable.isErr()) {
1658
+ return (0, import_neverthrow.err)(applicable.error);
1659
+ }
1660
+ }
1661
+ if (this.validateBeforeSend) {
1662
+ const validation = validateDocument(
1663
+ signedXml,
1664
+ this.xmllintBin !== void 0 ? { bin: this.xmllintBin } : {}
1665
+ );
1666
+ if (validation.isErr()) {
1667
+ return (0, import_neverthrow.err)(validation.error);
1668
+ }
1669
+ if (validation.value.length > 0) {
1670
+ return (0, import_neverthrow.err)(
1671
+ new DgiiError(
1672
+ `Document does not match the DGII XML Schema: ${formatViolations(validation.value)}`,
1673
+ { code: "VALIDATION", data: validation.value }
1674
+ )
1675
+ );
1676
+ }
1677
+ }
1350
1678
  const key = this.buildIdempotencyKey(signedXml);
1351
1679
  return this.submitIdempotent(key, () => {
1352
1680
  const url = buyerHost ? buildSenderReceiverUrl(buyerHost, "fe/recepcion/api/ecf" /* SendInvoice */) : buildUrl(this.environment, "recepcion/api/FacturasElectronicas" /* SendInvoice */);
@@ -1434,6 +1762,22 @@ var ECF = class {
1434
1762
  }
1435
1763
  return (0, import_neverthrow.ok)(Array.isArray(data) ? data : [data]);
1436
1764
  }
1765
+ /** General directory listing — every receiver that publishes service URLs. */
1766
+ async getDirectoryListing() {
1767
+ const url = buildUrl(this.environment, "consultadirectorio/api/consultas/listado" /* DirectoryList */);
1768
+ const result = await this.http.get(url, void 0, {
1769
+ absoluteUrl: true,
1770
+ resource: "consultadirectorio/api/consultas/listado" /* DirectoryList */
1771
+ });
1772
+ if (result.isErr()) {
1773
+ return (0, import_neverthrow.err)(result.error);
1774
+ }
1775
+ const data = result.value;
1776
+ if (data === void 0 || data === null) {
1777
+ return (0, import_neverthrow.ok)([]);
1778
+ }
1779
+ return (0, import_neverthrow.ok)(Array.isArray(data) ? data : [data]);
1780
+ }
1437
1781
  /** RFCE inquiry — DGII production only. */
1438
1782
  async getSummaryInvoiceInquiry(rnc, encf, codSeguridad) {
1439
1783
  if (this.environment !== "eCF" /* Prod */) {
@@ -1455,12 +1799,32 @@ var ECF = class {
1455
1799
  { absoluteUrl: true, resource: "consultarfce/api/Consultas/Consulta" /* InquiryInvoiceSummary */ }
1456
1800
  );
1457
1801
  }
1458
- /**
1459
- * Submit a signed commercial approval (ACECF). Idempotent like submissions,
1460
- * keyed on `RNC + eNCF + SHA-256(signed XML)`.
1461
- */
1462
- sendCommercialApproval(signedXml, fileName) {
1802
+ sendCommercialApproval(signedXml, fileName, buyerHost) {
1463
1803
  const key = this.buildIdempotencyKey(signedXml);
1804
+ if (buyerHost !== void 0) {
1805
+ return this.submitIdempotent(
1806
+ key,
1807
+ async () => {
1808
+ const url = buildSenderReceiverUrl(
1809
+ buyerHost,
1810
+ "fe/aprobacioncomercial/api/ecf" /* CommercialApproval */
1811
+ );
1812
+ const result = await this.http.postMultipart(
1813
+ url,
1814
+ fileName,
1815
+ signedXml,
1816
+ {
1817
+ absoluteUrl: true,
1818
+ resource: "fe/aprobacioncomercial/api/ecf" /* CommercialApproval */
1819
+ }
1820
+ );
1821
+ if (result.isErr()) {
1822
+ return result.error.status === 400 ? (0, import_neverthrow.ok)({ status: 400, accepted: false }) : (0, import_neverthrow.err)(result.error);
1823
+ }
1824
+ return (0, import_neverthrow.ok)({ status: 200, accepted: true });
1825
+ }
1826
+ );
1827
+ }
1464
1828
  return this.submitIdempotent(
1465
1829
  key,
1466
1830
  () => this.http.postMultipart(
@@ -1502,13 +1866,24 @@ var ECF = class {
1502
1866
  InMemoryIdempotencyStore,
1503
1867
  Ok,
1504
1868
  P12Reader,
1869
+ PENDING_SUBMISSION_MESSAGE,
1505
1870
  ResultAsync,
1871
+ SCHEMA_DIR,
1872
+ SUMMARY_ACCEPTED,
1873
+ SUMMARY_CONDITIONAL_ACCEPTED,
1874
+ SUMMARY_REJECTED,
1506
1875
  SenderReceiverEndpoint,
1507
1876
  Signature,
1877
+ StatusEndpoint,
1878
+ StatusEnvironment,
1508
1879
  TrackStatusEnum,
1509
1880
  Transformer,
1881
+ UNAVAILABLE_SCHEMAS,
1510
1882
  buildSenderReceiverUrl,
1883
+ buildStatusUrl,
1511
1884
  buildUrl,
1885
+ canReuseSequence,
1886
+ checkB2BApplicability,
1512
1887
  compareDecimal,
1513
1888
  convertECF32ToRFCE,
1514
1889
  createResiliencePolicies,
@@ -1516,6 +1891,7 @@ var ECF = class {
1516
1891
  err,
1517
1892
  errAsync,
1518
1893
  extractDgiiErrorMessage,
1894
+ formatViolations,
1519
1895
  fromPromise,
1520
1896
  fromSafePromise,
1521
1897
  fromThrowable,
@@ -1530,9 +1906,11 @@ var ECF = class {
1530
1906
  isRetryableStatus,
1531
1907
  ok,
1532
1908
  okAsync,
1909
+ pruneEmptyValues,
1533
1910
  resolveReceptionRoute,
1534
1911
  roundHalfUpToCents,
1535
1912
  toDgiiError,
1913
+ validateDocument,
1536
1914
  validateXMLCertificate
1537
1915
  });
1538
1916
  //# sourceMappingURL=index.cjs.map