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
| Dependency | Why |
|---|---|
PaymentMethodsModule | Determines if a payment method generates AP bills (auto-creation from purchases) |
AuditLogModule | Records create/update/void events in the audit log |
DatabaseModule | Kysely 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 (purchaseorcontractorAssignment)entityId— UUID of the source documentsupplierId— the supplier owedsupplierInvoiceNumber— supplier's own invoice reference (optional, unique per supplier)totalAmount/totalBaseAmount— bill total in transaction and base currencybalanceDue/baseBalanceDue— outstanding balance (decremented by payments)exchangeRate— rate used for multi-currency conversionstatus— current lifecycle state (see below)documentNumber— auto-generated sequential identifierdetail— JSONB field tracking payment history:{ items: [], voidItems: [] }
Status Workflow
DRAFT → SUBMITTED → APPROVED → SCHEDULED → PAID
↘ VOID ↘ PAID ↘ PAID
↘ VOID ↘ VOID
↘ SCHEDULED
Valid transitions:
| From | To |
|---|---|
DRAFT | SUBMITTED |
SUBMITTED | APPROVED, VOID |
APPROVED | SCHEDULED, PAID, VOID |
SCHEDULED | PAID, VOID |
PAID | (terminal) |
VOID | (terminal) |
Locking Rules
- Bills in
DRAFTstatus can be freely edited and deleted. - Non-draft bills are locked — only
statusandupdatedBymay be changed. - Exception: transitioning to
SCHEDULEDalso allowsdueDateto be set.
Main Use Cases
Create Bill (Manual)
- Validate required fields via DTO
- Generate sequential
documentNumberinside a transaction - Persist bill and log audit event
- Emit
accountsPayableBill.createevent
Create Bill (Auto — from Purchase or Contractor Assignment)
- Listen for
purchase.create,purchase.update,contractorAssignment.create,contractorAssignment.updateevents - Check if the source document status is
SUBMITTEDorREVIEWED - For each payment method item with
generatesAccountsPayable = true, create a new bill - Bills are created with status
SUBMITTEDand full balance due
Update Bill (Status Transition)
- Fetch current bill
- Validate status transition is allowed
- Check locking rules (non-draft bills restrict editable fields)
- Persist inside a transaction with before/after audit snapshot
- Emit
accountsPayableBill.updateevent
Payment Reconciliation
- Listen for
accountsPayablePayment.createandaccountsPayablePayment.updateevents - On
POSTEDpayment: decrementbalanceDueand record payment item indetail.items - If
balanceDuereaches zero, auto-transition toPAID - On
VOIDpayment: restorebalanceDueand move item todetail.voidItems
Delete Bill
- Only
DRAFTbills can be deleted
Validation Rules
| Rule | Error Code | Description |
|---|---|---|
| Bill must exist | 404 | Bill UUID must resolve |
| Draft-only edits | BILL_LOCKED | Non-draft bills cannot have fields changed (except status/updatedBy) |
| Valid transition | INVALID_STATUS_TRANSITION | Must follow the status workflow |
| Scheduled needs due date | MISSING_DUE_DATE | Bills transitioning to SCHEDULED must have a dueDate |
| Draft-only delete | BILL_LOCKED | Only draft bills can be deleted |
API Endpoints
| Method | Path | Description |
|---|---|---|
POST | /accounts-payable-bills | Create a new AP bill |
GET | /accounts-payable-bills | List bills (paginated, filterable) |
GET | /accounts-payable-bills/:id | Get bill by ID |
PATCH | /accounts-payable-bills/:id | Update / transition bill status |
DELETE | /accounts-payable-bills/:id | Delete 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
| Event | Emitted When | Typical Listener |
|---|---|---|
accountsPayableBill.create | Bill is created | (currently unused — available for future integrations) |
accountsPayableBill.update | Bill is updated or voided | (currently unused — available for future integrations) |
Consumed
| Event | Source Module | Action |
|---|---|---|
accountsPayablePayment.create | AP Payments | Decrements balanceDue; auto-sets PAID if zero |
accountsPayablePayment.update | AP Payments | Restores balanceDue on void |
purchase.create | Purchases | Auto-creates AP bill if payment method generates AP |
purchase.update | Purchases | Auto-creates AP bill on DRAFT→SUBMITTED/REVIEWED transition |
contractorAssignment.create | Contractor Assignments | Auto-creates AP bill if payment method generates AP |
contractorAssignment.update | Contractor Assignments | Auto-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/
| File | Endpoint |
|---|---|
accounts payable bills.yml | GET list |
accounts payable bill.yml | POST create |
accounts payable bill by Id.yml | GET by ID |
update accounts payable bill.yml | PATCH update/transition |
void accounts payable bill.yml | PATCH void |
delete accounts payable bill.yml | DELETE |