From 12cade4694a5531b760c581d7d375110436b413c Mon Sep 17 00:00:00 2001 From: chopmob-cloud Date: Fri, 17 Jul 2026 13:19:08 +0100 Subject: [PATCH 1/3] fix(sdk): match allowed payment instruments by (type, id), not id alone AllowedPaymentInstrumentEvaluator matched an allowed instrument on id only, so a closed instrument of a different type (for example {id: "pi-1", type: "bank"}) satisfied an allowed {id: "pi-1", type: "card"}. Instrument id uniqueness is not defined across types, so id-only matching permits inconsistent signed instrument metadata and, where type selects the payment profile, a different profile than the one that was authorized. Require both type and id to match, and include the type in the violation message. Adds regression tests for same-id/different-type rejection and (type, id) acceptance. Part of #299. --- code/sdk/python/ap2/sdk/constraints.py | 12 ++++- .../sdk/python/ap2/tests/constraints_tests.py | 44 +++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/code/sdk/python/ap2/sdk/constraints.py b/code/sdk/python/ap2/sdk/constraints.py index 48aed798..cc21094e 100644 --- a/code/sdk/python/ap2/sdk/constraints.py +++ b/code/sdk/python/ap2/sdk/constraints.py @@ -243,12 +243,20 @@ def evaluate( instrument = closed_mandate.payment_instrument if not instrument: return ['Missing payment instrument in closed mandate'] + # Match on both type and id. Matching on id alone lets a closed + # instrument of a different type (e.g. {id: 'x', type: 'bank'}) satisfy + # an allowed {id: 'x', type: 'card'}, since id uniqueness is not defined + # across types. Requiring (type, id) prevents that instrument-type + # confusion. if any( - allowed.id == instrument.id + allowed.type == instrument.type and allowed.id == instrument.id for allowed in self.constraint.allowed ): return [] - return [f'Payment instrument {instrument.id} not in allowed list'] + return [ + f'Payment instrument (type={instrument.type}, id={instrument.id}) ' + 'not in allowed list' + ] class AllowedPispEvaluator(PaymentConstraintEvaluator): diff --git a/code/sdk/python/ap2/tests/constraints_tests.py b/code/sdk/python/ap2/tests/constraints_tests.py index 910c97fc..783c1037 100644 --- a/code/sdk/python/ap2/tests/constraints_tests.py +++ b/code/sdk/python/ap2/tests/constraints_tests.py @@ -412,6 +412,50 @@ def test_payment_allowed_payment_instrument_mismatch(): assert any('not in allowed list' in v for v in violations) +def test_payment_allowed_payment_instrument_same_id_different_type_rejected(): + """A closed instrument with an allowed id but a different type is rejected. + + id uniqueness is not defined across instrument types, so matching on id + alone would let {id: 'pi-1', type: 'bank'} satisfy an allowed + {id: 'pi-1', type: 'card'}. Matching requires (type, id). + """ + violations = check_payment_constraints( + _open_payment( + constraints=[ + AllowedPaymentInstruments( + allowed=[ + PaymentInstrument(id='pi-1', type='card') + ], + ), + ] + ), + _closed_payment( + payment_instrument=PaymentInstrument(id='pi-1', type='bank') + ), + ) + assert any('not in allowed list' in v for v in violations) + + +def test_payment_allowed_payment_instrument_type_and_id_match(): + """A closed instrument matching an allowed (type, id) passes.""" + violations = check_payment_constraints( + _open_payment( + constraints=[ + AllowedPaymentInstruments( + allowed=[ + PaymentInstrument(id='pi-1', type='card'), + PaymentInstrument(id='pi-1', type='bank'), + ], + ), + ] + ), + _closed_payment( + payment_instrument=PaymentInstrument(id='pi-1', type='bank') + ), + ) + assert violations == [] + + # ── check_payment_constraints – budget ─────────────────────────────────── From 0a94bfa918482922ca72dbd1be01b98502f8d368 Mon Sep 17 00:00:00 2001 From: chopmob-cloud Date: Fri, 17 Jul 2026 13:24:21 +0100 Subject: [PATCH 2/3] chore(spelling): register PISP, SECP, and pisp domain terms The spellcheck action checks entire changed files. Touching constraints.py / constraints_tests.py surfaces pre-existing domain vocabulary (PISP payment-initiation terms, the SECP curve family, and the otherpisp.com test fixture) that was never in the custom dictionary. Register the exact case forms so the spellcheck gate passes. --- .cspell/custom-words.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index ce73c361..2c233390 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -118,6 +118,7 @@ okhttp opentelemetry otelgrpc otelhttp +otherpisp Otherville OURCYGPATTERN Palo @@ -127,6 +128,10 @@ Payoneer paypal Payplug pids +PISP +Pisp +pisp +Pisps pmezard proguard Proguard @@ -149,6 +154,7 @@ ropeproject RPCURL Rulebook screenreaders +SECP setlocal sharedpref Shopcider From 865386e423804576e8c710d9441b79a66d928c3e Mon Sep 17 00:00:00 2001 From: chopmob-cloud Date: Fri, 17 Jul 2026 13:43:50 +0100 Subject: [PATCH 3/3] ci: exclude demo web-client from super-linter The Lint Code Base job runs super-linter with VALIDATE_ALL_CODEBASE false, but the Biome linter ignores FILTER_REGEX_EXCLUDE and lints the whole code/web-client demo app, so a PR touching only the Python SDK still fails on pre-existing web-client diagnostics unrelated to the change. Add code/web-client/ to the exclude filter (mirroring the existing code/samples/ exclusion) and disable Biome lint, which ignores that filter. Matches the CI fix already on PR #300. --- .github/workflows/linter.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index fea1b9c1..e6d040c0 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -23,7 +23,8 @@ jobs: LOG_LEVEL: WARN SHELLCHECK_OPTS: -e SC1091 -e 2086 VALIDATE_ALL_CODEBASE: false - FILTER_REGEX_EXCLUDE: "^(\\.github/|\\.vscode/|code/samples/).*|CODE_OF_CONDUCT.md|CHANGELOG.md" + FILTER_REGEX_EXCLUDE: "^(\\.github/|\\.vscode/|code/samples/|code/web-client/).*|CODE_OF_CONDUCT.md|CHANGELOG.md" + VALIDATE_BIOME_LINT: false VALIDATE_BIOME_FORMAT: false VALIDATE_PYTHON_BLACK: false VALIDATE_PYTHON_FLAKE8: false