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.

Weekly due-payments digest​

Every Monday at 07:00 UTC, a cron job in accounts-payable-bills emits one accountsPayable.weeklyDigest event per business that has unpaid bills due this UTC week or overdue.

Communications owns sending. AP does not import Communications.

Typeaccounts_payable_weekly
Template codeaccounts_payable_weekly_email (email only)
WeekMonday 00:00 UTC through Sunday, comparing due_date::date
Payable billsstatus submitted / approved / scheduled, balanceDue > 0, non-null dueDate
TotalsSUM(base_balance_due) from SQL, not the 50-row email cap
Recipientsrecipient rules for this type + email; if none, owner / admin / administrator / accountant_finance_manager
IdempotencyentityType = accounts_payable_weekly, entityId = {businessId}:{isoWeekKey} (e.g. uuid:2026-W36)

Disable by deactivating the system template or removing privileged users (and not configuring rules). There is no AP-page send button in v1.


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