nigig-org/crates/apps/nigig-pay-ui/Cargo.toml
andodeki 1d3e6ab72a
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
doc-engine / consumer (push) Successful in 3m56s
Payment domain, storage, platform and UI / payment-ui-tests (push) Failing after 4s
doc-engine / engine (push) Successful in 15s
nigig-map / test (push) Failing after 2s
Payment domain, storage, platform and UI / isolated-payment-tests (push) Failing after 1m58s
sms / gates (push) Successful in 3s
sms / robius-sms (push) Successful in 22s
sms / android (push) Successful in 21s
sms / nigig-sms (push) Successful in 4m6s
sms / supply-chain (push) Successful in 4s
fix(pay): correlate USSD callbacks to the payment that asked for them
R2.1. Review items 2.7 and 5.3.

## SessionRegistry was built in Phase 5 and never wired

The pump still read:

    while let Some(ev) = robius_ussd::next_event() {
        ... if let Some(id) = h.current.take() { ... }
    }

next_event() drains a process-wide queue and its entries carry no session
id, so every event was applied to whatever `current` happened to be.

Reproduced before changing anything: payment A is dispatched then abandoned
with events still queued; payment B starts; the pump drains A's ResultText
and SessionEnded and applies both to B. An abandoned payment settles the one
that replaced it.

## Now

- Dispatch claims the single in-flight slot. The USSD backend returns no
  session handle, so the intent id is the correlation id — enough, because
  the registry only has to tell this payment from the previous one.
- Every event is admitted against the live operation before it can touch an
  intent. Foreign and stale events are logged and dropped.
- Terminal events are de-duplicated; progress chatter still repeats freely.
  ussd_duplicate_key mirrors ProviderSignal::duplicate_key in the platform
  crate, and a test pins the two together.
- All six terminal and teardown paths retire the session id, so a late
  duplicate cannot revive a closed operation.

6 tests, including the abandoned-payment scenario by name. Verified by
removing the close call: that test goes red. CI gate asserts the pump still
admits, dispatch still claims, and at least six paths still close — matching
the method rather than a receiver literal, because rustfmt wraps the call.

## What this does not do

The pump still lives in PayFlowHandler, which still owns the pending-store
writes and the bulk queue. Moving *ownership* to PaymentCoordinator changes
who cancels on teardown and who observes an out-of-order callback, which is
what ADR 0007's device matrix exists to check. That is now tracked as R2.1b.

The correlation defect — the one that could settle the wrong payment — is
closed, and it did not need a device. I had previously filed the whole of
R2.1 as device-blocked; that was too coarse.

## Validation

  nigig-pay-ui 78 (was 72) / nigig-mpesa 20                        pass
  domain 148 / storage 41 / platform 64 / mpesa 29                 pass
  clippy -p nigig-pay-ui --no-deps -D warnings                     0 errors
  builds: pay-ui, pay, mpesa, core; default and --no-default       pass
  correlation injection: abandoned-session test fails without it   pass
  pin-capture guard                                                pass
2026-08-02 09:21:45 +00:00

53 lines
2.5 KiB
TOML

[package]
name = "nigig-pay-ui"
version = "0.1.0"
edition = "2021"
description = "Shared payment UI and logic for nigig-mpesa and nigig-pay."
[features]
## This leaf crate stays default-off on purpose. The app crates above it
## enable `demo` through their own defaults and depend on this crate with
## `default-features = false`, so a packaging build that passes
## `--no-default-features` to the app actually turns the automation off
## instead of having it re-enabled here.
##
## Original note, still applicable:
## USSD automation is ON by default in the app crates.
##
## Review item 0.1 asked for dispatch behind a compile-time flag. That was
## implemented as default-off, which made the *334# automation — the app's
## primary function — unreachable in an ordinary build, and unreachable at
## all from the APK crate until 9f0e133.
##
## The containment requirement has not been dropped; it has moved to release
## packaging, which is where a shipping decision belongs. A Play-listed build
## must be produced with `--no-default-features` (plus the features it does
## want), and the unresolved Google Play policy risk is recorded in
## REVIEWS/adr/0007-payment-platform-boundary.md.
##
## Turning it off for a build:
## cargo build -p pageflipnav --no-default-features --features native
default = []
## Enable USSD dispatch, auto-retry, and bulk pay.
## Production builds must NOT enable this until authorized provider integration exists.
demo = []
[dependencies]
makepad-widgets = { git = "https://gitdab.com/andodeki/makepad", rev = "a79f0dce4d477e2232344facca0798d3f25043ec"}
nigig-core = { path = "../../nigig-core" }
nigig-pay-domain = { path = "../../nigig-pay-domain" }
# Session correlation (review items 2.7 / 5.3). The registry that refuses an
# out-of-order or foreign USSD callback lives here; before R2.1 it was built
# and never wired, and the pump matched events to "whatever is current".
nigig-pay-platform = { path = "../../nigig-pay-platform" }
nigig-pay-storage = { path = "../../nigig-pay-storage" }
nigig-uikit = { path = "../../nigig-uikit" }
robius-ussd = { path = "../../robius-ussd", features = ["serde"] }
robius-fingerprinting = { path = "../../robius-fingerprinting" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = { version = "0.4", features = ["serde"] }
zeroize = "1"
robius-contacts = { version = "0.1.0", path = "../../robius-contacts" }
robius-file-picker = { git = "https://github.com/project-robius/robius", rev = "b766e62b0600f5d2ee21cc6995648346fc277bd8" }