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
digifactandinfilecertify XML payloads rpafelapiis 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
voidCertificateendpoint, 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
| Layer | Codepaths | Responsibility |
|---|---|---|
| Domain | domain/fel.interface.ts, domain/electronic-certification-provider.interface.ts, repository ports | FEL document shapes, provider contract, validation result types, persistence contracts |
| Application | application/fel.service.ts, fel-credit-note.service.ts, fel-debit-note.service.ts, fel-cancellation.service.ts, provider.service.ts, event handlers | Build documents, choose provider path, enforce use-case rules, emit domain events |
| Infrastructure | infrastructure/provider-digifact.service.ts, provider-infile.service.ts, provider-rpafelapi.service.ts, Kysely repositories | Provider HTTP calls, token handling, XML/JSON transport details, persistence adapters |
| Interfaces | interfaces/fel.controller.ts, DTOs, pagination queries | HTTP 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 key | Adapter |
|---|---|
digifact | ProviderDigifactService |
infile | ProviderInfileService |
rpafelapi | ProviderRpaFelApiService |
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 refreshvoidCertificate()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":
- Load the business by
document.businessData.id. - Extract the certifier config from
business.felCertifierConfig. - Resolve provider API URL from
GuatemalanCertifiersUrls. - Decrypt the configured certifier token.
- Reject expired JWT-style tokens because direct certifier refresh is not implemented in this path.
- Convert the document to XML with
XmlConversionService. - Resolve
document.felProviderData.providerNamethroughProviderService. - Call
provider.certifyDocument({ taxId, xmlContent, apiUrl, token }).
RPAfelApi path
When USE_RPA_FEL_API === "true":
- Build the DTE JSON payload from the document using
XmlToDteJsonService. - Inject certifier credentials into the DTE payload.
- Resolve
rpafelapithroughProviderService. - Pass
JSON.stringify(dteJson)in thexmlContentfield 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 therpafelapiprovider. - 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:
- validates that the original sale invoice is certified
- checks the provided original invoice UUID against
sale.felAuthorization - prevents the credit amount from exceeding the original sale total
- builds and certifies the credit note
- 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:
- load the sale and original invoice UUID from
sale.felAuthorization - validate cancellation eligibility
- persist a pending cancellation
- 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:
| Workflow | Routing decision | Adapter call |
|---|---|---|
Retail cancellation with USE_RPA_FEL_API === "true" | RPA path | ProviderRpaFelApiService.voidCertificate() |
| Retail cancellation with any other flag value | Direct path | FelService.certifyDocument() with document type ANULACION |
| Restaurant order-bill cancellation | Always RPA; the flag is not read | ProviderRpaFelApiService.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:
- Resolve the certifier password from
encryptedFelPassword, with a legacy fallback to a non-JWT value inencryptedFelToken. - When the username and password are available, call
ProviderRpaFelApiService.getToken()and prefer the returned JWT. - If token retrieval is unavailable or fails, use a stored JWT only when it is not expired.
- Invoice certification and shared-info lookup may use the password as a final Authorization fallback because their request bodies also carry certifier credentials.
voidCertificatedoes 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.
| Area | Routes |
|---|---|
| Core utilities | POST /fel/convert-to-xml, POST /fel/certify-document, POST /fel/get-shared-info |
| Sale retry | POST /fel/sales/:id/retry |
| Credit notes | GET/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 notes | Same pattern under /fel/debit-notes |
| Cancellations | Same 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.felCertifierConfigmust contain the provider config expected by the selected path.- Direct certifier JWTs are not refreshed by
FelService; expired JWTs fail fast. getSharedInfousesrpafelapiwhenUSE_RPA_FEL_API === "true"; otherwise it uses the configured business certifier.- RPA cancellation uses the FEL UUID for
NumeroDocumentoAAnular, notSerie-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, butrpafelapicurrently 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.
Related docs
- FEL Provider Matrix
- FEL certifier registry — which certifiers appear in billing UI vs full SAT registry
- FEL Module Architecture
- Retail sales FEL workflow
- Restaurant FEL workflow