open-standards/openpay/crates
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Transaction Science 53d57dc5c2
All checks were successful
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 18s
standards workspaces build + test / macOS energy backends (pull_request) Has been skipped
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 3m36s
standards workspaces build + test / openpay (openpay, openpay, linux-arm64) (pull_request) Successful in 5m57s
op-3ds2: an unconverted foreign amount is not under an EUR SCA cap
`low_value_eligible` compared `intent.amount.minor_units` against the
RTS Article 16 caps of EUR 30 / EUR 100 whatever currency the intent
was denominated in, under this comment:

    // RTS 16 thresholds. We treat all non-EUR currencies conservatively
    // by checking the minor-unit value against EUR limits assuming a
    // 1:1 EUR equivalency; production deployments convert via op-fx.

The substitution is not conservative. It is conservative only for
currencies worth *less* than EUR per minor unit. A GBP 30.00 payment
(minor_units 3000, about EUR 35) and a CHF 30.00 payment (about
EUR 32) each satisfy `3000 <= 3000`, so `evaluate()` pushed
`LowValueTransaction` and the orchestrator authorised with no SCA
challenge above the Article 16 ceiling. Five such payments run to
about EUR 175 under a cap documented as EUR 100. The FX conversion was
not computed, and the uncomputed result was treated as a value inside
the cap — while `op_core::Money` carries the currency, so the
information needed to refuse was present and discarded. The second
clause compounded it by adding a possibly-non-EUR figure to
`cumulative_low_value_24h_eur_minor`, a field documented "in EUR minor
units", so the 24 h cap was summed across mixed units.

The same hole sat in the TRA branch, which the sweep that found this
did not report: `FraudRateBracket::tra_cap_minor_units` is documented
"the TRA cap (EUR minor units)" and was compared to the raw
minor-unit count too, at EUR 100 / 250 / 500. GBP 450.00 is about
EUR 527 and passed the UpTo1Bp cap of EUR 500.

Neither was visible because every fixture in the file is built by an
`eur()` helper, so no test ever presented a non-EUR amount.

The fix makes the unknown unusable rather than permissive:

- `ExemptionContext::eur_equivalent_minor: Option<i64>` carries the
  amount converted to EUR minor units, supplied by the caller that
  holds the rate (the comment already pointed at `op-fx`, which is not
  even a dependency of this crate).
- `amount_in_eur_minor()` returns the EUR figure for an EUR amount,
  the supplied conversion for anything else, and `None` when there is
  none. Both EUR-denominated caps now take that `Option` and decline
  when it is absent: an unconverted figure is not a figure below a cap.
- The cumulative clause adds two EUR figures rather than two different
  units, and saturates. The total is caller-supplied, and a plain `+`
  overflowing i64 wraps negative in release and so slips under the
  cap, while panicking in debug.

`ExemptionContext` derives `Default` and every construction site in
the repo uses `..Default::default()`, so the new field is additive and
defaults to `None` — the direction that withholds the exemption.

