@deuriib/dgii-ecf 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +211 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +83 -1
- package/dist/index.d.ts +83 -1
- package/dist/index.js +206 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -259,6 +259,10 @@ interface HttpClientOptions {
|
|
|
259
259
|
policies?: ResiliencePolicies;
|
|
260
260
|
timeoutMs?: number;
|
|
261
261
|
fetchImpl?: typeof fetch;
|
|
262
|
+
/** Runs before every authenticated request; may refresh credentials. */
|
|
263
|
+
beforeRequest?: () => Promise<void>;
|
|
264
|
+
/** Runs on a 401; return `true` to replay the request once. */
|
|
265
|
+
onUnauthorized?: () => Promise<boolean>;
|
|
262
266
|
}
|
|
263
267
|
interface RequestOptions {
|
|
264
268
|
method?: 'GET' | 'POST';
|
|
@@ -269,6 +273,8 @@ interface RequestOptions {
|
|
|
269
273
|
body?: BodyInit;
|
|
270
274
|
query?: Record<string, string | number | undefined>;
|
|
271
275
|
resource?: string;
|
|
276
|
+
/** Skip the auth hooks (used by the authentication round-trip itself). */
|
|
277
|
+
skipAuthHooks?: boolean;
|
|
272
278
|
}
|
|
273
279
|
/** Thin HTTP client over the native `fetch`, wrapped in resilience policies. */
|
|
274
280
|
declare class HttpClient {
|
|
@@ -276,17 +282,22 @@ declare class HttpClient {
|
|
|
276
282
|
private readonly policies;
|
|
277
283
|
private readonly fetchImpl;
|
|
278
284
|
private readonly baseUrl;
|
|
285
|
+
private readonly beforeRequest;
|
|
286
|
+
private readonly onUnauthorized;
|
|
279
287
|
constructor(options: HttpClientOptions);
|
|
280
288
|
setToken(token: string): void;
|
|
289
|
+
getToken(): string | undefined;
|
|
281
290
|
clearToken(): void;
|
|
282
291
|
private buildUrl;
|
|
283
292
|
private authHeaders;
|
|
284
293
|
private readBody;
|
|
285
294
|
private parseResponse;
|
|
286
295
|
request<T>(options: RequestOptions): Promise<Result<T, DgiiError>>;
|
|
296
|
+
private execute;
|
|
287
297
|
get<T>(path: string, query?: RequestOptions['query'], opts?: {
|
|
288
298
|
absoluteUrl?: boolean;
|
|
289
299
|
resource?: string;
|
|
300
|
+
skipAuthHooks?: boolean;
|
|
290
301
|
}): Promise<Result<T, DgiiError>>;
|
|
291
302
|
/**
|
|
292
303
|
* POST a signed XML as a multipart `xml` file part. The runtime computes
|
|
@@ -295,6 +306,7 @@ declare class HttpClient {
|
|
|
295
306
|
postMultipart<T>(path: string, fileName: string, xml: string, opts?: {
|
|
296
307
|
absoluteUrl?: boolean;
|
|
297
308
|
resource?: string;
|
|
309
|
+
skipAuthHooks?: boolean;
|
|
298
310
|
}): Promise<Result<T, DgiiError>>;
|
|
299
311
|
}
|
|
300
312
|
|
|
@@ -364,6 +376,45 @@ declare class Transformer {
|
|
|
364
376
|
xml2json<T>(xml: string): Result<T, DgiiError>;
|
|
365
377
|
}
|
|
366
378
|
|
|
379
|
+
/** Every e-CF type code defined by DGII. */
|
|
380
|
+
declare const ECF_TYPE_CODES: readonly ["31", "32", "33", "34", "41", "43", "44", "45", "46", "47"];
|
|
381
|
+
type EcfTypeCode = (typeof ECF_TYPE_CODES)[number];
|
|
382
|
+
/**
|
|
383
|
+
* Consumption invoices (E32) at or above this amount go to the general
|
|
384
|
+
* reception endpoint; below it they must be sent as an RFCE summary.
|
|
385
|
+
*/
|
|
386
|
+
declare const CONSUMPTION_SUMMARY_THRESHOLD = "250000";
|
|
387
|
+
interface GeneralReceptionRoute {
|
|
388
|
+
readonly kind: 'general';
|
|
389
|
+
readonly endpoint: Endpoint.SendInvoice;
|
|
390
|
+
}
|
|
391
|
+
interface SummaryReceptionRoute {
|
|
392
|
+
readonly kind: 'summary';
|
|
393
|
+
readonly endpoint: Endpoint.SendSummary;
|
|
394
|
+
}
|
|
395
|
+
type ReceptionRoute = GeneralReceptionRoute | SummaryReceptionRoute;
|
|
396
|
+
declare const isEcfTypeCode: (value: string) => value is EcfTypeCode;
|
|
397
|
+
/**
|
|
398
|
+
* Compare two non-negative decimal strings without going through `number`, so
|
|
399
|
+
* a money amount never picks up binary floating-point error.
|
|
400
|
+
*
|
|
401
|
+
* Returns a negative number when `a < b`, zero when equal, positive otherwise.
|
|
402
|
+
*/
|
|
403
|
+
declare const compareDecimal: (a: string, b: string) => number;
|
|
404
|
+
/**
|
|
405
|
+
* Decide which DGII reception endpoint a signed document belongs to.
|
|
406
|
+
*
|
|
407
|
+
* - `<RFCE>` → the consumption-summary reception (`recepcionfc`).
|
|
408
|
+
* - `<ECF>` with any type other than E32 → the general reception.
|
|
409
|
+
* - `<ECF>` type E32 at or above RD$250,000 → the general reception.
|
|
410
|
+
* - `<ECF>` type E32 below RD$250,000 → rejected: DGII only accepts the
|
|
411
|
+
* summary form for those, so the caller must run `convertECF32ToRFCE` first.
|
|
412
|
+
*
|
|
413
|
+
* The decision reads `TipoeCF` and `MontoTotal` from the signed document, so it
|
|
414
|
+
* cannot drift from what is actually being transmitted.
|
|
415
|
+
*/
|
|
416
|
+
declare const resolveReceptionRoute: (signedXml: string) => Result<ReceptionRoute, DgiiError>;
|
|
417
|
+
|
|
367
418
|
/** First 6 chars of the `<SignatureValue>` (DGII security code). */
|
|
368
419
|
declare const getCodeSixDigitfromSignature: (signedXml: string) => Result<string, DgiiError>;
|
|
369
420
|
/** QR URL for a factura de consumo (FC). */
|
|
@@ -394,6 +445,13 @@ interface EcfOptions {
|
|
|
394
445
|
timeoutMs?: number;
|
|
395
446
|
/** Injectable fetch (useful for tests). */
|
|
396
447
|
fetchImpl?: typeof fetch;
|
|
448
|
+
/**
|
|
449
|
+
* Refresh the token when its remaining lifetime falls below this window.
|
|
450
|
+
* Defaults to 5 minutes.
|
|
451
|
+
*/
|
|
452
|
+
tokenRefreshSkewMs?: number;
|
|
453
|
+
/** Injectable clock (epoch milliseconds); useful for tests. */
|
|
454
|
+
now?: () => number;
|
|
397
455
|
}
|
|
398
456
|
/**
|
|
399
457
|
* DGII e-CF client. Every method returns a `Result` — no thrown domain errors.
|
|
@@ -404,6 +462,10 @@ declare class ECF {
|
|
|
404
462
|
private readonly idempotency;
|
|
405
463
|
private readonly credentials;
|
|
406
464
|
private readonly inFlight;
|
|
465
|
+
private readonly tokenRefreshSkewMs;
|
|
466
|
+
private readonly now;
|
|
467
|
+
private tokenExpiresAt;
|
|
468
|
+
private refreshPromise;
|
|
407
469
|
constructor(credentials: P12ReaderData, environment?: Environment, options?: EcfOptions);
|
|
408
470
|
private buildIdempotencyKey;
|
|
409
471
|
/**
|
|
@@ -415,6 +477,17 @@ declare class ECF {
|
|
|
415
477
|
*/
|
|
416
478
|
private submitIdempotent;
|
|
417
479
|
private singleFlight;
|
|
480
|
+
/**
|
|
481
|
+
* Refresh the token when it is missing or about to expire.
|
|
482
|
+
*
|
|
483
|
+
* A pre-supplied `token` has no known expiry, so it is never refreshed
|
|
484
|
+
* proactively — only a 401 triggers a re-authentication for it.
|
|
485
|
+
*/
|
|
486
|
+
private ensureToken;
|
|
487
|
+
/** Authenticate, coalescing concurrent refreshes into a single round-trip. */
|
|
488
|
+
private refreshToken;
|
|
489
|
+
/** Drop the rejected token and authenticate once. Returns `true` on success. */
|
|
490
|
+
private reauthenticate;
|
|
418
491
|
/** Authenticate against DGII (or a buyer host) and store the bearer token. */
|
|
419
492
|
authenticate(buyerHost?: string): Promise<Result<AuthToken, DgiiError>>;
|
|
420
493
|
private getSeed;
|
|
@@ -426,6 +499,15 @@ declare class ECF {
|
|
|
426
499
|
sendElectronicDocument(signedXml: string, fileName: string, buyerHost?: string): Promise<Result<InvoiceResponse, DgiiError>>;
|
|
427
500
|
/** Submit a signed summary invoice (RFCE). Idempotent like submissions. */
|
|
428
501
|
sendSummary(signedXml: string, fileName: string): Promise<Result<InvoiceSummaryResponse, DgiiError>>;
|
|
502
|
+
/**
|
|
503
|
+
* Submit a signed document to the endpoint its own content belongs to.
|
|
504
|
+
*
|
|
505
|
+
* Reads the root element, `TipoeCF` and `MontoTotal` from the signed XML and
|
|
506
|
+
* dispatches to {@link sendSummary} for an RFCE or to
|
|
507
|
+
* {@link sendElectronicDocument} for an ECF. An E32 below RD$250,000 is
|
|
508
|
+
* rejected because DGII only accepts the summary form for those.
|
|
509
|
+
*/
|
|
510
|
+
sendDocument(signedXml: string, fileName: string, buyerHost?: string): Promise<Result<InvoiceResponse | InvoiceSummaryResponse, DgiiError>>;
|
|
429
511
|
statusTrackId(trackId: string): Promise<Result<TrackingStatusResponse, DgiiError>>;
|
|
430
512
|
inquiryStatus(rncEmisor: string, ncfElectronico: string, rncComprador?: string, codigoSeguridad?: string): Promise<Result<InquiryStatusResponse, DgiiError>>;
|
|
431
513
|
trackStatuses(rncEmisor: string, encf: string): Promise<Result<SummaryTrackingStatusResponse[], DgiiError>>;
|
|
@@ -445,4 +527,4 @@ declare class ECF {
|
|
|
445
527
|
voidENCF(signedXml: string, fileName: string): Promise<Result<VoidNCFResponse, DgiiError>>;
|
|
446
528
|
}
|
|
447
529
|
|
|
448
|
-
export { type AuthToken, BaseUrl, type BaseUrlKey, type CertificateInfo, type CommercialApprovalResponse, type DgiiDocumentType, DgiiError, type DgiiErrorCode, type DgiiErrorContext, type DgiiErrorOptions, type DgiiResult, ECF, type EcfOptions, Endpoint, Environment, HttpClient, type HttpClientOptions, type IdempotencyStore, InMemoryIdempotencyStore, type InMemoryIdempotencyStoreOptions, type InquiryInvoiceSummary, type InquiryStatus, type InquiryStatusCode, type InquiryStatusResponse, type InvoiceResponse, type InvoiceSummaryResponse, type Mensaje, P12Reader, type P12ReaderData, type RequestOptions, type ResilienceOptions, type ResiliencePolicies, SenderReceiverEndpoint, type ServiceDirectoryResponse, Signature, type SummaryTrackingStatusResponse, type TrackStatusCode, TrackStatusEnum, type TrackingStatusResponse, Transformer, type VoidNCFResponse, type XmlCertificateValidation, buildSenderReceiverUrl, buildUrl, convertECF32ToRFCE, createResiliencePolicies, defaultResilienceOptions, extractDgiiErrorMessage, generateEcfQRCodeURL, generateFcQRCodeURL, getCodeSixDigitfromSignature, getCurrentFormattedDate, getCurrentFormattedDateTime, getXmlFromBodyResponse, isRetryableError, isRetryableStatus, roundHalfUpToCents, toDgiiError, validateXMLCertificate };
|
|
530
|
+
export { type AuthToken, BaseUrl, type BaseUrlKey, CONSUMPTION_SUMMARY_THRESHOLD, type CertificateInfo, type CommercialApprovalResponse, type DgiiDocumentType, DgiiError, type DgiiErrorCode, type DgiiErrorContext, type DgiiErrorOptions, type DgiiResult, ECF, ECF_TYPE_CODES, type EcfOptions, type EcfTypeCode, Endpoint, Environment, type GeneralReceptionRoute, HttpClient, type HttpClientOptions, type IdempotencyStore, InMemoryIdempotencyStore, type InMemoryIdempotencyStoreOptions, type InquiryInvoiceSummary, type InquiryStatus, type InquiryStatusCode, type InquiryStatusResponse, type InvoiceResponse, type InvoiceSummaryResponse, type Mensaje, P12Reader, type P12ReaderData, type ReceptionRoute, type RequestOptions, type ResilienceOptions, type ResiliencePolicies, SenderReceiverEndpoint, type ServiceDirectoryResponse, Signature, type SummaryReceptionRoute, type SummaryTrackingStatusResponse, type TrackStatusCode, TrackStatusEnum, type TrackingStatusResponse, Transformer, type VoidNCFResponse, type XmlCertificateValidation, buildSenderReceiverUrl, buildUrl, compareDecimal, convertECF32ToRFCE, createResiliencePolicies, defaultResilienceOptions, extractDgiiErrorMessage, generateEcfQRCodeURL, generateFcQRCodeURL, getCodeSixDigitfromSignature, getCurrentFormattedDate, getCurrentFormattedDateTime, getXmlFromBodyResponse, isEcfTypeCode, isRetryableError, isRetryableStatus, resolveReceptionRoute, roundHalfUpToCents, toDgiiError, validateXMLCertificate };
|
package/dist/index.d.ts
CHANGED
|
@@ -259,6 +259,10 @@ interface HttpClientOptions {
|
|
|
259
259
|
policies?: ResiliencePolicies;
|
|
260
260
|
timeoutMs?: number;
|
|
261
261
|
fetchImpl?: typeof fetch;
|
|
262
|
+
/** Runs before every authenticated request; may refresh credentials. */
|
|
263
|
+
beforeRequest?: () => Promise<void>;
|
|
264
|
+
/** Runs on a 401; return `true` to replay the request once. */
|
|
265
|
+
onUnauthorized?: () => Promise<boolean>;
|
|
262
266
|
}
|
|
263
267
|
interface RequestOptions {
|
|
264
268
|
method?: 'GET' | 'POST';
|
|
@@ -269,6 +273,8 @@ interface RequestOptions {
|
|
|
269
273
|
body?: BodyInit;
|
|
270
274
|
query?: Record<string, string | number | undefined>;
|
|
271
275
|
resource?: string;
|
|
276
|
+
/** Skip the auth hooks (used by the authentication round-trip itself). */
|
|
277
|
+
skipAuthHooks?: boolean;
|
|
272
278
|
}
|
|
273
279
|
/** Thin HTTP client over the native `fetch`, wrapped in resilience policies. */
|
|
274
280
|
declare class HttpClient {
|
|
@@ -276,17 +282,22 @@ declare class HttpClient {
|
|
|
276
282
|
private readonly policies;
|
|
277
283
|
private readonly fetchImpl;
|
|
278
284
|
private readonly baseUrl;
|
|
285
|
+
private readonly beforeRequest;
|
|
286
|
+
private readonly onUnauthorized;
|
|
279
287
|
constructor(options: HttpClientOptions);
|
|
280
288
|
setToken(token: string): void;
|
|
289
|
+
getToken(): string | undefined;
|
|
281
290
|
clearToken(): void;
|
|
282
291
|
private buildUrl;
|
|
283
292
|
private authHeaders;
|
|
284
293
|
private readBody;
|
|
285
294
|
private parseResponse;
|
|
286
295
|
request<T>(options: RequestOptions): Promise<Result<T, DgiiError>>;
|
|
296
|
+
private execute;
|
|
287
297
|
get<T>(path: string, query?: RequestOptions['query'], opts?: {
|
|
288
298
|
absoluteUrl?: boolean;
|
|
289
299
|
resource?: string;
|
|
300
|
+
skipAuthHooks?: boolean;
|
|
290
301
|
}): Promise<Result<T, DgiiError>>;
|
|
291
302
|
/**
|
|
292
303
|
* POST a signed XML as a multipart `xml` file part. The runtime computes
|
|
@@ -295,6 +306,7 @@ declare class HttpClient {
|
|
|
295
306
|
postMultipart<T>(path: string, fileName: string, xml: string, opts?: {
|
|
296
307
|
absoluteUrl?: boolean;
|
|
297
308
|
resource?: string;
|
|
309
|
+
skipAuthHooks?: boolean;
|
|
298
310
|
}): Promise<Result<T, DgiiError>>;
|
|
299
311
|
}
|
|
300
312
|
|
|
@@ -364,6 +376,45 @@ declare class Transformer {
|
|
|
364
376
|
xml2json<T>(xml: string): Result<T, DgiiError>;
|
|
365
377
|
}
|
|
366
378
|
|
|
379
|
+
/** Every e-CF type code defined by DGII. */
|
|
380
|
+
declare const ECF_TYPE_CODES: readonly ["31", "32", "33", "34", "41", "43", "44", "45", "46", "47"];
|
|
381
|
+
type EcfTypeCode = (typeof ECF_TYPE_CODES)[number];
|
|
382
|
+
/**
|
|
383
|
+
* Consumption invoices (E32) at or above this amount go to the general
|
|
384
|
+
* reception endpoint; below it they must be sent as an RFCE summary.
|
|
385
|
+
*/
|
|
386
|
+
declare const CONSUMPTION_SUMMARY_THRESHOLD = "250000";
|
|
387
|
+
interface GeneralReceptionRoute {
|
|
388
|
+
readonly kind: 'general';
|
|
389
|
+
readonly endpoint: Endpoint.SendInvoice;
|
|
390
|
+
}
|
|
391
|
+
interface SummaryReceptionRoute {
|
|
392
|
+
readonly kind: 'summary';
|
|
393
|
+
readonly endpoint: Endpoint.SendSummary;
|
|
394
|
+
}
|
|
395
|
+
type ReceptionRoute = GeneralReceptionRoute | SummaryReceptionRoute;
|
|
396
|
+
declare const isEcfTypeCode: (value: string) => value is EcfTypeCode;
|
|
397
|
+
/**
|
|
398
|
+
* Compare two non-negative decimal strings without going through `number`, so
|
|
399
|
+
* a money amount never picks up binary floating-point error.
|
|
400
|
+
*
|
|
401
|
+
* Returns a negative number when `a < b`, zero when equal, positive otherwise.
|
|
402
|
+
*/
|
|
403
|
+
declare const compareDecimal: (a: string, b: string) => number;
|
|
404
|
+
/**
|
|
405
|
+
* Decide which DGII reception endpoint a signed document belongs to.
|
|
406
|
+
*
|
|
407
|
+
* - `<RFCE>` → the consumption-summary reception (`recepcionfc`).
|
|
408
|
+
* - `<ECF>` with any type other than E32 → the general reception.
|
|
409
|
+
* - `<ECF>` type E32 at or above RD$250,000 → the general reception.
|
|
410
|
+
* - `<ECF>` type E32 below RD$250,000 → rejected: DGII only accepts the
|
|
411
|
+
* summary form for those, so the caller must run `convertECF32ToRFCE` first.
|
|
412
|
+
*
|
|
413
|
+
* The decision reads `TipoeCF` and `MontoTotal` from the signed document, so it
|
|
414
|
+
* cannot drift from what is actually being transmitted.
|
|
415
|
+
*/
|
|
416
|
+
declare const resolveReceptionRoute: (signedXml: string) => Result<ReceptionRoute, DgiiError>;
|
|
417
|
+
|
|
367
418
|
/** First 6 chars of the `<SignatureValue>` (DGII security code). */
|
|
368
419
|
declare const getCodeSixDigitfromSignature: (signedXml: string) => Result<string, DgiiError>;
|
|
369
420
|
/** QR URL for a factura de consumo (FC). */
|
|
@@ -394,6 +445,13 @@ interface EcfOptions {
|
|
|
394
445
|
timeoutMs?: number;
|
|
395
446
|
/** Injectable fetch (useful for tests). */
|
|
396
447
|
fetchImpl?: typeof fetch;
|
|
448
|
+
/**
|
|
449
|
+
* Refresh the token when its remaining lifetime falls below this window.
|
|
450
|
+
* Defaults to 5 minutes.
|
|
451
|
+
*/
|
|
452
|
+
tokenRefreshSkewMs?: number;
|
|
453
|
+
/** Injectable clock (epoch milliseconds); useful for tests. */
|
|
454
|
+
now?: () => number;
|
|
397
455
|
}
|
|
398
456
|
/**
|
|
399
457
|
* DGII e-CF client. Every method returns a `Result` — no thrown domain errors.
|
|
@@ -404,6 +462,10 @@ declare class ECF {
|
|
|
404
462
|
private readonly idempotency;
|
|
405
463
|
private readonly credentials;
|
|
406
464
|
private readonly inFlight;
|
|
465
|
+
private readonly tokenRefreshSkewMs;
|
|
466
|
+
private readonly now;
|
|
467
|
+
private tokenExpiresAt;
|
|
468
|
+
private refreshPromise;
|
|
407
469
|
constructor(credentials: P12ReaderData, environment?: Environment, options?: EcfOptions);
|
|
408
470
|
private buildIdempotencyKey;
|
|
409
471
|
/**
|
|
@@ -415,6 +477,17 @@ declare class ECF {
|
|
|
415
477
|
*/
|
|
416
478
|
private submitIdempotent;
|
|
417
479
|
private singleFlight;
|
|
480
|
+
/**
|
|
481
|
+
* Refresh the token when it is missing or about to expire.
|
|
482
|
+
*
|
|
483
|
+
* A pre-supplied `token` has no known expiry, so it is never refreshed
|
|
484
|
+
* proactively — only a 401 triggers a re-authentication for it.
|
|
485
|
+
*/
|
|
486
|
+
private ensureToken;
|
|
487
|
+
/** Authenticate, coalescing concurrent refreshes into a single round-trip. */
|
|
488
|
+
private refreshToken;
|
|
489
|
+
/** Drop the rejected token and authenticate once. Returns `true` on success. */
|
|
490
|
+
private reauthenticate;
|
|
418
491
|
/** Authenticate against DGII (or a buyer host) and store the bearer token. */
|
|
419
492
|
authenticate(buyerHost?: string): Promise<Result<AuthToken, DgiiError>>;
|
|
420
493
|
private getSeed;
|
|
@@ -426,6 +499,15 @@ declare class ECF {
|
|
|
426
499
|
sendElectronicDocument(signedXml: string, fileName: string, buyerHost?: string): Promise<Result<InvoiceResponse, DgiiError>>;
|
|
427
500
|
/** Submit a signed summary invoice (RFCE). Idempotent like submissions. */
|
|
428
501
|
sendSummary(signedXml: string, fileName: string): Promise<Result<InvoiceSummaryResponse, DgiiError>>;
|
|
502
|
+
/**
|
|
503
|
+
* Submit a signed document to the endpoint its own content belongs to.
|
|
504
|
+
*
|
|
505
|
+
* Reads the root element, `TipoeCF` and `MontoTotal` from the signed XML and
|
|
506
|
+
* dispatches to {@link sendSummary} for an RFCE or to
|
|
507
|
+
* {@link sendElectronicDocument} for an ECF. An E32 below RD$250,000 is
|
|
508
|
+
* rejected because DGII only accepts the summary form for those.
|
|
509
|
+
*/
|
|
510
|
+
sendDocument(signedXml: string, fileName: string, buyerHost?: string): Promise<Result<InvoiceResponse | InvoiceSummaryResponse, DgiiError>>;
|
|
429
511
|
statusTrackId(trackId: string): Promise<Result<TrackingStatusResponse, DgiiError>>;
|
|
430
512
|
inquiryStatus(rncEmisor: string, ncfElectronico: string, rncComprador?: string, codigoSeguridad?: string): Promise<Result<InquiryStatusResponse, DgiiError>>;
|
|
431
513
|
trackStatuses(rncEmisor: string, encf: string): Promise<Result<SummaryTrackingStatusResponse[], DgiiError>>;
|
|
@@ -445,4 +527,4 @@ declare class ECF {
|
|
|
445
527
|
voidENCF(signedXml: string, fileName: string): Promise<Result<VoidNCFResponse, DgiiError>>;
|
|
446
528
|
}
|
|
447
529
|
|
|
448
|
-
export { type AuthToken, BaseUrl, type BaseUrlKey, type CertificateInfo, type CommercialApprovalResponse, type DgiiDocumentType, DgiiError, type DgiiErrorCode, type DgiiErrorContext, type DgiiErrorOptions, type DgiiResult, ECF, type EcfOptions, Endpoint, Environment, HttpClient, type HttpClientOptions, type IdempotencyStore, InMemoryIdempotencyStore, type InMemoryIdempotencyStoreOptions, type InquiryInvoiceSummary, type InquiryStatus, type InquiryStatusCode, type InquiryStatusResponse, type InvoiceResponse, type InvoiceSummaryResponse, type Mensaje, P12Reader, type P12ReaderData, type RequestOptions, type ResilienceOptions, type ResiliencePolicies, SenderReceiverEndpoint, type ServiceDirectoryResponse, Signature, type SummaryTrackingStatusResponse, type TrackStatusCode, TrackStatusEnum, type TrackingStatusResponse, Transformer, type VoidNCFResponse, type XmlCertificateValidation, buildSenderReceiverUrl, buildUrl, convertECF32ToRFCE, createResiliencePolicies, defaultResilienceOptions, extractDgiiErrorMessage, generateEcfQRCodeURL, generateFcQRCodeURL, getCodeSixDigitfromSignature, getCurrentFormattedDate, getCurrentFormattedDateTime, getXmlFromBodyResponse, isRetryableError, isRetryableStatus, roundHalfUpToCents, toDgiiError, validateXMLCertificate };
|
|
530
|
+
export { type AuthToken, BaseUrl, type BaseUrlKey, CONSUMPTION_SUMMARY_THRESHOLD, type CertificateInfo, type CommercialApprovalResponse, type DgiiDocumentType, DgiiError, type DgiiErrorCode, type DgiiErrorContext, type DgiiErrorOptions, type DgiiResult, ECF, ECF_TYPE_CODES, type EcfOptions, type EcfTypeCode, Endpoint, Environment, type GeneralReceptionRoute, HttpClient, type HttpClientOptions, type IdempotencyStore, InMemoryIdempotencyStore, type InMemoryIdempotencyStoreOptions, type InquiryInvoiceSummary, type InquiryStatus, type InquiryStatusCode, type InquiryStatusResponse, type InvoiceResponse, type InvoiceSummaryResponse, type Mensaje, P12Reader, type P12ReaderData, type ReceptionRoute, type RequestOptions, type ResilienceOptions, type ResiliencePolicies, SenderReceiverEndpoint, type ServiceDirectoryResponse, Signature, type SummaryReceptionRoute, type SummaryTrackingStatusResponse, type TrackStatusCode, TrackStatusEnum, type TrackingStatusResponse, Transformer, type VoidNCFResponse, type XmlCertificateValidation, buildSenderReceiverUrl, buildUrl, compareDecimal, convertECF32ToRFCE, createResiliencePolicies, defaultResilienceOptions, extractDgiiErrorMessage, generateEcfQRCodeURL, generateFcQRCodeURL, getCodeSixDigitfromSignature, getCurrentFormattedDate, getCurrentFormattedDateTime, getXmlFromBodyResponse, isEcfTypeCode, isRetryableError, isRetryableStatus, resolveReceptionRoute, roundHalfUpToCents, toDgiiError, validateXMLCertificate };
|
package/dist/index.js
CHANGED
|
@@ -370,9 +370,13 @@ var HttpClient = class {
|
|
|
370
370
|
policies;
|
|
371
371
|
fetchImpl;
|
|
372
372
|
baseUrl;
|
|
373
|
+
beforeRequest;
|
|
374
|
+
onUnauthorized;
|
|
373
375
|
constructor(options) {
|
|
374
376
|
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
375
377
|
this.token = options.token;
|
|
378
|
+
this.beforeRequest = options.beforeRequest;
|
|
379
|
+
this.onUnauthorized = options.onUnauthorized;
|
|
376
380
|
this.policies = options.policies ?? createResiliencePolicies({
|
|
377
381
|
...options.resilience,
|
|
378
382
|
...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {}
|
|
@@ -382,6 +386,9 @@ var HttpClient = class {
|
|
|
382
386
|
setToken(token) {
|
|
383
387
|
this.token = token;
|
|
384
388
|
}
|
|
389
|
+
getToken() {
|
|
390
|
+
return this.token;
|
|
391
|
+
}
|
|
385
392
|
clearToken() {
|
|
386
393
|
this.token = void 0;
|
|
387
394
|
}
|
|
@@ -441,6 +448,19 @@ var HttpClient = class {
|
|
|
441
448
|
return err(error);
|
|
442
449
|
}
|
|
443
450
|
async request(options) {
|
|
451
|
+
if (!options.skipAuthHooks) {
|
|
452
|
+
await this.beforeRequest?.();
|
|
453
|
+
}
|
|
454
|
+
const first = await this.execute(options);
|
|
455
|
+
if (!options.skipAuthHooks && first.isErr() && first.error.status === 401 && this.onUnauthorized !== void 0) {
|
|
456
|
+
const replayed = await this.onUnauthorized();
|
|
457
|
+
if (replayed) {
|
|
458
|
+
return this.execute(options);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return first;
|
|
462
|
+
}
|
|
463
|
+
async execute(options) {
|
|
444
464
|
const url = this.buildUrl(options.path, options.absoluteUrl, options.query);
|
|
445
465
|
const method = options.method ?? "GET";
|
|
446
466
|
const headers = this.authHeaders(options.headers);
|
|
@@ -469,7 +489,8 @@ var HttpClient = class {
|
|
|
469
489
|
path,
|
|
470
490
|
query,
|
|
471
491
|
absoluteUrl: opts?.absoluteUrl,
|
|
472
|
-
resource: opts?.resource
|
|
492
|
+
resource: opts?.resource,
|
|
493
|
+
skipAuthHooks: opts?.skipAuthHooks
|
|
473
494
|
});
|
|
474
495
|
}
|
|
475
496
|
/**
|
|
@@ -485,7 +506,8 @@ var HttpClient = class {
|
|
|
485
506
|
body: form,
|
|
486
507
|
headers: { Accept: "application/json" },
|
|
487
508
|
absoluteUrl: opts?.absoluteUrl,
|
|
488
|
-
resource: opts?.resource
|
|
509
|
+
resource: opts?.resource,
|
|
510
|
+
skipAuthHooks: opts?.skipAuthHooks
|
|
489
511
|
});
|
|
490
512
|
}
|
|
491
513
|
};
|
|
@@ -833,6 +855,98 @@ var extractElementText = (xml, tag) => {
|
|
|
833
855
|
const text = match?.[1]?.trim();
|
|
834
856
|
return text === void 0 || text === "" ? void 0 : text;
|
|
835
857
|
};
|
|
858
|
+
var ROOT_PATTERN = /<\s*(?:[A-Za-z_][\w.-]*:)?([A-Za-z_][\w.-]*)[\s/>]/;
|
|
859
|
+
var extractRootElementName = (xml) => {
|
|
860
|
+
const stripped = xml.replace(/<\?[\s\S]*?\?>/g, "").replace(/<!--[\s\S]*?-->/g, "").trimStart();
|
|
861
|
+
return ROOT_PATTERN.exec(stripped)?.[1];
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
// src/reception.ts
|
|
865
|
+
var ECF_TYPE_CODES = [
|
|
866
|
+
"31",
|
|
867
|
+
"32",
|
|
868
|
+
"33",
|
|
869
|
+
"34",
|
|
870
|
+
"41",
|
|
871
|
+
"43",
|
|
872
|
+
"44",
|
|
873
|
+
"45",
|
|
874
|
+
"46",
|
|
875
|
+
"47"
|
|
876
|
+
];
|
|
877
|
+
var CONSUMPTION_SUMMARY_THRESHOLD = "250000";
|
|
878
|
+
var CONSUMPTION_TYPE = "32";
|
|
879
|
+
var GENERAL = {
|
|
880
|
+
kind: "general",
|
|
881
|
+
endpoint: "recepcion/api/FacturasElectronicas" /* SendInvoice */
|
|
882
|
+
};
|
|
883
|
+
var SUMMARY = {
|
|
884
|
+
kind: "summary",
|
|
885
|
+
endpoint: "recepcionfc/api/recepcion/ecf" /* SendSummary */
|
|
886
|
+
};
|
|
887
|
+
var isEcfTypeCode = (value) => ECF_TYPE_CODES.includes(value);
|
|
888
|
+
var DECIMAL_PATTERN = /^\d+(?:\.\d+)?$/;
|
|
889
|
+
var compareDecimal = (a, b) => {
|
|
890
|
+
const [aInt = "", aFrac = ""] = a.split(".");
|
|
891
|
+
const [bInt = "", bFrac = ""] = b.split(".");
|
|
892
|
+
const aWhole = aInt.replace(/^0+(?=\d)/, "");
|
|
893
|
+
const bWhole = bInt.replace(/^0+(?=\d)/, "");
|
|
894
|
+
if (aWhole.length !== bWhole.length) {
|
|
895
|
+
return aWhole.length - bWhole.length;
|
|
896
|
+
}
|
|
897
|
+
if (aWhole !== bWhole) {
|
|
898
|
+
return aWhole < bWhole ? -1 : 1;
|
|
899
|
+
}
|
|
900
|
+
const width = Math.max(aFrac.length, bFrac.length);
|
|
901
|
+
const aPadded = aFrac.padEnd(width, "0");
|
|
902
|
+
const bPadded = bFrac.padEnd(width, "0");
|
|
903
|
+
if (aPadded === bPadded) {
|
|
904
|
+
return 0;
|
|
905
|
+
}
|
|
906
|
+
return aPadded < bPadded ? -1 : 1;
|
|
907
|
+
};
|
|
908
|
+
var validationError = (message) => new DgiiError(message, { code: "VALIDATION" });
|
|
909
|
+
var resolveReceptionRoute = (signedXml) => {
|
|
910
|
+
const root = extractRootElementName(signedXml);
|
|
911
|
+
if (root === "RFCE") {
|
|
912
|
+
return ok(SUMMARY);
|
|
913
|
+
}
|
|
914
|
+
if (root !== "ECF") {
|
|
915
|
+
return err(
|
|
916
|
+
validationError(
|
|
917
|
+
`Unsupported document root "${root ?? "unknown"}"; expected "ECF" or "RFCE"`
|
|
918
|
+
)
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
const tipo = extractElementText(signedXml, "TipoeCF");
|
|
922
|
+
if (tipo === void 0) {
|
|
923
|
+
return err(validationError("TipoeCF is missing from the document"));
|
|
924
|
+
}
|
|
925
|
+
if (!isEcfTypeCode(tipo)) {
|
|
926
|
+
return err(validationError(`Unknown TipoeCF "${tipo}"`));
|
|
927
|
+
}
|
|
928
|
+
if (tipo !== CONSUMPTION_TYPE) {
|
|
929
|
+
return ok(GENERAL);
|
|
930
|
+
}
|
|
931
|
+
const total = extractElementText(signedXml, "MontoTotal");
|
|
932
|
+
if (total === void 0) {
|
|
933
|
+
return err(
|
|
934
|
+
validationError(
|
|
935
|
+
"MontoTotal is required to route an E32 consumption invoice"
|
|
936
|
+
)
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
if (!DECIMAL_PATTERN.test(total)) {
|
|
940
|
+
return err(
|
|
941
|
+
validationError(`MontoTotal "${total}" is not a decimal amount`)
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
return compareDecimal(total, CONSUMPTION_SUMMARY_THRESHOLD) < 0 ? err(
|
|
945
|
+
validationError(
|
|
946
|
+
"An E32 below RD$250,000 must be sent as an RFCE summary; convert it with convertECF32ToRFCE first"
|
|
947
|
+
)
|
|
948
|
+
) : ok(GENERAL);
|
|
949
|
+
};
|
|
836
950
|
|
|
837
951
|
// src/utils.ts
|
|
838
952
|
var getCodeSixDigitfromSignature = (signedXml) => {
|
|
@@ -996,22 +1110,39 @@ import { createHash } from "crypto";
|
|
|
996
1110
|
var PENDING_SUBMISSION = { pending: true };
|
|
997
1111
|
var isPendingSubmission = (value) => typeof value === "object" && value !== null && value.pending === true;
|
|
998
1112
|
var sha256Hex = (value) => createHash("sha256").update(value, "utf8").digest("hex");
|
|
1113
|
+
var parseExpiry = (value) => {
|
|
1114
|
+
if (value === void 0) {
|
|
1115
|
+
return void 0;
|
|
1116
|
+
}
|
|
1117
|
+
const parsed = Date.parse(value);
|
|
1118
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
1119
|
+
};
|
|
999
1120
|
var ECF = class {
|
|
1000
1121
|
environment;
|
|
1001
1122
|
http;
|
|
1002
1123
|
idempotency;
|
|
1003
1124
|
credentials;
|
|
1004
1125
|
inFlight = /* @__PURE__ */ new Map();
|
|
1126
|
+
tokenRefreshSkewMs;
|
|
1127
|
+
now;
|
|
1128
|
+
tokenExpiresAt;
|
|
1129
|
+
refreshPromise;
|
|
1005
1130
|
constructor(credentials, environment = "TesteCF" /* Dev */, options = {}) {
|
|
1006
1131
|
this.credentials = credentials;
|
|
1007
1132
|
this.environment = environment;
|
|
1133
|
+
this.tokenRefreshSkewMs = options.tokenRefreshSkewMs ?? 5 * 60 * 1e3;
|
|
1134
|
+
this.now = options.now ?? Date.now;
|
|
1008
1135
|
this.idempotency = options.idempotencyStore ?? new InMemoryIdempotencyStore();
|
|
1009
1136
|
this.http = new HttpClient({
|
|
1010
1137
|
baseUrl: BaseUrl.ECF,
|
|
1011
1138
|
...options.token !== void 0 ? { token: options.token } : {},
|
|
1012
1139
|
...options.resilience !== void 0 ? { resilience: options.resilience } : {},
|
|
1013
1140
|
...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
|
|
1014
|
-
...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {}
|
|
1141
|
+
...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {},
|
|
1142
|
+
beforeRequest: async () => {
|
|
1143
|
+
await this.ensureToken();
|
|
1144
|
+
},
|
|
1145
|
+
onUnauthorized: () => this.reauthenticate()
|
|
1015
1146
|
});
|
|
1016
1147
|
}
|
|
1017
1148
|
buildIdempotencyKey(signedXml) {
|
|
@@ -1063,6 +1194,40 @@ var ECF = class {
|
|
|
1063
1194
|
this.inFlight.delete(key);
|
|
1064
1195
|
}
|
|
1065
1196
|
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Refresh the token when it is missing or about to expire.
|
|
1199
|
+
*
|
|
1200
|
+
* A pre-supplied `token` has no known expiry, so it is never refreshed
|
|
1201
|
+
* proactively — only a 401 triggers a re-authentication for it.
|
|
1202
|
+
*/
|
|
1203
|
+
async ensureToken() {
|
|
1204
|
+
if (this.tokenExpiresAt === void 0) {
|
|
1205
|
+
return ok(void 0);
|
|
1206
|
+
}
|
|
1207
|
+
if (this.now() < this.tokenExpiresAt - this.tokenRefreshSkewMs) {
|
|
1208
|
+
return ok(void 0);
|
|
1209
|
+
}
|
|
1210
|
+
const refreshed = await this.refreshToken();
|
|
1211
|
+
return refreshed.isOk() ? ok(void 0) : err(refreshed.error);
|
|
1212
|
+
}
|
|
1213
|
+
/** Authenticate, coalescing concurrent refreshes into a single round-trip. */
|
|
1214
|
+
refreshToken() {
|
|
1215
|
+
if (this.refreshPromise !== void 0) {
|
|
1216
|
+
return this.refreshPromise;
|
|
1217
|
+
}
|
|
1218
|
+
const promise = this.authenticate().finally(() => {
|
|
1219
|
+
this.refreshPromise = void 0;
|
|
1220
|
+
});
|
|
1221
|
+
this.refreshPromise = promise;
|
|
1222
|
+
return promise;
|
|
1223
|
+
}
|
|
1224
|
+
/** Drop the rejected token and authenticate once. Returns `true` on success. */
|
|
1225
|
+
async reauthenticate() {
|
|
1226
|
+
this.http.clearToken();
|
|
1227
|
+
this.tokenExpiresAt = void 0;
|
|
1228
|
+
const refreshed = await this.refreshToken();
|
|
1229
|
+
return refreshed.isOk();
|
|
1230
|
+
}
|
|
1066
1231
|
/** Authenticate against DGII (or a buyer host) and store the bearer token. */
|
|
1067
1232
|
async authenticate(buyerHost) {
|
|
1068
1233
|
const seed = await this.getSeed(buyerHost);
|
|
@@ -1089,6 +1254,7 @@ var ECF = class {
|
|
|
1089
1254
|
return err(token.error);
|
|
1090
1255
|
}
|
|
1091
1256
|
this.http.setToken(token.value.token);
|
|
1257
|
+
this.tokenExpiresAt = parseExpiry(token.value.expira);
|
|
1092
1258
|
return ok(token.value);
|
|
1093
1259
|
}
|
|
1094
1260
|
getSeed(buyerHost) {
|
|
@@ -1099,20 +1265,23 @@ var ECF = class {
|
|
|
1099
1265
|
);
|
|
1100
1266
|
return this.http.get(url2, void 0, {
|
|
1101
1267
|
absoluteUrl: true,
|
|
1102
|
-
resource: "fe/autenticacion/api/semilla" /* Seed
|
|
1268
|
+
resource: "fe/autenticacion/api/semilla" /* Seed */,
|
|
1269
|
+
skipAuthHooks: true
|
|
1103
1270
|
});
|
|
1104
1271
|
}
|
|
1105
1272
|
const url = buildUrl(this.environment, "Autenticacion/api/Autenticacion/Semilla" /* Seed */);
|
|
1106
1273
|
return this.http.get(url, void 0, {
|
|
1107
1274
|
absoluteUrl: true,
|
|
1108
|
-
resource: "Autenticacion/api/Autenticacion/Semilla" /* Seed
|
|
1275
|
+
resource: "Autenticacion/api/Autenticacion/Semilla" /* Seed */,
|
|
1276
|
+
skipAuthHooks: true
|
|
1109
1277
|
});
|
|
1110
1278
|
}
|
|
1111
1279
|
getAuthToken(signedSeed, buyerHost) {
|
|
1112
1280
|
const url = buyerHost ? buildSenderReceiverUrl(buyerHost, "fe/autenticacion/api/validacioncertificado" /* ValidateSeed */) : buildUrl(this.environment, "autenticacion/api/Autenticacion/ValidarSemilla" /* ValidateSeed */);
|
|
1113
1281
|
return this.http.postMultipart(url, "signed.xml", signedSeed, {
|
|
1114
1282
|
absoluteUrl: true,
|
|
1115
|
-
resource: "autenticacion/api/Autenticacion/ValidarSemilla" /* ValidateSeed
|
|
1283
|
+
resource: "autenticacion/api/Autenticacion/ValidarSemilla" /* ValidateSeed */,
|
|
1284
|
+
skipAuthHooks: true
|
|
1116
1285
|
});
|
|
1117
1286
|
}
|
|
1118
1287
|
/**
|
|
@@ -1144,6 +1313,32 @@ var ECF = class {
|
|
|
1144
1313
|
)
|
|
1145
1314
|
);
|
|
1146
1315
|
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Submit a signed document to the endpoint its own content belongs to.
|
|
1318
|
+
*
|
|
1319
|
+
* Reads the root element, `TipoeCF` and `MontoTotal` from the signed XML and
|
|
1320
|
+
* dispatches to {@link sendSummary} for an RFCE or to
|
|
1321
|
+
* {@link sendElectronicDocument} for an ECF. An E32 below RD$250,000 is
|
|
1322
|
+
* rejected because DGII only accepts the summary form for those.
|
|
1323
|
+
*/
|
|
1324
|
+
async sendDocument(signedXml, fileName, buyerHost) {
|
|
1325
|
+
const route = resolveReceptionRoute(signedXml);
|
|
1326
|
+
if (route.isErr()) {
|
|
1327
|
+
return err(route.error);
|
|
1328
|
+
}
|
|
1329
|
+
if (route.value.kind === "summary") {
|
|
1330
|
+
if (buyerHost !== void 0) {
|
|
1331
|
+
return err(
|
|
1332
|
+
new DgiiError(
|
|
1333
|
+
"An RFCE summary cannot be delivered directly to a buyer host",
|
|
1334
|
+
{ code: "VALIDATION" }
|
|
1335
|
+
)
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
return this.sendSummary(signedXml, fileName);
|
|
1339
|
+
}
|
|
1340
|
+
return this.sendElectronicDocument(signedXml, fileName, buyerHost);
|
|
1341
|
+
}
|
|
1147
1342
|
statusTrackId(trackId) {
|
|
1148
1343
|
const url = buildUrl(this.environment, "consultaresultado/api/Consultas/Estado" /* TrackResultStatus */);
|
|
1149
1344
|
return this.http.get(
|
|
@@ -1237,8 +1432,10 @@ var ECF = class {
|
|
|
1237
1432
|
};
|
|
1238
1433
|
export {
|
|
1239
1434
|
BaseUrl,
|
|
1435
|
+
CONSUMPTION_SUMMARY_THRESHOLD,
|
|
1240
1436
|
DgiiError,
|
|
1241
1437
|
ECF,
|
|
1438
|
+
ECF_TYPE_CODES,
|
|
1242
1439
|
Endpoint,
|
|
1243
1440
|
Environment,
|
|
1244
1441
|
Err,
|
|
@@ -1253,6 +1450,7 @@ export {
|
|
|
1253
1450
|
Transformer,
|
|
1254
1451
|
buildSenderReceiverUrl,
|
|
1255
1452
|
buildUrl,
|
|
1453
|
+
compareDecimal,
|
|
1256
1454
|
convertECF32ToRFCE,
|
|
1257
1455
|
createResiliencePolicies,
|
|
1258
1456
|
defaultResilienceOptions,
|
|
@@ -1268,10 +1466,12 @@ export {
|
|
|
1268
1466
|
getCurrentFormattedDate,
|
|
1269
1467
|
getCurrentFormattedDateTime,
|
|
1270
1468
|
getXmlFromBodyResponse,
|
|
1469
|
+
isEcfTypeCode,
|
|
1271
1470
|
isRetryableError,
|
|
1272
1471
|
isRetryableStatus,
|
|
1273
1472
|
ok,
|
|
1274
1473
|
okAsync,
|
|
1474
|
+
resolveReceptionRoute,
|
|
1275
1475
|
roundHalfUpToCents,
|
|
1276
1476
|
toDgiiError,
|
|
1277
1477
|
validateXMLCertificate
|