Retail Sales FEL Workflow
This diagram shows the complete flow from retail sale creation through FEL invoice certification. FEL is triggered asynchronously via the internal event bus after the sale is saved, and the resulting FEL columns are persisted by a dedicated event handler.
Prerequisites
| Configuration step | Where |
|---|---|
Business has FEL certifier config (felCertifierConfig on business) | Business settings |
Payment method has generateElectronicTaxDocument = true on documentPaymentMethod | Payment method config |
Products have unitOfMeasure and type (goods vs. service) set | Product catalog |
Products have correct tax assignments (product_tax rows) | Product catalog |
Unit conventions
| Field | Units |
|---|---|
sale_detail.items[].unitPrice | Major (decimal) |
sale_detail.items[].total | Major |
sale_detail.items[].discountDetail.unitPriceFinal | Minor |
sale_detail.items[].bundleDetail.unitPriceFinal | Major |
sale.total_amount | Major |
Flow
flowchart TD
A([Cashier builds cart\nin PWA SaleForm]) --> B{Payment method has\ngenerateElectronicTaxDocument=true?}
B -- no --> C[POST /sales\ngenerateElectronicTaxDocument=false\nno buyer tax fields required]
B -- yes --> D[Cashier enters buyer NIT\nor leaves blank for CF\nClient-side NIT validation]
D --> E[POST /sales\ngenerateElectronicTaxDocument=true\ntaxId, taxName, taxAddress, taxpayerType\nsaleDetail.items with full product data]
C --> F[SalesService.create\nsale row inserted\nwith saleDetail JSONB]
E --> F
F --> G[OnCreateSaleEvent emitted\nasynchronously via EventEmitter2]
G --> H[FelService.processSaleEvent handler]
H --> I{sale.generateElectronicTaxDocument\n=== true?}
I -- no --> Z([Skip FEL])
I -- yes --> J
J{Business has\nfelCertifierConfig?}
J -- no --> Z
J -- yes --> K
K[Build receiver\nsale.taxId ?? 'CF'\nsale.taxName ?? 'CONSUMIDOR FINAL'\nsale.taxAddress ?? 'CIUDAD'\nsale.taxpayerType ?? 'NIT']
K --> L[toFelInvoiceItemInputFromSaleDetail\nfor each saleDetail.items entry\ngoodOrService from product type\nunitOfMeasure from product uom\ntax rows from product_tax join]
L --> M[buildFelInvoiceItems\nshared pure function\ntax rows, discount lines\nbundle handling, rounding]
M --> N[FelService.certifyDocument\nSerie, Numero, Autorizacion\nFecha_DTE, felCertificationDate\nfelAcknowledgmentOfReceipt]
N -- success --> O[OnDocumentCertifiedEvent emitted]
N -- certifier error --> P[try/catch swallowed\nSentry log\nsale stays in DB\nFEL columns stay null]
O --> Q[FelService.handleOnDocumentCertifiedEvent]
Q --> R[UPDATE sale SET\nfel_authorization\nfel_serial_number\nfel_number\nfel_date_dte\nfel_certification_date\nfel_acknowledgment_of_receipt\nis_exported_to_fel=true\nfel_uuid]
R --> S[PWA refetches / receives WS update]
S --> T{Print FEL invoice?}
T -- yes --> U[POST /document-print-jobs\ndocumentType=FEL_INVOICE\nsourceKind=sale\nsourceId=saleId]
U --> V[buildFelInvoicePayloadFromSale\nloads sale + business + location\nfrom sale.saleDetail JSONB\nassembles certifier fields]
V --> W([FEL invoice printed])
T -- no --> W2([Done])
Event bus vs. inline persistence
Unlike the restaurant bill flow (which persists FEL data inline inside certifyPaidBillIfRequired), the retail flow uses two event bus hops:
OnCreateSaleEvent/OnUpdateSaleEvent→ triggersprocessSaleEventwhich runscertifyDocumentOnDocumentCertifiedEvent→ triggershandleOnDocumentCertifiedEventwhich writes the FEL columns
This means there is a brief window where isExportedToFel = false even after successful certification. The PWA should refetch after the WS notification rather than polling.
Shared item builder
Both the retail and restaurant flows converge on the same pure function:
toFelInvoiceItemInputFromSaleDetail ──┐
├──► buildFelInvoiceItems ──► InvoiceItem[]
toFelInvoiceItemInputFromOrderItem ──┘
buildFelInvoiceItems in apps/backend/src/fel/application/fel-invoice-item.builder.ts contains all tax row construction, line-discount aggregation, bundle handling, and amount rounding. Neither adapter duplicates this logic.