Skip to main content

FEL Provider Port

Source-backed guide for the FEL/SAT provider pattern in apps/backend/src/fel/.

Use this page when adding or troubleshooting electronic invoicing providers. For product workflows, see the retail and restaurant FEL workflow pages under dev/fel/.


Intent

The FEL module keeps tax-document use cases independent from provider HTTP details. Application services build and validate invoice, credit note, debit note, and cancellation documents; infrastructure adapters handle provider-specific request formats.

This separation matters because Guatemala FEL providers do not expose identical APIs:

  • direct certifiers such as digifact and infile certify XML payloads
  • rpafelapi is an intermediary adapter and receives a JSON DTE payload through the same provider port parameter
  • cancellation can use the standard document certification path or the RPA voidCertificate endpoint, depending on runtime routing

Do not describe provider URL formats, token refresh behavior, or HTTP retries as domain behavior. Those are adapter concerns.


Hexagonal map

LayerCodepathsResponsibility
Domaindomain/fel.interface.ts, domain/electronic-certification-provider.interface.ts, repository portsFEL document shapes, provider contract, validation result types, persistence contracts
Applicationapplication/fel.service.ts, fel-credit-note.service.ts, fel-debit-note.service.ts, fel-cancellation.service.ts, provider.service.ts, event handlersBuild documents, choose provider path, enforce use-case rules, emit domain events
Infrastructureinfrastructure/provider-digifact.service.ts, provider-infile.service.ts, provider-rpafelapi.service.ts, Kysely repositoriesProvider HTTP calls, token handling, XML/JSON transport details, persistence adapters
Interfacesinterfaces/fel.controller.ts, DTOs, pagination queriesHTTP routes, Swagger metadata, request parsing

Provider contract

All provider adapters implement ElectronicCertificationProvider:

export interface ElectronicCertificationProvider {
certifyDocument(
params: IProviderCertifyDocumentParameters,
): Promise<CertifierResponse | undefined>;

getSharedInfo(params: IProviderGetSharedInfoParameters): Promise<unknown>;
}

ProviderService maps provider names to adapters:

Provider keyAdapter
digifactProviderDigifactService
infileProviderInfileService
rpafelapiProviderRpaFelApiService

Unknown provider names fail fast with Unknown provider: (<name>). For merchant-selectable status and direct-vs-RPA constraints, see FEL Provider Matrix.

ProviderRpaFelApiService also exposes adapter-specific methods that are not part of ElectronicCertificationProvider:

  • getToken() for RPA token refresh
  • voidCertificate() for RPA cancellation requests

Keep new cross-provider behavior on the port only when every supported provider can satisfy the contract. Provider-only capabilities should stay behind adapter-specific application routing.


Certification routing

FelService.certifyDocument chooses the certification path before it calls the provider port.

Direct certifier path

When USE_RPA_FEL_API !== "true":

  1. Load the business by document.businessData.id.
  2. Extract the certifier config from business.felCertifierConfig.
  3. Resolve provider API URL from GuatemalanCertifiersUrls.
  4. Decrypt the configured certifier token.
  5. Reject expired JWT-style tokens because direct certifier refresh is not implemented in this path.
  6. Convert the document to XML with XmlConversionService.
  7. Resolve document.felProviderData.providerName through ProviderService.
  8. Call provider.certifyDocument({ taxId, xmlContent, apiUrl, token }).

RPAfelApi path

When USE_RPA_FEL_API === "true":

  1. Build the DTE JSON payload from the document using XmlToDteJsonService.
  2. Inject certifier credentials into the DTE payload.
  3. Resolve rpafelapi through ProviderService.
  4. Pass JSON.stringify(dteJson) in the xmlContent field of the provider port.

rpafelapi is an intermediary, not the fiscal certifier itself. The business certifier still determines fiscal provider-specific fields that are placed in the DTE payload.

Shared-info routing

FelService.getSharedInfo follows the same runtime routing flag:

  • When USE_RPA_FEL_API === "true", shared-info requests resolve the rpafelapi provider.
  • Otherwise, shared-info requests resolve the business certifier provider from business.felCertifierConfig.

Do not assume NIT lookups always hit the direct certifier. This matters for MCP get_taxpayer_info, which delegates to FelService.getSharedInfo.


Document workflows

Sale invoice certification

Sales events call FelService.processSaleEvent when a sale is submitted or reviewed. On success, the service emits fel.documentCertified; the sales module persists FEL fields on the sale. On failure, the sale flow continues and FEL status is marked failed.

Credit notes (NCRE)

