Skip to content

Latest commit

 

History

History
162 lines (123 loc) · 5.72 KB

File metadata and controls

162 lines (123 loc) · 5.72 KB

Invoice Workflow

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:

  1. Load and validate fiscal settings (loadValidatedSettings)
  2. Check CSC for NFC-e model 65
  3. Load order with items from DB
  4. Get next number/series from settings
  5. Build InvoiceBuildData from order items
  6. buildInvoiceXml() → unsigned XML + access key
  7. loadCertificate() + signXml() → signed XML
  8. Send to SEFAZ via sefazRequest()
  9. Parse response → determine status (authorized/rejected/denied)
  10. If authorized: attachProtocol() → nfeProc XML
  11. saveInvoice() → persist to DB
  12. incrementNextNumber() → 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 4× 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.