Skip to main content

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​

DependencyWhy
AccountsPayableBillsModuleLoads and validates referenced AP bills (via AccountsPayableBillsService)
PaymentMethodsModuleValidates that used payment methods are active
AuditLogModuleRecords create/update/void events in the audit log
PdfModuleGenerates payment PDF documents via Puppeteer
DatabaseModuleKysely 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 paid
  • detail.items — list of bill references with applied amounts
  • paymentDetail.items — list of payment methods used (cash, bank transfer, etc.)
  • totalAmount — total in payment currency
  • totalBaseAmount — total in base business currency
  • exchangeRate — rate used for cross-currency conversion
  • status — posted | void
  • documentNumber — auto-generated sequential document number
  • primaryBillId — optional shortcut reference when paying a single bill
  • notes — optional internal memo
  • referenceNumber — optional external reference (bank transfer ID, check number)

Void​

A payment is voided (not deleted) when reversed. Voiding:

  • Sets status = void
  • Records voidedAt and voidedBy timestamps
  • Emits an accountsPayablePayment.update event
  • The bills module listens to this event and restores the bill's balanceDue

Main Use Cases​

Create Payment​

  1. Validate all bill IDs exist
  2. Ensure bill statuses are APPROVED or SCHEDULED
  3. Ensure totalAmount equals the sum of detail.items[*].amount
  4. Ensure payment amounts do not exceed each bill's balanceDue
  5. Ensure cross-currency payments provide a valid exchangeRate > 0
  6. Ensure all payment methods are active
  7. Generate sequential documentNumber inside a transaction
  8. Persist and emit accountsPayablePayment.create

Void Payment​

  1. Fetch existing payment
  2. Set status = void, record voidedAt / voidedBy
  3. Persist inside a transaction with audit log entry
  4. Emit accountsPayablePayment.update (bills module restores balanceDue)

List Payments (Standard)​

  • Paginated with optional businessId and supplierId filters
  • Sortable by: documentNumber, paymentDate, createdAt, totalAmount, status
  • Returns payment row joined with supplier name/tax fields

List Payments with Bill Items​

  • Uses the vwAccountsPayablePaymentDetails database view
  • Supports additional accountsPayableBillId filter
  • 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 size
  • GET /:id/print — returns raw HTML for the same template (debug/preview)

Validation Rules​

RuleError CodeDescription
Bills must existBILL_NOT_FOUNDAll accountsPayableBillId values must resolve
Bill status must be payableBILL_STATUS_NOT_APPROVEDOnly APPROVED or SCHEDULED bills may be paid
Total must match itemsTOTAL_AMOUNT_MISMATCHtotalAmount must equal sum of detail.items[*].amount
No overpaymentOVERPAYMENTApplied amount cannot exceed the bill's balanceDue
FX required for cross-currencyFX_REQUIRED_FOR_CROSS_CURRENCYexchangeRate must be > 0 when bill and payment use different currencies
Payment method must be activePAYMENT_METHOD_INACTIVEAll payment methods in paymentDetail.items must be active
Empty detail itemsDETAIL_ITEMS_REQUIREDAt least one bill item is required
Cannot delete posted/voidDELETE_NOT_ALLOWED_FOR_POSTED_PAYMENTOnly draft payments may be deleted

API Endpoints​

MethodPathDescription
POST/accounts-payable-paymentsCreate a new payment
GET/accounts-payable-paymentsList payments (paginated)
GET/accounts-payable-payments/with-bill-itemsList payments with bill line items
GET/accounts-payable-payments/:idGet payment by ID
GET/accounts-payable-payments/:id/pdfDownload payment as PDF
GET/accounts-payable-payments/:id/printPrint preview (HTML)
PATCH/accounts-payable-payments/:idUpdate / void a payment
DELETE/accounts-payable-payments/:idDelete 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​

EventEmitted WhenListener
accountsPayablePayment.createPayment is createdAccountsPayableBillsService — decrements balanceDue on referenced bills
accountsPayablePayment.updatePayment is updated or voidedAccountsPayableBillsService — 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 — see docs/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/

FileEndpoint
accounts payable payments.ymlGET list
accounts payable payments with bill items.ymlGET with bill items
accounts payable payment.ymlPOST create
accounts payable payment by Id.ymlGET by ID
update accounts payable payment.ymlPATCH update/void
delete accounts payable payment.ymlDELETE
accounts payable payment pdf.ymlGET PDF
accounts payable payment print.ymlGET print preview