169 lines
10 KiB
Markdown
169 lines
10 KiB
Markdown
# Customer quotations and payment execution
|
||
|
||
Task [SXCHNGE #50](https://git.sendico.io/tech/sxchnge/issues/50), branch
|
||
`payments-50`, based on `identity-verification-48` / PR #49. Provider prerequisite:
|
||
[Sendico #2896](https://git.sendico.io/tech/sendico/issues/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/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.
|
||
|
||
Delivery: [SXCHNGE PR #51](https://git.sendico.io/tech/sxchnge/pulls/51), stacked
|
||
on PR #49. Merge #47 → #49 → #51. The separate
|
||
[Sendico provider PR #2897](https://git.sendico.io/tech/sendico/pulls/2897) must be
|
||
deployed before enabling this consumer. Both implementation branches are pushed;
|
||
merge, deployment and real financial-resource provisioning remain separate.
|