Files
sxchnge/docs/payments.md

12 KiB
Raw Permalink Blame History

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} and GET /ledger_accounts/{organization}/{ledger}/balance;
  • POST /payments/quote/{organization} with external_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 create/read, balance read and the permissions needed to resolve its stored payment methods. Verify those permissions in the intended environment before enabling real operations.

Automatic currency accounts (#72)

The backend ensures RUB, USDT and USDC accounts after confirmed identity intake during signup, before sending the activation email. Resending activation retries the same setup. Every authenticated wallet load also ensures the defaults, so existing customers receive missing accounts automatically when they next open their wallet. This is lazy backfill, not an eager scan of dormant customers.

Setup creates dedicated unowned organization ledgers through the public POST /ledger_accounts/{organization} API, with active asset / operating semantics, zero initial funds, no negative balance and metadata:

Ledger metadata Meaning
sxchnge_subject_ref Confirmed independent subject reference from sign-up
external_subject_ref Same confirmed subject; provider-enforced organization/subject/currency uniqueness
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

Existing customer ledgers and their balances are preserved, including manually provisioned ledgers with the original sxchnge_subject_ref tag. Only missing defaults are created; existing additional currencies remain visible. Repeated bound creates reuse the ledger and resume Sendico's durable wallet/route setup after an interrupted response. Frozen, duplicate or incorrectly owned bindings fail instead of creating a replacement. All required currency precision entries are checked against the provider catalog before issuing creates. Saved funding and payout bindings remain separate from creation of the currency accounts.

Deploy Sendico #2968 first: Ledger (including its unique index), then Product Edge, then SXCHNGE backend and frontend from PR #73. The public ledger list must advertise externalSubjectAccounts: true before SXCHNGE sends any create requests. Older providers fail safely without creating accounts. The acknowledgement assumes Ledger and Edge are deployed in that order; it does not discover the version of a separately running Ledger. No new runtime configuration or credentials are introduced. Existing deployments must give the integration principal the ledger-create permission in addition to its reads.

These are financial-resource bindings maintained by server-side setup, 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.

If setup reports a failure, the API returns an unavailable wallet rather than an empty snapshot or demo balances. Retrying the app load retries setup. 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.

Delivery: SXCHNGE PR #51, stacked on PR #49. Merge #47 → #49 → #51. The separate Sendico provider PR #2897 must be deployed before enabling this consumer. Both implementation branches are pushed; merge, deployment and real financial-resource provisioning remain separate.