Saltar al contenido principal

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 stepWhere
Business has FEL certifier config (felCertifierConfig on business)Business settings
Payment method has generateElectronicTaxDocument = true on documentPaymentMethodPayment method config
Products have unitOfMeasure and type (goods vs. service) setProduct catalog
Products have correct tax assignments (product_tax rows)Product catalog

Unit conventions

FieldUnits
sale_detail.items[].unitPriceMajor (decimal)
sale_detail.items[].totalMajor
sale_detail.items[].discountDetail.unitPriceFinalMinor
sale_detail.items[].bundleDetail.unitPriceFinalMajor
sale.total_amountMajor

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\nfelStatus=failed\nfelErrorMessage set]

O --> Q[SalesService.handleOnDocumentCertifiedEvent]
Q --> R[UPDATE sale SET\nfel_authorization\nfel_serial_number\nfel_number\nfel_date_dte\nfel_certification_date\nfel_acknowledgment_of_receipt\nfel_status=certified\nfel_error_message=null]

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:

  1. OnCreateSaleEvent / OnUpdateSaleEvent → triggers processSaleEvent which runs certifyDocument
  2. OnDocumentCertifiedEvent → triggers SalesService.handleOnDocumentCertifiedEvent, which writes the certified FEL fields and marks felStatus=certified

This means there is a brief window after the sale is saved where felStatus may still be pending or processing. The certified sale fields are written by the sales module, not by the provider adapter. In the current retail sale handler, felAuthorization, serial/number/date fields, acknowledgment, felStatus, and felErrorMessage are updated; do not rely on retail sale.isExportedToFel or sale.felUuid being set by this event.

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.