Accounts Payable Payments
Overview
The accounts-payable-payments module records supplier payments against AP bills. It handles the full payment lifecycle: validation, document number generation, audit logging, event emission for bill balance reconciliation, and PDF generation.
Architecture
This module follows Hexagonal Architecture with four layers:
domain/ — Repository interface + injection token (ACCOUNTS_PAYABLE_PAYMENTS_REPOSITORY)
application/ — Business logic (AccountsPayablePaymentsService)
infrastructure/ — Kysely DB adapter (AccountsPayablePaymentsRepository)
interfaces/ — HTTP controller, DTOs, query objects
The repository is injected via a Symbol token (ACCOUNTS_PAYABLE_PAYMENTS_REPOSITORY) so the service depends only on the domain interface, not the concrete implementation.
Module Dependencies
| Dependency | Why |
|---|---|
AccountsPayableBillsModule | Loads and validates referenced AP bills (via AccountsPayableBillsService) |
PaymentMethodsModule | Validates that used payment methods are active |
AuditLogModule | Records create/update/void events in the audit log |
PdfModule | Generates payment PDF documents via Puppeteer |
DatabaseModule | Kysely database connection for queries and transactions |
Domain Concepts
Payment
A payment represents money transferred to a supplier to settle one or more AP bills.
Key fields:
supplierId— the supplier being paiddetail.items— list of bill references with applied amountspaymentDetail.items— list of payment methods used (cash, bank transfer, etc.)totalAmount— total in payment currencytotalBaseAmount— total in base business currencyexchangeRate— rate used for cross-currency conversionstatus—posted|voiddocumentNumber— auto-generated sequential document numberprimaryBillId— optional shortcut reference when paying a single billnotes— optional internal memoreferenceNumber— optional external reference (bank transfer ID, check number)
Void
A payment is voided (not deleted) when reversed. Voiding:
- Sets
status = void - Records
voidedAtandvoidedBytimestamps - Emits an
accountsPayablePayment.updateevent - The bills module listens to this event and restores the bill's
balanceDue
Main Use Cases
Create Payment
- Validate all bill IDs exist
- Ensure bill statuses are
APPROVEDorSCHEDULED - Ensure
totalAmountequals the sum ofdetail.items[*].amount - Ensure payment amounts do not exceed each bill's
balanceDue - Ensure cross-currency payments provide a valid
exchangeRate > 0 - Ensure all payment methods are active
- Generate sequential
documentNumberinside a transaction - Persist and emit
accountsPayablePayment.create
Void Payment
- Fetch existing payment
- Set
status = void, recordvoidedAt/voidedBy - Persist inside a transaction with audit log entry
- Emit
accountsPayablePayment.update(bills module restores balanceDue)
List Payments (Standard)
- Paginated with optional
businessIdandsupplierIdfilters - Sortable by:
documentNumber,paymentDate,createdAt,totalAmount,status - Returns payment row joined with supplier name/tax fields
List Payments with Bill Items
- Uses the
vwAccountsPayablePaymentDetailsdatabase view - Supports additional
accountsPayableBillIdfilter - Returns one row per payment-bill pair (useful for reconciliation UIs)
Generate PDF / Print Preview
GET /:id/pdf— streams a PDF with configurable margins and page sizeGET /:id/print— returns raw HTML for the same template (debug/preview)
Validation Rules
| Rule | Error Code | Description |
|---|---|---|
| Bills must exist | BILL_NOT_FOUND | All accountsPayableBillId values must resolve |
| Bill status must be payable | BILL_STATUS_NOT_APPROVED | Only APPROVED or SCHEDULED bills may be paid |
| Total must match items | TOTAL_AMOUNT_MISMATCH | totalAmount must equal sum of detail.items[*].amount |
| No overpayment | OVERPAYMENT | Applied amount cannot exceed the bill's balanceDue |
| FX required for cross-currency | FX_REQUIRED_FOR_CROSS_CURRENCY | exchangeRate must be > 0 when bill and payment use different currencies |
| Payment method must be active | PAYMENT_METHOD_INACTIVE | All payment methods in paymentDetail.items must be active |
| Empty detail items | DETAIL_ITEMS_REQUIRED | At least one bill item is required |
| Cannot delete posted/void | DELETE_NOT_ALLOWED_FOR_POSTED_PAYMENT | Only draft payments may be deleted |
API Endpoints
| Method | Path | Description |
|---|---|---|
POST | /accounts-payable-payments | Create a new payment |
GET | /accounts-payable-payments | List payments (paginated) |
GET | /accounts-payable-payments/with-bill-items | List payments with bill line items |
GET | /accounts-payable-payments/:id | Get payment by ID |
GET | /accounts-payable-payments/:id/pdf | Download payment as PDF |
GET | /accounts-payable-payments/:id/print | Print preview (HTML) |
PATCH | /accounts-payable-payments/:id | Update / void a payment |
DELETE | /accounts-payable-payments/:id | Delete a draft payment |
Full Swagger documentation is available at /docs when the backend is running.
Example: Create Payment
POST /accounts-payable-payments
{
"supplierId": "uuid",
"status": "posted",
"currencyId": "uuid",
"currencyCode": "GTQ",
"minorUnit": 2,
"exchangeRate": 1.00,
"currency": { "id": "uuid", "name": "Quetzal", "symbol": "Q" },
"totalAmount": 95.00,
"totalBaseAmount": 95.00,
"paymentDate": "2026-03-12",
"businessId": "uuid",
"createdBy": "uuid",
"detail": {
"items": [
{ "accountsPayableBillId": "uuid", "amount": 95.00, "baseAmount": 95.00 }
]
},
"paymentDetail": {
"items": [
{
"paymentMethodId": "uuid",
"paymentMethodName": "Cash",
"amount": 95.00,
"baseAmount": 95.00,
"currencyId": "uuid",
"currencyCode": "GTQ",
"minorUnit": 2,
"exchangeRate": 1.00
}
]
},
"notes": "Monthly supplier payment",
"referenceNumber": "TRX-20260312-001"
}
Example: Void Payment
PATCH /accounts-payable-payments/:id
{
"status": "void",
"updatedBy": "uuid"
}
Events
| Event | Emitted When | Listener |
|---|---|---|
accountsPayablePayment.create | Payment is created | AccountsPayableBillsService — decrements balanceDue on referenced bills |
accountsPayablePayment.update | Payment is updated or voided | AccountsPayableBillsService — restores balanceDue if voided |
Database
- Table:
accountsPayablePayment - View:
vwAccountsPayablePaymentDetails— joins payment rows with their bill line items (one row per bill-payment pair) - Reporting views (Metabase):
accounts_payable_payments_v— seedocs/accounts-payable/dashboards.md
Key Design Decision: JSONB for detail fields
detail and paymentDetail are stored as jsonb columns rather than normalized tables. This trades query flexibility for schema stability and write simplicity — payment records are essentially append-only financial documents.
Bruno API Collection
All endpoints are covered in:
api-client/flowpos/collections/accounts-payable-payments/
| File | Endpoint |
|---|---|
accounts payable payments.yml | GET list |
accounts payable payments with bill items.yml | GET with bill items |
accounts payable payment.yml | POST create |
accounts payable payment by Id.yml | GET by ID |
update accounts payable payment.yml | PATCH update/void |
delete accounts payable payment.yml | DELETE |
accounts payable payment pdf.yml | GET PDF |
accounts payable payment print.yml | GET print preview |