FelCreditNoteService:

  1. validates that the original sale invoice is certified
  2. checks the provided original invoice UUID against sale.felAuthorization
  3. prevents the credit amount from exceeding the original sale total
  4. builds and certifies the credit note
  5. emits fel.creditNote.certified

Inventory listens to the certified credit-note event for reversal behavior.

Debit notes (NDEB)

FelDebitNoteService follows the same certified-invoice and UUID checks as credit notes. Tax parity validation is still marked as a TODO in FelValidationService; do not claim that new-tax prevention is enforced until that TODO is implemented.

Cancellations (ANULACION)

Retail cancellation requests use FelCancellationService:

  1. load the sale and original invoice UUID from sale.felAuthorization
  2. validate cancellation eligibility
  3. persist a pending cancellation
  4. certify the cancellation

Validation currently enforces:

  • original invoice must be certified
  • cancellation date must be in the same calendar month/year as the sale date
  • cancellation is blocked when credit notes already exist for the sale

Cancellation routing is asymmetric:

WorkflowRouting decisionAdapter call
Retail cancellation with USE_RPA_FEL_API === "true"RPA pathProviderRpaFelApiService.voidCertificate()
Retail cancellation with any other flag valueDirect pathFelService.certifyDocument() with document type ANULACION
Restaurant order-bill cancellationAlways RPA; the flag is not readProviderRpaFelApiService.voidCertificate()

Both RPA void paths send the certified invoice UUID (felAuthorization / originalInvoiceUuid) as NumeroDocumentoAAnular. They do not send felSerialNumber-felNumber.

Restaurant bill cancellation uses the separate cancelOrderBill application path. It therefore requires the RPA adapter to be reachable even when invoice certification uses a direct certifier. Retail cancellation emits fel.cancellation.certified; restaurant bill cancellation emits fel.order-bill-cancellation.certified.

RPA authorization by operation

RPA token resolution is infrastructure behavior implemented by resolveRpaFelApiAuthToken:

  1. Resolve the certifier password from encryptedFelPassword, with a legacy fallback to a non-JWT value in encryptedFelToken.
  2. When the username and password are available, call ProviderRpaFelApiService.getToken() and prefer the returned JWT.
  3. If token retrieval is unavailable or fails, use a stored JWT only when it is not expired.
  4. Invoice certification and shared-info lookup may use the password as a final Authorization fallback because their request bodies also carry certifier credentials.
  5. voidCertificate does not carry the password in its request body. Both retail and restaurant void paths therefore require a JWT and never use the password as the Authorization token.

This difference explains a common failure mode: invoice certification can succeed while cancellation fails. For a void failure, verify that the business billing configuration has the certifier username and password needed by getToken(), or a valid stored JWT. extractFelCertifierConfig also requires felCertifier and encryptedFelToken before token resolution begins.


Public HTTP surface

Base controller: FelController at /fel.

AreaRoutes
Core utilitiesPOST /fel/convert-to-xml, POST /fel/certify-document, POST /fel/get-shared-info
Sale retryPOST /fel/sales/:id/retry
Credit notesGET/POST /fel/credit-notes, GET /fel/credit-notes/:id, GET /fel/credit-notes/sale/:saleId, POST /fel/credit-notes/:id/certify, PDF/print endpoints
Debit notesSame pattern under /fel/debit-notes
CancellationsSame pattern under /fel/cancellations

Only POST /fel/sales/:id/retry has an explicit @Permission(Sale, Update) decorator in this controller. Other routes still run through the backend's global authentication guard unless separately marked public.


Common pitfalls

  • business.felCertifierConfig must contain the provider config expected by the selected path.
  • Direct certifier JWTs are not refreshed by FelService; expired JWTs fail fast.
  • getSharedInfo uses rpafelapi when USE_RPA_FEL_API === "true"; otherwise it uses the configured business certifier.
  • RPA cancellation uses the FEL UUID for NumeroDocumentoAAnular, not Serie-Numero.
  • Restaurant order-bill cancellation always uses RPA voidCertificate, regardless of the invoice-routing flag.
  • A password fallback can keep RPA invoice certification working, but RPA void operations require a JWT from getToken() or a valid stored JWT.
  • The provider port accepts xmlContent, but rpafelapi currently receives JSON serialized into that field.
  • Credit-note validation caps a single note against the original sale total; it does not currently enforce an aggregate cap across multiple credit notes.
  • Debit-note tax parity is still a TODO in FelValidationService; do not document debit notes as enforcing "no new taxes."
  • FEL provider network failures are surfaced as gateway-style errors; inspect provider response bodies only when an HTTP response exists.