Tests cover both caps on both sides: an unconverted GBP 30.00 is not
low-value, the same intent with a supplied EUR 35.00 is not and with
EUR 25.00 is, an EUR amount still needs no conversion, an unconverted
GBP 450.00 is not under the TRA cap while EUR 460.00 is, and a 24 h
total of i64::MAX does not slip under EUR 100.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-17 04:27:27 -04:00
..
op-3ds2 op-3ds2: an unconverted foreign amount is not under an EUR SCA cap 2026-10-17 04:27:27 -04:00
op-batch Ingest batch 3: 9 more crates across openpay / smart-byte / eoc 2026-05-24 01:09:35 -04:00
op-bin ingest wave E: 3 crates · op-bin + smart-byte-tendermint + eoc-safety 2026-05-24 16:52:07 -04:00
op-bnpl chore(deps): update dependencies for Rust 1.98, pinning the crypto generations 2026-08-26 12:19:50 -04:00
op-cli Parallel ingest: 9 issues' worth of work across openpay / smart-byte / eoc 2026-05-23 13:24:05 -04:00
op-connect openpay: a PEP match is recorded as a PEP match, not an OFAC sanctions hit 2026-10-15 20:47:42 -04:00
op-core Parallel ingest: 9 issues' worth of work across openpay / smart-byte / eoc 2026-05-23 13:24:05 -04:00
op-crypto-extra ingest wave F: 3 crates · op-crypto-extra + smart-byte-acdc + eoc-memory 2026-05-24 17:28:24 -04:00
op-dispute Initial import — three open standards under one shelf 2026-05-23 07:58:09 -04:00
op-dispute-evidence ingest wave A2: 3 crates · op-dispute-evidence + smart-byte-edv + eoc-agent 2026-05-24 11:42:26 -04:00
op-driver-sdk Parallel ingest: 9 issues' worth of work across openpay / smart-byte / eoc 2026-05-23 13:24:05 -04:00
op-emv Initial import — three open standards under one shelf 2026-05-23 07:58:09 -04:00
op-ewallets-asia chore(deps): update dependencies for Rust 1.98, pinning the crypto generations 2026-08-26 12:19:50 -04:00
op-ffi-jni Initial import — three open standards under one shelf 2026-05-23 07:58:09 -04:00
op-ffi-swift Initial import — three open standards under one shelf 2026-05-23 07:58:09 -04:00
op-fraud Repo-wide toolchain refresh + jouleclaw policy/sandbox updates 2026-06-07 17:04:45 -04:00
op-fraud-graph Repo-wide toolchain refresh + jouleclaw policy/sandbox updates 2026-06-07 17:04:45 -04:00
op-fx Initial import — three open standards under one shelf 2026-05-23 07:58:09 -04:00
op-graph Initial import — three open standards under one shelf 2026-05-23 07:58:09 -04:00
op-iso8583 Ingest batch 2: 9 new crates across openpay / smart-byte / eoc 2026-05-23 18:12:24 -04:00
op-iso20022 Initial import — three open standards under one shelf 2026-05-23 07:58:09 -04:00
op-ledger Initial import — three open standards under one shelf 2026-05-23 07:58:09 -04:00
op-mobile-wallets chore(deps): update dependencies for Rust 1.98, pinning the crypto generations 2026-08-26 12:19:50 -04:00
op-openbanking ingest wave A4: 3 crates · smart-byte-atproto + op-openbanking + eoc-distributed 2026-05-24 12:22:09 -04:00
op-orchestrator Repo-wide toolchain refresh + jouleclaw policy/sandbox updates 2026-06-07 17:04:45 -04:00
op-payouts ingest wave B: 3 crates · op-payouts + smart-byte-nostr + eoc-streaming 2026-05-24 13:39:09 -04:00
op-rails-a2a Initial import — three open standards under one shelf 2026-05-23 07:58:09 -04:00
op-rails-card Parallel ingest: 9 issues' worth of work across openpay / smart-byte / eoc 2026-05-23 13:24:05 -04:00
op-rails-crypto Initial import — three open standards under one shelf 2026-05-23 07:58:09 -04:00
op-rails-x402 op-rails-x402: x402 agentic-payment rail (HTTP 402 acceptance) 2026-06-25 15:43:20 -04:00
op-reconciliation Initial import — three open standards under one shelf 2026-05-23 07:58:09 -04:00
op-refund Initial import — three open standards under one shelf 2026-05-23 07:58:09 -04:00
op-revrec ingest batch 5 (salvage): 2 crates from 9-agent dispatch (session-limit hit mid-batch) 2026-05-24 09:28:38 -04:00
op-routing Ingest batch 2: 9 new crates across openpay / smart-byte / eoc 2026-05-23 18:12:24 -04:00
op-screening openpay: a PEP match is recorded as a PEP match, not an OFAC sanctions hit 2026-10-15 20:47:42 -04:00
op-server Initial import — three open standards under one shelf 2026-05-23 07:58:09 -04:00
op-settlement Initial import — three open standards under one shelf 2026-05-23 07:58:09 -04:00
op-statements ingest wave C: 3 crates · op-statements + smart-byte-activitypub + eoc-observability 2026-05-24 13:51:09 -04:00
op-subscriptions Initial import — three open standards under one shelf 2026-05-23 07:58:09 -04:00
op-tax chore(deps): update dependencies for Rust 1.98, pinning the crypto generations 2026-08-26 12:19:50 -04:00
op-vault op-vault: test that a token is independent of the PAN, not free of "4242" 2026-09-30 23:07:55 -04:00
op-wasm Initial import — three open standards under one shelf 2026-05-23 07:58:09 -04:00
op-webhook fix(op-webhook): import KeyInit — hmac 0.13 moved new_from_slice off the Mac trait, breaking compute_signature 2026-08-26 09:28:40 -04:00