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\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:

  1. OnCreateSaleEvent / OnUpdateSaleEvent → triggers processSaleEvent which runs certifyDocument
  2. OnDocumentCertifiedEvent → triggers handleOnDocumentCertifiedEvent which 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.