X/V/A reconciliation and exports
This back-office surface finds money that has not reconciled with the ledger and exports the same frozen snapshot for offline review. It never exposes bank-account data, vendor identity, liquidity routing, or an object-storage location.
Ready for the deployment procedure
The DB, HTTP, UI, and worker contracts for reconciliation.read, export.create, and export.auth.step-up have passed the repository release gates and are now serverBacked: true as one vertical. Navigation appears only after an operator deploys this release and the authoritative bootstrap grants the required capability/API contracts; no remote deployment is implied by this source status.
Scope boundary
| Current scope | self | subtree |
|---|---|---|
| White Label | Current scope | The same White Label tree |
| Agent | Current scope | That Agent and its Merchants |
| Merchant | Current Merchant | Denied |
Reading requires reconciliation:read plus the reconciliation API contract. Creating a file also requires exports:create, the exports contract, and the source dataset's read authority. A visible route or role label is not authorization. PostgreSQL re-checks membership, hierarchy, environment, and scope on every request; an out-of-tree target receives the same generic 404 as any other inaccessible target.
X, V, and A
X = ordered_minor amount requested by the order
V = transferred_minor amount from authoritative transfer evidence
A = accepted_minor amount accepted by the platform
net = A - fee| Case | Rule |
|---|---|
| Exact | V=X and A=X |
| Short pay | Accept the observed amount: A=V |
| Overpayment | Accept no more than the order: A=X; close the work and set action_required=contact_support |
| No evidence yet | V=null, A=fee=net=0, pending |
| Operator decision needed | Explicit manual review; never represented as balanced |
An overpayment never creates an automatic excess credit. The customer follows the row's flag and contacts back-office Support.
balanced means accepted, fee, and net agree with the immutable ledger, not merely that V=X. variance requires at least one ledger difference. The response adapter recomputes every money equation and retains minor-unit amounts as decimal strings.
Frozen pages
Page one uses:
GET /v1/bo/reconciliation?scope_id={scope}&livemode=true¤cy=THB&view=self&from={UTC}&to={UTC}&limit=50
X-PG-BO-Client: 1The interval must be positive, no longer than 31 days, and not in the future. Page one omits both cursor and as_of; PostgreSQL pins as_of after authorization. Every continuation sends the previous cursor and exactly the same as_of. Changing scope, mode, interval, status, resolution, or limit invalidates the cursor.
Rows have stable (occurred_at DESC, reconciliation_ref DESC) order. Facts later than as_of cannot enter an existing snapshot.
Asynchronous CSV creation
The export command snapshots one source:
reconciliation— X/V/A, fee, net, and ledger differences;statement— immutable-ledger postings; orfinancial_report— scoped volume and economics.
Each dataset has a fixed column set and a maximum row_limit of 100,000. An oversized result ends as row_limit_exceeded; no successful file is silently truncated. The file expires after seven days and excludes names, email, phone, account numbers, raw evidence, metadata, and internal routing data.
Creation opens an OIDC popup whose callback type is paygate:export-step-up. Its proof binds method, path, scope, livemode, body digest, idempotency digest, query_digest, and column_set_hash. The authenticated browser then submits the original bytes to:
POST /v1/bo/exports
If-Match: "0"
Idempotency-Key: {stable-key}
X-PG-Step-Up-Token: pgsea1.{opaque}
X-PG-BO-Client: 1
Content-Type: application/json; charset=utf-8queued and processing are truthful asynchronous states. Reusing the same idempotency key with the same body returns the same job and never creates a second snapshot.
Metadata-safe one-time download
A ready job returns a handoff valid for no more than five minutes:
{
"scheme": "pg_opaque_grant_v1",
"method": "POST",
"path": "/v1/bo/export-downloads:consume",
"one_time": true,
"grant": "{opaque-base64url}",
"issued_at": "{canonical-UTC}",
"expires_at": "{canonical-UTC}"
}The UI clears the grant-bearing handoff from global/reactive state before invoking the download helper. It never puts the grant in an href, URL, query, or header. The helper sends exactly one JSON member to the static path:
POST /v1/bo/export-downloads:consume
Accept: text/csv
Content-Type: application/json
X-PG-BO-Client: 1
{"grant":"{opaque-base64url}"}The request uses same-origin cookies, Origin, and Fetch Metadata, and redirects are forbidden. A successful response must be 200, text/csv; charset=utf-8, gzip, a safe .csv attachment, private, no-store, nosniff, no-referrer, and CSP default-src 'none'; base-uri 'none'; frame-ancestors 'none'. It has no ETag, Digest, R2 locator, or redirect.
The grant is one-time. A generic 404 covers used, expired, foreign, wrong-scope, and inactive-session cases without disclosing which condition occurred. 502 indicates an integrity or response-contract mismatch. The UI does not automatically retry 404 or 502.
Readiness gate
Before enabling the feature, pass fresh 0083/0084 migration replay and pgTAP, DB/API/worker/Money/Admin tests, checksum and expiry against the private Money-only R2_EXPORTS bucket, one-time consume races, Tail native Workers Logs redaction, contract/OpenAPI drift, UI tests/build/ZIP, and staging smoke. Only then set all three operation-readiness flags to true and rebuild the White Label artifacts.
