Saltar al contenido principal

Accounts Payable Bills

Overview

The accounts-payable-bills module manages supplier bills (AP bills) — the financial obligations a business owes to its suppliers. It handles the full bill lifecycle: creation, status workflow, payment reconciliation, and audit logging.

Bills can be created manually or auto-generated from purchases and contractor assignments via the event system.


Architecture

This module follows Hexagonal Architecture with four layers:

domain/         — Repository interface (IAccountsPayableBillsRepository), domain types
application/ — Business logic (AccountsPayableBillsService), event handlers
infrastructure/ — Kysely DB adapter (AccountsPayableBillsRepository)
interfaces/ — HTTP controller, DTOs, query objects

Module Dependencies

DependencyWhy
PaymentMethodsModuleDetermines if a payment method generates AP bills (auto-creation from purchases)
AuditLogModuleRecords create/update/void events in the audit log
DatabaseModuleKysely database connection for queries and transactions

Domain Concepts

Bill

An AP bill represents money owed to a supplier for goods or services received.

Key fields:

  • entityType — source document type (purchase or contractorAssignment)
  • entityId — UUID of the source document
  • supplierId — the supplier owed
  • supplierInvoiceNumber — supplier's own invoice reference (optional, unique per supplier)
  • totalAmount / totalBaseAmount — bill total in transaction and base currency
  • balanceDue / baseBalanceDue — outstanding balance (decremented by payments)
  • exchangeRate — rate used for multi-currency conversion
  • status — current lifecycle state (see below)
  • documentNumber — auto-generated sequential identifier
  • detail — JSONB field tracking payment history: { items: [], voidItems: [] }

Status Workflow

DRAFT → SUBMITTED → APPROVED → SCHEDULED → PAID
↘ VOID ↘ PAID ↘ PAID
↘ VOID ↘ VOID
↘ SCHEDULED

Valid transitions:

FromTo
DRAFTSUBMITTED
SUBMITTEDAPPROVED, VOID
APPROVEDSCHEDULED, PAID, VOID
SCHEDULEDPAID, VOID
PAID(terminal)
VOID(terminal)

Locking Rules

  • Bills in DRAFT status can be freely edited and deleted.
  • Non-draft bills are locked — only status and updatedBy may be changed.
  • Exception: transitioning to SCHEDULED also allows dueDate to be set.

Main Use Cases

Create Bill (Manual)

  1. Validate required fields via DTO
  2. Generate sequential documentNumber inside a transaction
  3. Persist bill and log audit event
  4. Emit accountsPayableBill.create event

Create Bill (Auto — from Purchase or Contractor Assignment)

  1. Listen for purchase.create, purchase.update, contractorAssignment.create, contractorAssignment.update events
  2. Check if the source document status is SUBMITTED or REVIEWED
  3. For each payment method item with generatesAccountsPayable = true, create a new bill
  4. Bills are created with status SUBMITTED and full balance due

Update Bill (Status Transition)

  1. Fetch current bill
  2. Validate status transition is allowed
  3. Check locking rules (non-draft bills restrict editable fields)
  4. Persist inside a transaction with before/after audit snapshot
  5. Emit accountsPayableBill.update event

Payment Reconciliation

  1. Listen for accountsPayablePayment.create and accountsPayablePayment.update events
  2. On POSTED payment: decrement balanceDue and record payment item in detail.items
  3. If balanceDue reaches zero, auto-transition to PAID
  4. On VOID payment: restore balanceDue and move item to detail.voidItems

Delete Bill

  • Only DRAFT bills can be deleted

Validation Rules

RuleError CodeDescription
Bill must exist404Bill UUID must resolve
Draft-only editsBILL_LOCKEDNon-draft bills cannot have fields changed (except status/updatedBy)
Valid transitionINVALID_STATUS_TRANSITIONMust follow the status workflow
Scheduled needs due dateMISSING_DUE_DATEBills transitioning to SCHEDULED must have a dueDate
Draft-only deleteBILL_LOCKEDOnly draft bills can be deleted

API Endpoints

MethodPathDescription
POST/accounts-payable-billsCreate a new AP bill
GET/accounts-payable-billsList bills (paginated, filterable)
GET/accounts-payable-bills/:idGet bill by ID
PATCH/accounts-payable-bills/:idUpdate / transition bill status
DELETE/accounts-payable-bills/:idDelete a draft bill

Full Swagger documentation is available at /docs when the backend is running.


Example: Create Bill

POST /accounts-payable-bills
{
"entityType": "purchase",
"entityId": "uuid",
"purchaseDate": "2026-03-25",
"status": "draft",
"supplierId": "uuid",
"currencyId": "uuid",
"currencyCode": "GTQ",
"minorUnit": 2,
"exchangeRate": 1.00,
"currency": { "id": "uuid", "name": "Quetzal", "symbol": "Q" },
"totalAmount": 95.00,
"totalBaseAmount": 95.00,
"balanceDue": 95.00,
"baseBalanceDue": 95.00,
"businessId": "uuid",
"createdBy": "uuid",
"terms": "Net 30"
}

Example: Transition to Submitted

PATCH /accounts-payable-bills/:id
{
"status": "submitted",
"updatedBy": "uuid"
}

Example: Void a Bill

PATCH /accounts-payable-bills/:id
{
"status": "void",
"updatedBy": "uuid"
}

Events

Emitted

EventEmitted WhenTypical Listener
accountsPayableBill.createBill is created(currently unused — available for future integrations)
accountsPayableBill.updateBill is updated or voided(currently unused — available for future integrations)

Consumed

EventSource ModuleAction
accountsPayablePayment.createAP PaymentsDecrements balanceDue; auto-sets PAID if zero
accountsPayablePayment.updateAP PaymentsRestores balanceDue on void
purchase.createPurchasesAuto-creates AP bill if payment method generates AP
purchase.updatePurchasesAuto-creates AP bill on DRAFT→SUBMITTED/REVIEWED transition
contractorAssignment.createContractor AssignmentsAuto-creates AP bill if payment method generates AP
contractorAssignment.updateContractor AssignmentsAuto-creates AP bill on DRAFT→SUBMITTED/REVIEWED transition

Database

  • Table: accounts_payable_bill
  • Views: accounts_payable_bill_balance_v, accounts_payable_aging_v, accounts_payable_payments_v
  • Unique constraint: (supplier_id, supplier_invoice_number) where invoice_number IS NOT NULL

Key Design Decision: JSONB for detail

detail is stored as jsonb rather than a normalized table. It tracks { items: [...], voidItems: [...] } — the payment application history for the bill. This matches the AP payments module's approach.


Known Issues

Void Payment Detail Bug

When voiding a payment, the code removes ALL payment items from detail.items instead of only the voided payment's items. This is because each stored item carries accountsPayableBillId === this bill's id, so the filter matches all items.

Impact: In multi-payment scenarios, voiding payment-2 erases payment-1's record from detail.items.

Fix required: Add paymentId to AccountsPayablePaymentItem so the filter can target only the specific payment's items. The AP-Payments service must also populate paymentId when posting items.


Bruno API Collection

All endpoints are covered in: api-client/flowpos/collections/accounts-payable-bills/

FileEndpoint
accounts payable bills.ymlGET list
accounts payable bill.ymlPOST create
accounts payable bill by Id.ymlGET by ID
update accounts payable bill.ymlPATCH update/transition
void accounts payable bill.ymlPATCH void
delete accounts payable bill.ymlDELETE