Skip to content

ORB-202: request 3D Secure for subscription checkout - #780

Merged
thomasluizon merged 2 commits into
mainfrom
thomasluizon/ticket-208-stripe-3ds
Oct 19, 2026
Merged

thomasluizon merged 2 commits into
mainfrom
thomasluizon/ticket-208-stripe-3ds

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

Summary

Refs thomasluizon/orbit-tickets#208 (ORB-202).

StripeBillingService.CreateCheckoutSessionAsync now sets PaymentMethodOptions.Card.RequestThreeDSecure to any using a named constant. This requests 3DS for every supported card in a new subscription Checkout Session rather than only elevated-risk cards. The policy follows the work order's stronger default, with a preference for frictionless authentication. The issuer controls the final flow; this does not guarantee a visible challenge or authentication for unsupported cards.

The Stripe Checkout Session creation reference states: "3DS will always be attempted if it is supported for the card". It documents any as preferring a frictionless flow, challenge as preferring active authentication, and automatic as the risk-based default. Rechecked at execution time. Stripe also recommends its SCA Engine default; the blanket request is the explicit policy chosen in the work order.

The orchestrator's supplied acceptance probe created a test-mode Checkout Session with mode=subscription, an existing recurring test price, and payment_method_options[card][request_three_d_secure]=any. Stripe accepted it and returned this reported fragment:

{"livemode":false,"payment_method_options":{"card":{"request_three_d_secure":"any"}}}

That session was then expired. This probe was provided by the orchestrator, not rerun by this worker, and proves parameter acceptance rather than completed payment or Orbit activation.

The orchestrator selected this session-option path because there is no authenticated Stripe Dashboard session. Dashboard Radar configuration was not observed or changed. The session policy is versioned in code and reviewable. Only src/Orbit.Infrastructure/Services/StripeBillingService.cs and its existing unit test file change. The test captures the actual options passed to the SDK and checks subscription mode, customer, price, quantity, metadata, return URLs, discounts and promotion-code handling for null, empty and referral coupons. OpenAPI regeneration produced no changes. No DTO or client changes.

Recurring renewals do not enter this Checkout Session creation path. Google Play Billing uses a separate rail. No renewal or Play code changes are included; observing the existing subscriber's next renewal remains a rollout check.

The extra bank step can cost card conversions when customers fail or abandon authentication. This covers the Stripe web flow only; Android purchases through Google Play Billing remain outside this policy. This is not complete payment-fraud protection and does not prevent legitimate cardholders from disputing purchases.

External interface evidence

The installed Stripe.net 52.4.2 package XML confirms SessionCreateOptions.PaymentMethodOptions, SessionPaymentMethodOptionsOptions.Card and SessionPaymentMethodOptionsCardOptions.RequestThreeDSecure, including any as an allowed value. The installed NuGet manifest pins source commit f4f1c8ce704825d7d7f00b4c9d848a2d028757eb. Its corresponding source confirms the exact property types and serialized field names:

Re-derive the evidence by reading the installed package's stripe.net.nuspec and lib/net9.0/Stripe.net.xml, then fetching those three source files at its repository commit. The new assertion concerns an SDK request option, not an invented response fixture.

Test evidence

  • Before changing either implementation or tests: dotnet test tests/Orbit.Infrastructure.Tests --filter FullyQualifiedName~StripeBillingServiceTests exited 0, with all 38 existing tests passing while the 3DS option was absent.
  • Added Checkout_RequestsThreeDSecureWithoutChangingSubscriptionOptions with the implementation still unchanged: dotnet test tests/Orbit.Infrastructure.Tests --filter FullyQualifiedName~Checkout_RequestsThreeDSecureWithoutChangingSubscriptionOptions exited 1. All three cases failed because PaymentMethodOptions was null instead of containing the card's any request.
  • After adding the option: dotnet test tests/Orbit.Infrastructure.Tests --filter FullyQualifiedName~StripeBillingServiceTests exited 0, all 41 tests passed, including the three new cases.
  • dotnet build Orbit.slnx exited 0 with zero errors. Existing warnings remain. Both OpenAPI documents regenerated without a diff.
  • dotnet test tests/Orbit.Domain.Tests exited 0, all 750 tests passed.
  • dotnet test tests/Orbit.Application.Tests exited 0, all 5,573 tests passed.
  • dotnet test tests/Orbit.Infrastructure.Tests exited 0, all 3,815 tests passed.

