@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.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]);
@@ -146,9 +177,10 @@ var readMensajes = (value) => {
146
177
  if (typeof item === "string" && item.trim()) {
147
178
  out.push({ valor: item.trim(), codigo: 0 });
148
179
  } else if (isObject(item) && typeof item.valor === "string") {
180
+ const codigo = item.codigo;
149
181
  out.push({
150
182
  valor: item.valor,
151
- codigo: typeof item.codigo === "number" ? item.codigo : 0
183
+ codigo: typeof codigo === "number" || typeof codigo === "string" ? codigo : 0
152
184
  });
153
185
  }
154
186
  }
@@ -465,9 +497,9 @@ var HttpClient = class {
465
497
  }
466
498
  return url;
467
499
  }
468
- authHeaders(extra) {
500
+ authHeaders(extra, omitAuth = false) {
469
501
  const headers = { Accept: "application/json" };
470
- if (this.token) {
502
+ if (!omitAuth && this.token) {
471
503
  headers.Authorization = `Bearer ${this.token}`;
472
504
  }
473
505
  return { ...headers, ...extra };
@@ -520,7 +552,7 @@ var HttpClient = class {
520
552
  async execute(options) {
521
553
  const url = this.buildUrl(options.path, options.absoluteUrl, options.query);
522
554
  const method = options.method ?? "GET";
523
- const headers = this.authHeaders(options.headers);
555
+ const headers = this.authHeaders(options.headers, options.omitAuth);
524
556
  const ctx = {
525
557
  method,
526
558
  resource: options.resource ?? options.path,
@@ -842,18 +874,43 @@ var roundHalfUpToCents = (value) => {
842
874
  const padded = rounded.padStart(3, "0");
843
875
  return `${sign}${padded.slice(0, -2)}.${padded.slice(-2)}`;
844
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
+ };
845
902
  var Transformer = class {
846
903
  json2xml(dataObject, round = false) {
847
904
  try {
905
+ const pruned = pruneEmptyValues(dataObject);
848
906
  const data = {
849
907
  _declaration: { _attributes: { version: "1.0", encoding: "utf-8" } },
850
- ...dataObject
908
+ ...pruned !== null && pruned !== void 0 && typeof pruned === "object" && !Array.isArray(pruned) ? pruned : {}
851
909
  };
852
910
  const xml = import_xml_js.default.js2xml(data, {
853
911
  compact: true,
854
912
  ignoreComment: true,
855
913
  spaces: 2,
856
- fullTagEmptyElement: true,
857
914
  textFn: (value, label) => {
858
915
  if (round && label.toLowerCase() !== "version") {
859
916
  return roundHalfUpToCents(value);
@@ -1004,6 +1061,191 @@ var resolveReceptionRoute = (signedXml) => {
1004
1061
  )
1005
1062
  ) : (0, import_neverthrow.ok)(GENERAL);
1006
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
+ };
1007
1249
 
1008
1250
  // src/utils.ts
1009
1251
  var getCodeSixDigitfromSignature = (signedXml) => {
@@ -1063,31 +1305,6 @@ var toArray = (value) => {
1063
1305
  }
1064
1306
  return Array.isArray(value) ? value : [value];
1065
1307
  };
1066
- var isEmptyObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0;
1067
- var removeEmptyValues = (input) => {
1068
- if (input === null || input === void 0 || input === "") {
1069
- return void 0;
1070
- }
1071
- if (Array.isArray(input)) {
1072
- const filtered = input.map(removeEmptyValues).filter(
1073
- (item) => item !== null && item !== void 0 && item !== "" && !isEmptyObject(item)
1074
- );
1075
- return filtered.length ? filtered : void 0;
1076
- }
1077
- if (typeof input === "object") {
1078
- const output = {};
1079
- for (const [key, value] of Object.entries(
1080
- input
1081
- )) {
1082
- const cleaned = removeEmptyValues(value);
1083
- if (cleaned !== void 0 && cleaned !== "" && !isEmptyObject(cleaned)) {
1084
- output[key] = cleaned;
1085
- }
1086
- }
1087
- return Object.keys(output).length ? output : void 0;
1088
- }
1089
- return input;
1090
- };
1091
1308
  var convertECF32ToRFCE = (ecf32Xml) => {
1092
1309
  const securityCode = getCodeSixDigitfromSignature(ecf32Xml);
1093
1310
  if (securityCode.isErr()) {
@@ -1102,7 +1319,7 @@ var convertECF32ToRFCE = (ecf32Xml) => {
1102
1319
  const impuestos = toArray(
1103
1320
  encabezado?.Totales?.ImpuestosAdicionales?.ImpuestoAdicional
1104
1321
  );
1105
- const rfce = removeEmptyValues({
1322
+ const rfce = pruneEmptyValues({
1106
1323
  RFCE: {
1107
1324
  Encabezado: {
1108
1325
  Version: encabezado?.Version,
@@ -1182,6 +1399,10 @@ var ECF = class {
1182
1399
  inFlight = /* @__PURE__ */ new Map();
1183
1400
  tokenRefreshSkewMs;
1184
1401
  now;
1402
+ statusApiKey;
1403
+ enforceB2BRules;
1404
+ validateBeforeSend;
1405
+ xmllintBin;
1185
1406
  tokenExpiresAt;
1186
1407
  refreshPromise;
1187
1408
  constructor(credentials, environment = "TesteCF" /* Dev */, options = {}) {
@@ -1189,6 +1410,10 @@ var ECF = class {
1189
1410
  this.environment = environment;
1190
1411
  this.tokenRefreshSkewMs = options.tokenRefreshSkewMs ?? 5 * 60 * 1e3;
1191
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;
1192
1417
  this.idempotency = options.idempotencyStore ?? new InMemoryIdempotencyStore();
1193
1418
  this.http = new HttpClient({
1194
1419
  baseUrl: BaseUrl.ECF,
@@ -1203,8 +1428,8 @@ var ECF = class {
1203
1428
  });
1204
1429
  }
1205
1430
  buildIdempotencyKey(signedXml) {
1206
- const rnc = extractElementText(signedXml, "RNCEmisor") ?? "unknown-rnc";
1207
- 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";
1208
1433
  return `${rnc}:${ncf}:${sha256Hex(signedXml)}`;
1209
1434
  }
1210
1435
  /**
@@ -1219,10 +1444,7 @@ var ECF = class {
1219
1444
  if (cached !== void 0) {
1220
1445
  if (isPendingSubmission(cached)) {
1221
1446
  return (0, import_neverthrow.err)(
1222
- new DgiiError(
1223
- "A submission with this key is already pending; reconcile via a status lookup before retrying",
1224
- { code: "IDEMPOTENCY" }
1225
- )
1447
+ new DgiiError(PENDING_SUBMISSION_MESSAGE, { code: "IDEMPOTENCY" })
1226
1448
  );
1227
1449
  }
1228
1450
  return (0, import_neverthrow.ok)(cached);
@@ -1314,6 +1536,90 @@ var ECF = class {
1314
1536
  this.tokenExpiresAt = parseExpiry(token.value.expira);
1315
1537
  return (0, import_neverthrow.ok)(token.value);
1316
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
+ }
1317
1623
  getSeed(buyerHost) {
1318
1624
  if (buyerHost) {
1319
1625
  const url2 = buildSenderReceiverUrl(
@@ -1346,6 +1652,29 @@ var ECF = class {
1346
1652
  * a repeated call returns the cached response instead of double-issuing.
1347
1653
  */
1348
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
+ }
1349
1678
  const key = this.buildIdempotencyKey(signedXml);
1350
1679
  return this.submitIdempotent(key, () => {
1351
1680
  const url = buyerHost ? buildSenderReceiverUrl(buyerHost, "fe/recepcion/api/ecf" /* SendInvoice */) : buildUrl(this.environment, "recepcion/api/FacturasElectronicas" /* SendInvoice */);
@@ -1433,6 +1762,22 @@ var ECF = class {
1433
1762
  }
1434
1763
  return (0, import_neverthrow.ok)(Array.isArray(data) ? data : [data]);
1435
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
+ }
1436
1781
  /** RFCE inquiry — DGII production only. */
1437
1782
  async getSummaryInvoiceInquiry(rnc, encf, codSeguridad) {
1438
1783
  if (this.environment !== "eCF" /* Prod */) {
@@ -1454,12 +1799,32 @@ var ECF = class {
1454
1799
  { absoluteUrl: true, resource: "consultarfce/api/Consultas/Consulta" /* InquiryInvoiceSummary */ }
1455
1800
  );
1456
1801
  }
1457
- /**
1458
- * Submit a signed commercial approval (ACECF). Idempotent like submissions,
1459
- * keyed on `RNC + eNCF + SHA-256(signed XML)`.
1460
- */
1461
- sendCommercialApproval(signedXml, fileName) {
1802
+ sendCommercialApproval(signedXml, fileName, buyerHost) {
1462
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
+ }
1463
1828
  return this.submitIdempotent(
1464
1829
  key,
1465
1830
  () => this.http.postMultipart(
@@ -1501,13 +1866,24 @@ var ECF = class {
1501
1866
  InMemoryIdempotencyStore,
1502
1867
  Ok,
1503
1868
  P12Reader,
1869
+ PENDING_SUBMISSION_MESSAGE,
1504
1870
  ResultAsync,
1871
+ SCHEMA_DIR,
1872
+ SUMMARY_ACCEPTED,
1873
+ SUMMARY_CONDITIONAL_ACCEPTED,
1874
+ SUMMARY_REJECTED,
1505
1875
  SenderReceiverEndpoint,
1506
1876
  Signature,
1877
+ StatusEndpoint,
1878
+ StatusEnvironment,
1507
1879
  TrackStatusEnum,
1508
1880
  Transformer,
1881
+ UNAVAILABLE_SCHEMAS,
1509
1882
  buildSenderReceiverUrl,
1883
+ buildStatusUrl,
1510
1884
  buildUrl,
1885
+ canReuseSequence,
1886
+ checkB2BApplicability,
1511
1887
  compareDecimal,
1512
1888
  convertECF32ToRFCE,
1513
1889
  createResiliencePolicies,
@@ -1515,6 +1891,7 @@ var ECF = class {
1515
1891
  err,
1516
1892
  errAsync,
1517
1893
  extractDgiiErrorMessage,
1894
+ formatViolations,
1518
1895
  fromPromise,
1519
1896
  fromSafePromise,
1520
1897
  fromThrowable,
@@ -1529,9 +1906,11 @@ var ECF = class {
1529
1906
  isRetryableStatus,
1530
1907
  ok,
1531
1908
  okAsync,
1909
+ pruneEmptyValues,
1532
1910
  resolveReceptionRoute,
1533
1911
  roundHalfUpToCents,
1534
1912
  toDgiiError,
1913
+ validateDocument,
1535
1914
  validateXMLCertificate
1536
1915
  });
1537
1916
  //# sourceMappingURL=index.cjs.map