Skip to content

fix(checkout): verify settled payment before completing checkout#119

Open
valter-silva-au wants to merge 1 commit into
NVIDIA-AI-Blueprints:mainfrom
valter-silva-au:fix/retail-agentic-commerce-payment-verification-gate
Open

fix(checkout): verify settled payment before completing checkout#119
valter-silva-au wants to merge 1 commit into
NVIDIA-AI-Blueprints:mainfrom
valter-silva-au:fix/retail-agentic-commerce-payment-verification-gate

Conversation

@valter-silva-au

Copy link
Copy Markdown

Summary

complete_checkout_session created an order after only a readiness check (line items, buyer, and fulfillment present) and ignored payment_data entirely — the parameter was annotated # noqa: ARG001 # Reserved for payment validation. A client could complete a checkout with an arbitrary token and receive a confirmed order with no settled payment, releasing the paid resource before any confirmed transfer.

Fixes #112.

The invariant this enforces

An order is now created only when a settled payment is bound to the session. Before releasing the order, verify_payment_for_session requires a PaymentIntent that is:

  1. Settledstatus == COMPLETED;
  2. Reached via the presented vault token — looked up from payment_data.token;
  3. Bound to this session — the vault token's allowance.checkout_session_id matches the session being completed (a payment settled for another session cannot be replayed here);
  4. Sufficientamount >= the server-computed session total (read from totals_json, never a caller-supplied amount — this addresses the issue's "PSP allowance is client supplied and not bound to merchant total" point);
  5. Same currency as the session.

If any clause fails, the merchant returns 402 Payment Required with code invalid_payment and no order is created.

How the merchant sees the payment

The PSP and merchant share the same database (see src/payment/config.py — "Database (shared with merchant service)"). Importing the PSP models into the checkout service also registers their tables on the shared SQLModel metadata, so the merchant reads the settled payment_intent / vault_token state in-process — no cross-service HTTP call. This is a verify-only gate: it confirms a completed payment exists; it does not itself initiate payment (that remains the PSP's create_and_process_payment_intent).

Ordering / backwards-compatibility

The payment check runs after the existing readiness check, so prior behaviour is preserved: not-found → 404, not-ready → 405, already-completed → 405. The new failure mode is 402 for a ready session lacking a verified payment. Callers that previously completed a checkout without settling a payment will now correctly receive 402 instead of a free order — an intended behaviour change.

Tests

  • New TestCompleteCheckoutPaymentVerification covers the bypass and every binding clause: no settled payment, underpayment, payment bound to another session, currency mismatch, pending (not completed) intent → all 402; and the happy path (settled, sufficient, matching) → 200 + order.
  • Existing happy-path completion tests (ACP and UCP) updated to mint a real settled payment first.
  • Full suite green (395 passed), ruff check, ruff format --check, and pyright (strict) all clean — verified on Linux/Python 3.12.

DCO: commit is signed off.

complete_checkout_session created an order after only a readiness check
(line items, buyer, fulfillment present) and ignored payment_data
entirely. A client could complete checkout with an arbitrary token and
receive a confirmed order with no settled payment (issue NVIDIA-AI-Blueprints#112).

Gate order creation on a settled payment that is bound to the session.
Before releasing the order, verify_payment_for_session requires a
COMPLETED PaymentIntent reached via the presented vault token, whose
allowance is bound to this checkout session, that covers the
server-computed total (never a caller-supplied amount), in the session
currency. Otherwise the merchant responds 402 Payment Required
(invalid_payment) and no order is created.

The merchant reads the PSP's settled state from the shared database;
importing the PSP models also registers their tables on the shared
SQLModel metadata. The verification runs after the readiness check, so
not-ready (405), not-found (404), and already-completed (405) behaviour
is unchanged.

Fixes NVIDIA-AI-Blueprints#112

Signed-off-by: Valter Silva <valter.silva.au@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Potential payment verification issue in paid resource gate

1 participant