Manual steps

  1. Release the API to staging through GitHub Actions > Release API (.github/workflows/release.yml), dispatched from main, with environment=staging and branch=thomasluizon/ticket-208-stripe-3ds. Use the staging web checkout with Stripe test credentials and the published 3DS-required card 4000000000003220. Observe the hosted challenge, complete it, inspect the resulting Stripe test subscription for active status, and read GET /api/subscriptions/status for the same Orbit user to confirm the paid plan and interval. Repeat with a fresh checkout and fail or abandon authentication: verify no paid Orbit subscription and no successful charge, and a usable return path. These browser checks were not run by this worker, as prohibited by the work order.
  2. Also test supported optional-3DS card 4000000000003055 to confirm the explicit request applies beyond cards already requiring 3DS. Do not expect 4242424242424242 to display a challenge: Stripe documents it as unenrolled. With any, frictionless authentication is allowed.
  3. Production effect starts only after a production API release, with the owner's explicit approval. After staging verification and merge, use GitHub Actions > Release API with environment=production and branch=main. Verify the release commit, observe the next real checkouts, and read the existing subscriber's next recurring invoice/subscription state to confirm renewal succeeds. No live release, renewal or payment was performed here. A report that a customer cannot pay is a rollback trigger.
  4. Reversal: remove PaymentMethodOptions.Card.RequestThreeDSecure or set it to automatic in StripeBillingService.CreateCheckoutSessionAsync, then release the API through the same workflow. Confirm newly created sessions use the default or automatic policy. No Dashboard rule needs changing.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Checkout retries spanning deployment can fail because the new request parameters reuse the previous policy's Stripe idempotency keys.

Reviewed changes Reviewed the subscription Checkout 3DS policy and its regression coverage.

  • Checkout authentication: Sets PaymentMethodOptions.Card.RequestThreeDSecure to any, requesting supported-card authentication with a frictionless preference.
  • Regression coverage: Verifies the complete outgoing Checkout options for absent, empty, and referral coupons; all 41 StripeBillingServiceTests passed locally.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

A checkout request with the new card authentication option must not reuse
an idempotency key that a request without it created, because Stripe
rejects changed parameters for a retained key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes Reviewed 7622b588 since the prior Pullfrog review and confirmed the resolved finding is addressed.

  • Separated checkout policies: Included the 3DS policy in checkout idempotency key derivation, preventing reuse of pre-policy keys while preserving deterministic retries and leaving portal keys unchanged.
  • Added regression coverage: Verified separation from pre-policy keys with and without referral coupons. All 43 StripeBillingServiceTests passed locally.

Pullfrog  | View workflow run | Using gpt-6.1-sol | 𝕏

@sonarqubecloud

Copy link
Copy Markdown

@thomasluizon
thomasluizon merged commit 46e3aac into main Oct 19, 2026
22 checks passed
@thomasluizon
thomasluizon deleted the thomasluizon/ticket-208-stripe-3ds branch October 19, 2026 12:26
thomasluizon added a commit that referenced this pull request Oct 19, 2026
…nd accountability removal (#746) (#785)

* ORB-202: request 3D Secure for subscription checkout (#780)

* fix(billing): request 3D Secure for subscription checkout

* fix(billing): key checkout retries by the 3D Secure policy

A checkout request with the new card authentication option must not reuse
an idempotency key that a request without it created, because Stripe
rejects changed parameters for a retained key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 46e3aac)

* Remove challenge and accountability endpoints (ORB-212) (#779)

* feat: remove challenge and accountability endpoints (ORB-212)

* fix: exclude removed achievements from earned totals (ORB-212)

(cherry picked from commit c75ab1b)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.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.

1 participant