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 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 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 ───────────────────────────────────