10 KiB
Customer quotations and payment execution
Task SXCHNGE #50, branch
payments-50, based on identity-verification-48 / PR #49. Provider prerequisite:
Sendico #2896.
Boundary and behavior
The authenticated app uses BackendWalletService whenever backend authentication
is configured. Explicit service injection remains available for tests; demo mode
continues to use the isolated in-memory wallet. Missing provider configuration
returns an unavailable wallet, never demonstration balances or fake settlements.
The path is Flutter DTO → mapper → service → domain model → provider → UI, then SXCHNGE's authenticated customer BFF → Sendico public HTTP. The existing confirmed identity binding resolves each signed-in customer to an independent subject. Neither a browser-selected organization nor an integration-login-owned balance is used as that customer's financial resource.
Consumed public endpoints, below the existing configured API prefix:
GET /payments/fx/currencies/{organization}for accounting scales;GET /ledger_accounts/{organization}andGET /ledger_accounts/{organization}/{ledger}/balance;POST /payments/quote/{organization}withexternal_subject_ref;POST /payments/by-quote/{organization};GET /payments/by-ref/{payment}for authoritative status.
The adapter requires the exact externalSubjectRef acknowledgement on the quote
response. An old deployment that ignores the new request field cannot silently
produce an acceptable integration-principal quote. Public provider DTOs and
mappers stay inside the adapter. No Sendico modules, databases, gRPC clients or
message bus are imported by SXCHNGE.
Amounts are parsed and formatted as exact decimal strings at the provider boundary and represented as integer minor units locally. Unknown scales, values beyond the browser's exact integer range, excess fractional precision, unknown payment states, unexecutable quotes and mismatched payment/quote references fail closed. Source principal, total debit and net destination credit come from the quote; rates account for different currency precisions. Only same-currency saved payout/funding methods are currently represented by the wallet review model. Exchange connects two dedicated currency ledgers belonging to the same customer.
Provisioning and rollout
Merge/deploy the identity prerequisites first: SXCHNGE #47 → #49 and the Sendico
identity-proof/limits-assignment work documented in identity-verification.md.
Deploy the Sendico #2896 Product Edge and the Quotation/Orchestrator builds that
consume the changed shared payment-storage Limits adapter together. Then deploy
SXCHNGE #50. The Edge acknowledgement is a compatibility guard; it does not replace
coordinated deployment of the platform's financial services.
Reuse api.sendico and its existing Vault-rendered token file. Do not put provider
credentials or organization/source settings in Flutter. Provision the integration
account's organization membership, payment-create/read, ledger-account/balance
read and the permissions needed to resolve its stored payment methods. Verify
those permissions in the intended environment before enabling real operations.
Through Sendico's existing operator APIs, provision one dedicated unowned
organization ledger per customer currency, with active asset / operating
semantics, no negative balance and metadata:
| Ledger metadata | Meaning |
|---|---|
sxchnge_subject_ref |
Confirmed independent subject reference from sign-up |
sxchnge_payout_method_ref |
Optional prevalidated same-currency saved payout method |
sxchnge_payout_label |
Safe customer-visible destination label, e.g. bank plus masked suffix |
sxchnge_funding_method_ref |
Optional same-currency saved funding source supported by the platform route |
These are financial-resource bindings maintained by trusted provisioning, not client choices or new runtime configuration. Do not tag a pooled/integration-login ledger as a customer ledger. Do not place full bank/card details or secret values in labels or metadata. The BFF checks subject, organization, account type, role, status and owner on each account projection/quotation. It accepts only the bound saved payout method and replaces its opaque reference with the safe label in the reviewed quote. No arbitrary recipient identifier or free-text bank input can be executed from the live UI. Missing bindings have an explanatory disabled review action. Funding uses a quote for the provisioned stored source; it does not mint money or treat demo top-ups as real deposits. Additional destinations, recipient onboarding and other funding instructions remain separate features.
An empty set of provisioned ledgers is an empty wallet. Existing organization payments and pooled balances are never imported into customer history. This integration exposes the durable operations initiated through this BFF, with live provider reconciliation; independent inbound deposits/history ingestion and pagination are not added by this contribution. Ledger balances are posted ledger projections; execution-time solvency and holds remain the platform's authority.
Authorization, idempotency and recovery
Local routes are under /accounts/wallet: snapshot GET, /profile GET,
/quote, /challenge, /resend, /verify, /submit and /submission POST.
All use the existing full authenticated-account wrapper, access token and browser
CSRF guard. Pending-login credentials cannot access them. Local quote references
are random BFF IDs; provider quote/payment references are not accepted as user
ownership evidence.
payment_quotes stores customer, adapter namespace, confirmed subject, immutable
quote, upstream quote reference, authorized execution key and eventual payment
reference. A unique partial index enforces customer/namespace/key uniqueness.
The existing second-factor service uses the dedicated payment purpose and
exact local quote context. Successful code verification consumes its one-use
proof in the same local Mongo transaction that records execution authorization.
No external I/O runs in that transaction and raw factor proofs never leave the
server. Verification response retries retain their key and recover committed
intent. Resend uses a separate stable-per-retry issuance key.
Submit/reconciliation replays sxchnge:payment:<local-quote-id> at Sendico.
A quote cannot be authorized under a second execution key. A network failure or
lost provider response remains uncertain; it never permits execution with a new
provider key. Once the provider payment reference is attached, refresh uses GET.
Authorization is durable: reopening the wallet reconciles authorized intents even
if the original client never received verification/submission success. Unknown
outcomes remain pending and do not hide unrelated balances. needs_attention
also stays pending; only settled/failed become terminal UI outcomes.
Generic upstream errors cannot prove that no payment exists. An unresolved pre-execution rejection (including an expired quote after durable authorization) therefore requires operational reconciliation rather than automatic release or a new payment. Inspect the local quote/provider reference and platform audit trail; never delete/rekey a record to bypass uncertainty. The UI's live copy does not promise a debit, refund or completion from mere acceptance. Only local drafts, not durable authorized intents, count as abandoned when a user leaves.
The adapter rejects changed-session HTTP responses, and the app clears wallet snapshots/journeys when the authenticated customer changes. Account-language reads use the authenticated customer. Backend language saving is deliberately unsupported and reports failure; the previous demo setting is not presented as persisted backend state.
UX, observation and validation
The existing typed transfer journey recorder measures draft → quotation → second-factor challenge/verification → submission → authoritative settlement. Attempts/retries share their client journey; accepted operations finish once when the wallet observes a terminal platform status. An uncertain result is not success. Leaving an unresolved asynchronous operation is not draft abandonment. Use the existing per-attempt timestamps for durations and a 30-minute inactivity window for unfinished local drafts; server-authorized pending operations remain outside that abandonment denominator. Compare draft-to-quote and quote-to-submit drop-off, intent-to-terminal completion/failure and time to terminal outcome. The existing recorder provides in-memory client evidence only: deployment of a retained analytics sink and backend event delivery is needed for cross-session conversion/settlement measurement. No amounts, recipients, bank labels, codes or credentials enter analytics. Structured server logs use quote/payment references and bounded states; operational reconciliation uses the durable database record.
Tests cover public wire requests, exact money, source/destination isolation, blocked/unknown states, factor errors and retry keys, changed-session responses, durable authorization rollback/uniqueness, lost-response reconciliation and customer/provider-namespace isolation. Widget checks exercise saved destinations and the missing-destination state at 320px with 1.6× text, alongside the existing EN/RU workspace flows. Default Flutter keyboard/focus semantics are retained; full browser/screen-reader acceptance and live funded settlement are deployment checks, not claimed by synthetic tests. No live payments or deployment are run.
Validation passed after review: make test (backend race tests and all frontend
suites), make lint-backend, a subsequent make test-frontend after the final
long-label UI adjustment, changed-Dart format verification, and flutter analyze
in frontend/app and frontend/packages/sxchnge_wallet. The final frontend run
passed 21 wallet, 20 KYC, 7 analytics, 40 auth, 3 theme, 3 theme-example and 81 app
tests. The provider prerequisite passed Sendico's full backend test/lint targets.