Supplier payables — shipped boundary
Product version: 1.0.0+1
Last updated: 2026-11-02
Audience: Tenant Owners, Inventory and Finance operators, integrators
This page describes the supplier-payables behavior currently present in this workspace. It is a bounded AP workflow with accounting transitions, receipt-accrual projection, and a permission-gated payables workspace; it is not a claim of a complete AP settlement or reconciliation product.
Purchase receipts
Inventory validates a purchase receipt against the same tenant's active supplier, items, stock pool, bins, and optional requisition. It snapshots supplier and stock facts, rejects invalid quantities/costs, and requires explicit confirmation for zero-cost lines. A receipt starts as draft.
Posting is the boundary at which the injected Inventory stock writer may mutate stock. Only a draft can be posted; posting is idempotent for an already-posted receipt. A posted receipt can be reversed only after downstream dependencies are unwound. The receipt posting boundary does not post a Finance journal.
Finance accepts an idempotent receipt-line snapshot as a pending, no-journal accrual projection. The projection carries received quantity, returned quantity, commercial amount, source revision/sequence, supplier, and currency. Conflicting immutable source data is rejected. Pending accrual data is a projection and must not be described as a posted liability or journal entry. Finance exposes the tenant-scoped receipt accrual status read route, including per-line pending/posted/failed/reversed state and a bounded retryable indicator. Receipt-to-Finance delivery is not a distributed transaction: the inbox claim and failure record are durable. A bounded Finance recovery worker periodically claims failed or stale receipt-projection inbox rows with row locking and replays only the stored Inventory receipt event; it never creates a payment or transfer. The service-token-only reconciliation feed compares Inventory's committed movement watermark with Finance's durable inbox and reports ready, pending, lagging, or failed state without exposing event payloads or mutating accounting.
Supplier bill and payment drafts
Using the supplier payables workspace
Open Finance → Supplier payables (/finance/payables) to review supplier bills, payments, and their current status.
- Automatic requisition-to-payable creation & posting: When an inventory requisition is completed/accepted in Inventory (RequisitionAcceptedEvent), Finance (handle_requisition_accepted → EnsureRequisitionPayableUseCase) automatically creates, approves, and posts the corresponding supplier bill (status="posted") with invoice number equal to the requisition number (REQ-...), linked via requisition_id and requisition_number. The frontend workspace also idempotently syncs accepted requisitions via POST /api/finance/v1/tenants/{tenant_id}/payables/sync-requisition.
- Manual supplier bill drafts: Use Create supplier bill draft (/finance/payables/bills/new, POST /api/finance/v1/tenants/{tenant_id}/supplier-bills/drafts) only for standalone/manual supplier invoices not originating from an inventory requisition (there is no requisition selector on the manual bill form).
- Unified 1-step payment & installment scheduling: From the Supplier Payable Detail page (/finance/payables/:id), users pay or schedule a supplier bill in 1 step via the unified Pay or Split into Installments card (POST /api/finance/v1/tenants/{tenant_id}/payables/{document_id}/pay, PayOrScheduleSupplierBillUseCase):
- Full Payment (دفع كامل المبلغ): Automatically approves/posts the bill if needed, records the external payment against the selected Cash/Bank account (1000/1010), and allocates the full amount to the bill.
- Installment Split (تجزئة الدفعة (أقساط)): Allows paying an optional immediate amount now (0 or partial) and scheduling the remaining balance across custom installment amounts and due dates (installments stored on the bill's supplier_snapshot and surfaced in the Installment Schedule table on the detail page).
- Standalone payment draft UI removed: The separate standalone "New Supplier Payment Draft" screen (/finance/payables/payments/new) was removed from the frontend UI to eliminate confusion; all supplier bill payments and installment schedules are initiated directly from the Supplier Payable Detail page (/finance/payables/:id). (The backend /supplier-payments/drafts endpoint remains for internal/API compatibility.)
The workspace toolbar provides document-type and status filters, a Settings shortcut (/finance/payables/settings), Advanced Accounting Controls (/finance/payables/control), and a consolidated Reports menu that groups:
- Supplier payables aging (/finance/payables/aging)
- Supplier statement (/finance/payables/statement)
- Receipt accounting status (GRNI) (/finance/payables/grni)
- Supplier payment register (/finance/payables/payments)
All date inputs across Supplier Payables reports (Aging, Supplier Statement, GRNI, Payment Register) and controls use interactive Date Pickers (AppDateField) with quick presets (30 days, This month, This year). Advanced Accounting Controls (/finance/payables/control) use searchable account dropdowns, currency dropdowns, date+time pickers for conversion cutoffs, and multi-select filter chips for Inventory and Finance readiness flags. Expand any record card in the list or open its full document detail screen (/finance/payables/:id) to inspect accounting metadata, review the installment schedule and eligible allocations, and run permission-gated workflow actions. Amounts come from accounting data; unavailable information is reported explicitly.
Bill and payment forms group supplier, dates, amounts and accounting references. On validation failure, correct the highlighted field before submitting again. Record an external payment only after it has been completed and checked. Settings retain unsaved edits when a newer revision is detected; reload the current settings before replacing a conflicting draft.
The supplier-bill contract captures a draft with a normalized invoice reference, supplier snapshot, stock/cost snapshots, line quantities and amounts, tax treatment, totals, currency, invoice date, optional due date, and an optional hold reason. A held bill remains held until a later workflow decides otherwise. Draft capture does not create receipt matches or post a bill journal.
The supplier-payment contract captures an immutable draft with supplier and recipient snapshots, payment method, cash account reference, currency, amount, and payment date. Supported methods are cash, card, transfer, and other. The workflow is draft → submitted → approved → recorded; each transition is permission-gated, revision-bound, idempotency-keyed, and held payments cannot advance. Draft creation, submission, and approval do not mutate cash or post a journal. Recording is a separate explicit transition.
Finance provides tenant-bound list and detail reads. They return persisted document fields plus server-derived allocated_amount and outstanding_amount only when the required tenant-scoped allocation facts are available. Current allocation rows contain action identifiers only when source, target, supplier, currency, and amount facts are all trusted; clients must hide remove/reallocate actions when field_availability.allocations is false. field_availability identifies which derived fields are authoritative; clients must not calculate balances from gross_amount when a field is unavailable.
Finance also exposes GET /api/finance/v1/tenants/{tenant_id}/suppliers/{supplier_id}/statement with currency, from_date, to_date, and optional as_of query parameters. The response is a complete, tenant-scoped signed activity statement built from persisted AP documents: posted bills increase the signed balance, while recorded external payments, credits, advances, and refunds decrease it. Draft, submitted, approved, cancelled, reversed, foreign-currency, and truncated-source rows are not presented as authoritative activity. The endpoint is read-only and fails closed when the requested source is too large; it never calculates a payment, mutates cash, calls a provider, or transfers funds.
Finance also exposes GET /api/finance/v1/tenants/{tenant_id}/payables/aging with currency, date_from, date_to, and optional as_of. It returns only posted supplier bills with a positive, allocation-derived outstanding amount, grouped into server-calculated overdue buckets. The source is bounded and complete: a truncated document set or unavailable allocation fact returns a conflict instead of an inferred total. The endpoint is read-only and does not initiate or represent a supplier payment.
The Finance accountant workbook includes a localized Supplier payables aging sheet. It uses the same posted-bill and current-allocation facts, due-date fallback, Decimal numeric cells, and overdue buckets; if that source is incomplete or truncated, the sheet is marked unavailable rather than showing an inferred total. Exporting the workbook is read-only and does not execute or represent a supplier transfer.
The read-only GET /api/finance/v1/tenants/{tenant_id}/payables/payments register lists only recorded payment documents within the requested currency/date range. It shows the user-selected method (cash, card, transfer, or other), the tenant cash/bank account ID used by the internal accounting entry, user-supplied payment reference, allocation versus advance, fee, and journal ID. It is an accounting register, not a transfer log: external_payment_confirmed is always explicit and provider_transfer/supplier_transfer remain not_supported. Missing payment subtype or allocation facts fail closed.
The accountant workbook includes the same payment register as a localized Supplier payment register sheet. Recipient snapshots and private bank details are intentionally not exported; the sheet contains only the safe accounting fields above and marks the source unavailable when it cannot be read completely. The Flutter Finance area opens this report behind the payable-read permission, with explicit currency and date inputs and a complete-source check. It never fabricates totals or starts a payment.
Important: recording requires an explicit external_payment_confirmed: true request. recorded means the tenant has confirmed that money was paid outside SanadiqCloud. The application does not send money, call a bank or card provider, transfer funds between the supplier and the Tenant Owner, or report a failed transfer. A transfer/reference value is evidence supplied by the user, not an execution result. Once the confirmation passes validation, Finance records the payment and stages the balanced supplier_payment_recorded AP-to-cash/bank journal (including a separately identified fee when present); it does not execute the external payment.
Matching, allocation, advances, returns, credits, and refunds
These contracts validate immutable plans and source links. Application code must lock the relevant rows and commit any later persistence atomically.
- Receipt matching requires the same tenant, supplier, and currency; the accrual must be posted and eligible; and requested quantity/amount must stay within both bill-line and receipt capacity.
- A receipt match is first saved as
reserved. An authorized reviewer can finalize it throughPOST /supplier-bills/{document_id}/receipt-matches/{match_id}/finalize, which promotes only the allocation toposted; bill posting still owns the GRNI-to-AP journal and the finalize operation is idempotent. - Payment allocation requires a recorded, unheld payment and posted, unheld bills in the same tenant/supplier/currency scope. Duplicate links, over-allocation, and non-deterministic ordering are rejected. The current route reserves allocation facts only; it does not mutate cash or post a journal.
- The domain contract defines a supplier advance as requiring a recorded payment and the user's explicit advance choice. The tenant-scoped HTTP surface exposes advance creation, advance application/removal, and same-scope payment reallocation as permission-gated, idempotent operations. Creating an advance posts the non-cash DR Supplier Advances / CR AP reclassification; applying an advance posts DR AP / CR Supplier Advances; removing it appends the opposite reclassification and restores bill capacity. Reallocation changes same-scope AP links without cash movement; the implemented reversal flow can unwind persisted allocation/advance dependencies.
- Supplier returns/credit sources are source-linked commercial capacity. A credit note requires posted sources, one tenant/supplier/currency scope, known immutable source IDs, and remaining source capacity. The validated credit note is a commercial correction only; it does not refund cash or post a journal.
- A posted supplier-credit allocation can be reversed through a separate reservation, approval, and execution flow. Execution appends a negative allocation, compensating journal, and reversed AP-ledger facts in one Finance transaction; it never transfers money, calls a provider, or creates a supplier-to-tenant-owner payment.
- A supplier refund can be recorded only after approval, explicit
external_refund_confirmed: true, a valid active tenant-currency cash/bank book, and a posted supplier-credit or recorded supplier-advance source with remaining capacity. Recording stores the externally received refund and its source allocation; it does not call a provider, mutate cash, or post a journal. Its reservation/approval/execution reversal flow restores the source allocation and marks the refund reversed, but likewise does not undo an external receipt, mutate cash, call a provider, or post a journal.
Attachment references
Finance authorizes an active attachment reference only when the attachment belongs to both the requested tenant and document. Missing, foreign-tenant, foreign-document, or removed references resolve as unavailable so identifier probing does not reveal which case occurred. The result contains reference metadata such as media_id, hash, file name, and media type. Media now exposes a separate authenticated finance-service internal route for a short-lived supplier_payable URL after its own tenant/private-media check; Finance does not issue URLs, serve blobs, or provide a delete command, and reference/hash/document association still has to be verified before the client consumes that route.
Dashboard read model
On the Merchant Dashboard (/dashboard), the Attention (انتباه) widget is positioned first at the top, followed directly by Supplier Payables (ذمم الموردين) (which displays outstanding_ap, overdue_ap, due_today_ap, due_soon_ap, cash_paid, and accounting_pending).
Finance emits rebuild-only facts when an external refund is recorded, a supplier credit is allocated, an advance is applied or removed, or a payment allocation is reallocated. These events refresh the Dashboard read model only; they do not execute cash movement, provider transfers, or supplier transfers.
Approval versus payment
Approving a supplier bill or payment is an internal revision-bound workflow fact. SupplierBillApprovedEvent and SupplierPaymentApprovedEvent are audit/read-model signals only. Payment approval means an authorized operator may later record that the supplier was paid externally; it does not send money, call a bank/provider, or create a supplier-to-tenant-owner transfer. Only the later operator-confirmed recording creates the internal accounting entry, and it remains explicitly marked provider_transfer=not_supported and supplier_transfer=not_supported.
The supplier-payables dashboard is built from one Finance-owned absolute snapshot, in the tenant functional currency. It includes as_of, business date, business timezone, cutoff sequence, and the following definitions:
| Metric | Definition |
|---|---|
outstanding_ap |
Positive posted supplier-bill balances after effective payments, credits, advances, and reversals; drafts and voided bills are excluded. |
overdue_ap |
Outstanding posted-bill balance with due date before the tenant business date. |
due_today_ap |
Outstanding posted-bill balance with due date equal to the tenant business date. |
due_soon_ap |
Outstanding posted-bill balance due from the tenant business date through business date + 7 days, inclusive. |
held_ap |
Gross amount of supplier bills held for unresolved mismatch or review; held drafts and voided bills are excluded from posted liabilities. |
grni_pending |
Commercial received-not-invoiced accrual, net of returns and matching; separate from accounting-pending receipts. |
cash_paid |
Posted supplier-payment cash outflows in the selected date range, including an advance once; reversals reduce the total. |
cash_refunds |
Posted supplier-refund cash inflows in the selected date range; reversals reduce the total and refunds are not counted as supplier payments. |
accounting_pending |
Count and oldest age of receipt, bill, or payment postings pending or failed; these are not posted liabilities. |
Without the read permission or an available snapshot, the contract returns restricted/unavailable metrics and no attention items. A partial source feed can also mark individual metrics unavailable; unavailable values retain null value/currency and are not inferred. It does not infer financial values from partial data. The current Flutter surface is read-only and does not fabricate totals when the snapshot is unavailable.
Explicit limitations
The shipped slice does not claim supplier-payment execution, bank/provider integration, payment-transfer failure states, a complete AP settlement workflow, Media blob deletion, or production rollout. Receipt projection recovery is bounded to the Finance worker and the existing broker retry path; cross-service atomicity and hosted cutover evidence remain separate gates. Supplier-bill posting and externally confirmed supplier-payment recording do create Finance accounting effects. A posted receipt accrual can now append an idempotent compensating journal during receipt reversal when its source journal and target open period are available; dependency-unwind and cross-service atomicity checks still fail closed. A posted supplier-bill reversal now appends match/allocation unwind facts, a compensating AP journal, reversed ledger rows, and a SupplierBillReversedEvent in one Finance transaction. Recorded payment reversal appends compensating accounting facts and unwinds persisted allocation/advance dependencies, including currently applied advance allocations through the append-only unapplication workflow, but it does not transfer cash or call a provider. Pending receipt accruals remain no-journal, supplier-refund recording/reversal remains non-cash/no-journal, and payment reallocation remains non-cash/no-journal. Advance application/removal now has the append-only non-cash reclassification facts described above; separately authorized closed-period date lineage remains bounded. Inventory remains the source of truth for stock, and Finance does not expose a general-purpose attachment/blob API.
Permissions and tenant bootstrap
The read route requires finance:payable:read. Business mutations use the specific supplier-bill, receipt-match, supplier-payment, and finance:payable:reconcile permissions; platform-support identities and service tokens are denied AP business writes. Built-in starter roles and older organizations do not automatically gain every new AP permission: an Owner or Administrator must grant the required keys under Team → Roles. Owner approval exceptions fail closed unless authoritative tenant-owner identity is available.
Finance CoA, fiscal year, cash books, and payment maps are bootstrapped when a tenant is created. Existing tenants must be made Finance-ready before supplier bill posting or payment recording; use the Finance bootstrap/heal endpoint only with the authorized tenant context. Account listing is read-only and never provisions missing seeded Finance/AP accounts. A payment must use an active, non-custody cash/bank book in the tenant currency.
If supplier-payables settings have not been provisioned, the settings page shows a setup form instead of a generic failure. The tenant owner enters the currency and IANA accounting timezone and selects Set up supplier payables. Opening the page alone never provisions or activates purchasing.
For an existing tenant, the authoritative owner with finance:payable:configure may explicitly call
POST /api/finance/v1/tenants/{tenant_id}/configuration/provision with the
functional currency and accounting time zone. This validates and binds the
canonical AP accounts and leaves the configuration in preparing; it does not
activate cross-service purchasing, post a journal, or transfer money. The
cross-service activation handshake must complete before AP business writes are
enabled. Reprovisioning an already active configuration is rejected.
The Inventory side of that handshake is service-to-service only. It reports
readiness only when its supplier and valuation streams have contract
version 2 and the same non-null activation generation; missing or mismatched
stream state remains unavailable.
After provisioning, the authoritative tenant owner may call
POST /api/finance/v1/tenants/{tenant_id}/configuration/activate with an
approved reconciliation_batch_id, its current revision, and contract
version. Finance asks Inventory to acknowledge the same generation and moves
to active only when Inventory confirms readiness. A failed or unavailable
acknowledgement leaves the configuration in preparing.
The owner can read the tenant-scoped GET /purchasing-settings and update
PATCH /purchasing-settings with the current revision. Updates use optimistic
revision checks, require a confirmed timezone while active, and validate every
referenced account as an active, posting tenant account with the required
account type. POST /payables/suspend requires the current revision and an
audit reason. Suspension changes control state only and does not reverse a
journal, call a payment provider, or transfer funds.
Cutover reconciliation batches
Before enabling a historical cutover, Finance can record a draft evidence set
with POST /api/finance/v1/tenants/{tenant_id}/payables/reconciliation-batches.
The request contains a timezone-aware cutover_at, currency, the committed
Inventory and Finance watermarks, and non-empty evidence metadata. The response
is a tenant-scoped draft with an optimistic revision. It records evidence
only; it does not import balances, post journals, or move money.
The authoritative tenant owner approves the draft with
POST .../payables/reconciliation-batches/{batch_id}/approve, supplying the
current revision and a reason. Owner lookup is fail-closed. Approval is
append-safe through revision checking and does not activate AP by itself; the
later activation handshake must still be completed. Never use this workflow to
represent a supplier, bank, provider, or Tenant Owner transfer.
Permission-scoped attention recipients
Supplier-payables attentions must not use a tenant-wide notification
broadcast. Notification Service resolves active tenant members through the
internal Auth Service permission-recipient contract with the required tenant
permission (for example finance:payable:read), and fails closed when the
tenant or permission echoed by Auth does not match the request. Inactive users
and inactive roles are excluded. Reading or delivering an attention does not
settle a payable and does not initiate a supplier, bank, provider, or Tenant
Owner transfer.
Dashboard rebuild snapshot
The service-token-only POST /api/finance/v1/internal/dashboard-supplier-payables/{tenant_id}
endpoint is the Finance source for Dashboard rebuilds. It locks the tenant's
supplier-payables stream and returns one absolute snapshot from posted AP,
GRNI, recorded payments/refunds, allocations, and pending accounting states.
If the watermark is absent or an allocation would exceed a posted bill, the
request fails closed. The endpoint never initiates or reports a provider or
supplier transfer.
Operations and Dashboard may also read the service-token-only
GET /api/finance/v1/internal/supplier-payables-reconciliation/{tenant_id}
feed. It returns watermarks, bounded inbox counts, and a retryable flag for
monitoring and cutover evidence. It never returns raw event payloads and never
mutates Finance or Inventory.
Supplier choices
Supplier names come from Inventory. Finance keeps a tenant-scoped identity projection for its supplier selector; creating, editing, deactivating or deleting a supplier updates this projection asynchronously. An inactive or deleted supplier cannot be selected for a new draft. If an existing active supplier is missing, an operator can reconcile its identity using the internal repair procedure in the operations runbook. This does not create a bill or payment.
Deadline reminders
Dashboard refreshes supplier-payables attentions at the tenant business-date
boundary, even when no Finance event arrives. It emits a reminder only when a
condition enters a new bucket: raised, due-soon, due-today,
overdue-1, overdue-7, or overdue-30. Notification Service rechecks the
permission audience and stores a durable claim per condition revision,
recipient, channel, and reminder bucket. A repeated rebuild is therefore
safe and does not send a notification every day. These reminders are review
signals only; they do not approve, record, or transfer a payment.
Operational checks
From the repository root, the authoritative service-isolated checks are:
Push-Location apps/backend/services/finance_service
python -m pytest -q --disable-warnings
Pop-Location
Push-Location apps/backend/services/documentation_service
python -m pytest -q --disable-warnings
Pop-Location
For a live deployment, confirm the gateway and service workloads before investigating a payable as missing:
curl -sf https://api.sanadiqcloud.com/health
kubectl get deployments,pods,services -n sanadiq
kubectl logs deployment/finance-service -n sanadiq --since=30m
kubectl logs deployment/inventory-service -n sanadiq --since=30m
kubectl rollout status deployment/finance-service -n sanadiq --timeout=300s
These commands diagnose service availability only; they do not retry a receipt projection, reconcile AP, or transfer money.
Related: Finance overview · Finance boundaries · Posting lifecycle · Financial reports · Finance permissions · Finance events · Dashboard KPI reference