Invoice Workflow
The invoice service orchestrates the complete lifecycle of an electronic invoice from XML generation to SEFAZ submission, handling authorization, cancellation, voiding, contingency, and persistence.
Overview
The invoice service orchestrates the complete lifecycle of an electronic invoice: from building the XML to sending it to SEFAZ, handling responses, and persisting results. It sits in the application/service layer, coordinating domain logic, infrastructure, and persistence.
These three files are the DB-coupled layer that lives in the app, not in the @finopenpos/fiscal package (which has zero database dependencies).
Files: apps/web/src/lib/invoice-service.ts, apps/web/src/lib/fiscal-settings-repository.ts, apps/web/src/lib/invoice-repository.ts
Invoice Lifecycle
┌──────────┐ ┌──────────┐ ┌───────────┐ ┌────────────┐
│ pending │────→│authorized│────→│ cancelled │ │ voided │
└──────────┘ └──────────┘ └───────────┘ └────────────┘
│ ↑
│ ┌──────────┐ │
├──────────→│ rejected │ (number range)
│ └──────────┘
│ ┌──────────┐
├──────────→│ denied │
│ └──────────┘
│ ┌────────────┐
└──────────→│contingency │──→ (sync later → authorized/rejected)
└────────────┘Main Operations
issueInvoice
async function issueInvoice(
orderId: number,
model: InvoiceModel, // 55 or 65
userUid: string,
recipientTaxId?: string,
recipientName?: string
): Promise<{ invoiceId: number; status: InvoiceStatus; accessKey: string }>Flow:
- Load and validate fiscal settings (
loadValidatedSettings) - Check CSC for NFC-e model 65
- Load order with items from DB
- Get next number/series from settings
- Build
InvoiceBuildDatafrom order items buildInvoiceXml()→ unsigned XML + access keyloadCertificate()+signXml()→ signed XML- Send to SEFAZ via
sefazRequest() - Parse response → determine status (authorized/rejected/denied)
- If authorized:
attachProtocol()→ nfeProc XML saveInvoice()→ persist to DBincrementNextNumber()→ update counter
Offline fallback (NFC-e only): If SEFAZ is unavailable, save as "contingency" status and sync later.
checkSefazStatus
async function checkSefazStatus(userUid: string): Promise<{
online: boolean;
statusCode: number;
statusMessage: string;
}>Calls NfeStatusServico to check if SEFAZ is online. Status 107 = running.
cancelInvoice
async function cancelInvoice(
invoiceId: number,
userUid: string,
reason: string // min 15 chars
): Promise<{ success: boolean; statusCode: number }>Only authorized invoices can be cancelled. Builds cancellation event XML, signs, and sends to SEFAZ RecepcaoEvento. Saves the event in invoiceEvents.
voidNumberRange
async function voidNumberRange(
model: InvoiceModel,
series: number,
startNumber: number,
endNumber: number,
reason: string,
userUid: string
): Promise<{ success: boolean; statusCode: number }>Voids unused invoice number ranges (inutilizacao). Required when numbers are skipped (e.g., system crash before issuing).
syncPendingInvoices
async function syncPendingInvoices(userUid: string): Promise<{
total: number; authorized: number; failed: number;
}>Retries pending/contingency invoices that weren't confirmed by SEFAZ.
Settings Validation (DRY)
The loadValidatedSettings helper deduplicates the 4x repeated validation pattern:
async function loadValidatedSettings(userUid: string): Promise<FiscalSettings> {
const settings = await loadFiscalSettings(userUid);
if (!settings || !settings.certificatePfx || !settings.certificatePassword) {
throw new Error("Fiscal settings or certificate not configured");
}
return settings;
}Used by: checkSefazStatus, cancelInvoice, voidNumberRange, syncPendingInvoices.
Fiscal Settings Repository (apps/web/src/lib/fiscal-settings-repository.ts)
loadFiscalSettings
async function loadFiscalSettings(userUid: string): Promise<FiscalSettings | null>Maps DB snake_case fields to domain camelCase. Provides defaults:
- Series: 1
- NCM: "00000000"
- CFOP: "5102" (internal sale)
- ICMS CST: "00", PIS/COFINS CST: "99"
incrementNextNumber
async function incrementNextNumber(userUid: string, model: InvoiceModel): Promise<void>Atomically increments next_nfe_number or next_nfce_number.
Invoice Repository (apps/web/src/lib/invoice-repository.ts)
Key operations
| Function | Description |
|---|---|
loadOrderWithItems(orderId, userUid) | Load order + items + products (for building XML) |
findInvoice(invoiceId, userUid) | Get single invoice by ID |
saveInvoice(data) | Insert invoice + items, returns invoiceId |
findPendingInvoices(userUid) | Get invoices with status "pending" or "contingency" |
updateInvoiceStatus(invoiceId, data) | Update status, response XML, protocol, etc. |
saveVoidedInvoice(data) | Create a "voided" record for number range voiding |
saveInvoiceEvent(data) | Log cancellation/voiding event with XML |
Multi-tenancy
Every query includes eq(table.user_uid, userUid) in the WHERE clause.
Fiscal Module Architecture
The fiscal module implements complete Brazilian electronic invoicing (NF-e model 55 and NFC-e model 65) following the SEFAZ MOC 4.00 specification, ported from PHP sped-nfe to TypeScript with a DDD layered architecture.
Tax Calculation Engine
The tax engine calculates Brazilian taxes for each invoice item and produces structured TaxElement objects that the XML builder serializes, following a DDD decoupling pattern where domain logic never touches XML directly.