1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-04 21:15:19 +02:00

Subscription tests and domain (#2209)

* Save work

* Subscriptions and trials domain

* Store family ID on customer

* Remove indirection of stripe calls

* Test simplifications

* Update brakeman

* Fix stripe tests in CI

* Update billing page to show subscription details

* Remove legacy columns

* Complete billing settings page

* Fix hardcoded plan name

* Handle subscriptions for self hosting mode

* Lint fixes
This commit is contained in:
Zach Gollwitzer 2025-05-06 14:05:21 -04:00 committed by GitHub
parent 8c10e87387
commit 5da4bb6dc3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 1041 additions and 233 deletions

View file

@ -3,6 +3,7 @@ require "test_helper"
class OnboardableTest < ActionDispatch::IntegrationTest
setup do
sign_in @user = users(:empty)
@user.family.subscription.destroy
end
test "must complete onboarding before any other action" do
@ -10,32 +11,18 @@ class OnboardableTest < ActionDispatch::IntegrationTest
get root_path
assert_redirected_to onboarding_path
@user.family.update!(trial_started_at: 1.day.ago, stripe_subscription_status: "active")
get root_path
assert_redirected_to onboarding_path
end
test "must subscribe if onboarding complete and no trial or subscription is active" do
test "must have subscription to visit dashboard" do
@user.update!(onboarded_at: 1.day.ago)
@user.family.update!(trial_started_at: nil, stripe_subscription_status: "incomplete")
get root_path
assert_redirected_to upgrade_subscription_path
end
test "onboarded trial user can visit dashboard" do
@user.update!(onboarded_at: 1.day.ago)
@user.family.update!(trial_started_at: 1.day.ago, stripe_subscription_status: "incomplete")
get root_path
assert_response :success
assert_redirected_to trial_onboarding_path
end
test "onboarded subscribed user can visit dashboard" do
@user.update!(onboarded_at: 1.day.ago)
@user.family.update!(stripe_subscription_status: "active")
@user.family.start_trial_subscription!
get root_path
assert_response :success

View file

@ -1,41 +1,78 @@
require "test_helper"
require "ostruct"
class SubscriptionsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in @user = users(:family_admin)
sign_in @user = users(:empty)
@family = @user.family
@mock_stripe = mock
Provider::Registry.stubs(:get_provider).with(:stripe).returns(@mock_stripe)
end
test "can start trial" do
@user.update!(onboarded_at: nil)
@user.family.update!(trial_started_at: nil, stripe_subscription_status: "incomplete")
assert_nil @user.onboarded_at
assert_nil @user.family.trial_started_at
post start_trial_subscription_path
assert_redirected_to root_path
assert_equal "Welcome to Maybe!", flash[:notice]
assert @user.reload.onboarded?
assert @user.family.reload.trial_started_at.present?
end
test "if user re-enters onboarding, don't restart trial" do
onboard_time = 1.day.ago
trial_start_time = 1.day.ago
@user.update!(onboarded_at: onboard_time)
@user.family.update!(trial_started_at: trial_start_time, stripe_subscription_status: "incomplete")
post start_trial_subscription_path
assert_redirected_to root_path
assert @user.reload.family.trial_started_at < Date.current
end
test "redirects to settings if self hosting" do
test "disabled for self hosted users" do
Rails.application.config.app_mode.stubs(:self_hosted?).returns(true)
get subscription_path
post subscription_path
assert_response :forbidden
end
# Trial subscriptions are managed internally and do NOT go through Stripe
test "can create trial subscription" do
@family.subscription.destroy
@family.reload
assert_difference "Subscription.count", 1 do
post subscription_path
end
assert_redirected_to root_path
assert_equal "Welcome to Maybe!", flash[:notice]
assert_equal "trialing", @family.subscription.status
assert_in_delta Subscription::TRIAL_DAYS.days.from_now, @family.subscription.trial_ends_at, 1.minute
end
test "users who have already trialed cannot create a new subscription" do
@family.start_trial_subscription!
assert_no_difference "Subscription.count" do
post subscription_path
end
assert_redirected_to root_path
assert_equal "You have already started or completed a trial. Please upgrade to continue.", flash[:alert]
end
test "creates new checkout session" do
@mock_stripe.expects(:create_checkout_session).with(
plan: "monthly",
family_id: @family.id,
family_email: @family.billing_email,
success_url: success_subscription_url + "?session_id={CHECKOUT_SESSION_ID}",
cancel_url: upgrade_subscription_url
).returns(
OpenStruct.new(
url: "https://checkout.stripe.com/c/pay/test-session-id",
customer_id: "test-customer-id"
)
)
get new_subscription_path(plan: "monthly")
assert_redirected_to "https://checkout.stripe.com/c/pay/test-session-id"
assert_equal "test-customer-id", @family.reload.stripe_customer_id
end
test "creates active subscription on checkout success" do
@mock_stripe.expects(:get_checkout_result).with("test-session-id").returns(
OpenStruct.new(
success?: true,
subscription_id: "test-subscription-id"
)
)
get success_subscription_url(session_id: "test-session-id")
assert @family.subscription.active?
assert_equal "Welcome to Maybe! Your subscription has been created.", flash[:notice]
end
end

View file

@ -1,10 +1,8 @@
empty:
name: Family
stripe_subscription_status: active
last_synced_at: <%= Time.now %>
dylan_family:
name: The Dylan Family
stripe_subscription_status: active
last_synced_at: <%= Time.now %>

9
test/fixtures/subscriptions.yml vendored Normal file
View file

@ -0,0 +1,9 @@
active:
family: dylan_family
status: active
stripe_id: "test_1234567890"
trialing:
family: empty
status: trialing
trial_ends_at: <%= 12.days.from_now %>

View file

@ -5,6 +5,7 @@ empty:
email: user1@email.com
password_digest: $2a$12$XoNBo/cMCyzpYtvhrPAhsubG21mELX48RAcjSVCRctW8dG8wrDIla
onboarded_at: <%= 3.days.ago %>
role: admin
ai_enabled: true
maybe_support_staff:

View file

@ -0,0 +1,13 @@
require "test_helper"
class Family::SubscribeableTest < ActiveSupport::TestCase
setup do
@family = families(:dylan_family)
end
# We keep the status eventually consistent, but don't rely on it for guarding the app
test "trial respects end date even if status is not yet updated" do
@family.subscription.update!(trial_ends_at: 1.day.ago, status: "trialing")
assert_not @family.trialing?
end
end

View file

@ -1,7 +0,0 @@
require "test_helper"
class Provider::Stripe::CustomerEventProcessorTest < ActiveSupport::TestCase
# test "process" do
# end
end

View file

@ -1,6 +1,57 @@
require "test_helper"
require "ostruct"
class Provider::Stripe::SubscriptionEventProcessorTest < ActiveSupport::TestCase
# test "process" do
# end
test "handles subscription event" do
test_customer_id = "test-customer-id"
test_subscription_id = "test-subscription-id"
mock_event = JSON.parse({
type: "customer.subscription.created",
data: {
object: {
id: test_subscription_id,
status: "active",
customer: test_customer_id,
items: {
data: [
{
current_period_end: 1.month.from_now.to_i,
plan: {
interval: "month",
amount: 900,
currency: "usd"
}
}
]
}
}
}
}.to_json, object_class: OpenStruct)
family = Family.create!(
name: "Test Subscribed Family",
stripe_customer_id: test_customer_id
)
family.start_subscription!(test_subscription_id)
processor = Provider::Stripe::SubscriptionEventProcessor.new(mock_event)
assert_equal "active", family.subscription.status
assert_equal test_subscription_id, family.subscription.stripe_id
assert_nil family.subscription.amount
assert_nil family.subscription.currency
assert_nil family.subscription.current_period_ends_at
processor.process
family.reload
assert_equal "active", family.subscription.status
assert_equal test_subscription_id, family.subscription.stripe_id
assert_equal 9, family.subscription.amount
assert_equal "USD", family.subscription.currency
assert family.subscription.current_period_ends_at > 20.days.from_now
end
end

View file

@ -0,0 +1,45 @@
require "test_helper"
class Provider::StripeTest < ActiveSupport::TestCase
setup do
@stripe = Provider::Stripe.new(
secret_key: ENV["STRIPE_SECRET_KEY"] || "foo",
webhook_secret: ENV["STRIPE_WEBHOOK_SECRET"] || "bar"
)
end
test "creates checkout session" do
test_email = "test@example.com"
test_success_url = "http://localhost:3000/subscription/success?session_id={CHECKOUT_SESSION_ID}"
test_cancel_url = "http://localhost:3000/subscription/upgrade"
VCR.use_cassette("stripe/create_checkout_session") do
session = @stripe.create_checkout_session(
plan: "monthly",
family_id: 1,
family_email: test_email,
success_url: test_success_url,
cancel_url: test_cancel_url
)
assert_match /https:\/\/checkout.stripe.com\/c\/pay\/cs_test_.*/, session.url
assert_match /cus_.*/, session.customer_id
end
end
# To re-run VCR for this test:
# 1. Complete a checkout session locally in the UI
# 2. Find the session ID, replace below
# 3. Re-run VCR, make sure ENV vars are in test environment
test "validates checkout session and returns subscription ID" do
test_session_id = "cs_test_b1RD8r6DAkSA8vrQ3grBC2QVgR5zUJ7QQFuVHZkcKoSYaEOQgCMPMOCOM5" # must exist in test Dashboard
VCR.use_cassette("stripe/checkout_session") do
result = @stripe.get_checkout_result(test_session_id)
assert result.success?
assert_match /sub_.*/, result.subscription_id
end
end
end

View file

@ -0,0 +1,33 @@
require "test_helper"
class SubscriptionTest < ActiveSupport::TestCase
setup do
@family = families(:empty)
end
test "can create subscription without stripe details if trial" do
subscription = Subscription.new(
family: @family,
status: :trialing,
)
assert_not subscription.valid?
subscription.trial_ends_at = 14.days.from_now
assert subscription.valid?
end
test "stripe details required for all statuses except trial" do
subscription = Subscription.new(
family: @family,
status: :active,
)
assert_not subscription.valid?
subscription.stripe_id = "test-stripe-id"
assert subscription.valid?
end
end

View file

@ -26,6 +26,8 @@ VCR.configure do |config|
config.filter_sensitive_data("<SYNTH_API_KEY>") { ENV["SYNTH_API_KEY"] }
config.filter_sensitive_data("<OPENAI_ACCESS_TOKEN>") { ENV["OPENAI_ACCESS_TOKEN"] }
config.filter_sensitive_data("<OPENAI_ORGANIZATION_ID>") { ENV["OPENAI_ORGANIZATION_ID"] }
config.filter_sensitive_data("<STRIPE_SECRET_KEY>") { ENV["STRIPE_SECRET_KEY"] }
config.filter_sensitive_data("<STRIPE_WEBHOOK_SECRET>") { ENV["STRIPE_WEBHOOK_SECRET"] }
end
module ActiveSupport

View file

@ -0,0 +1,181 @@
---
http_interactions:
- request:
method: get
uri: https://api.stripe.com/v1/checkout/sessions/cs_test_b1RD8r6DAkSA8vrQ3grBC2QVgR5zUJ7QQFuVHZkcKoSYaEOQgCMPMOCOM5
body:
encoding: US-ASCII
string: ''
headers:
User-Agent:
- Stripe/v1 RubyBindings/15.1.0
Authorization:
- Bearer <STRIPE_SECRET_KEY>
Stripe-Version:
- 2025-04-30.basil
X-Stripe-Client-User-Agent:
- '{"bindings_version":"15.1.0","lang":"ruby","lang_version":"3.4.1 p0 (2024-12-25)","platform":"arm64-darwin24","engine":"ruby","publisher":"stripe","uname":"Darwin
Zachs-MacBook-Pro.local 24.3.0 Darwin Kernel Version 24.3.0: Thu Jan 2 20:24:16
PST 2025; root:xnu-11215.81.4~3/RELEASE_ARM64_T6000 arm64","hostname":"Zachs-MacBook-Pro.local"}'
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
response:
status:
code: 200
message: OK
headers:
Server:
- nginx
Date:
- Mon, 05 May 2025 16:09:23 GMT
Content-Type:
- application/json
Content-Length:
- '2667'
Connection:
- keep-alive
Access-Control-Allow-Credentials:
- 'true'
Access-Control-Allow-Methods:
- GET, HEAD, PUT, PATCH, POST, DELETE
Access-Control-Allow-Origin:
- "*"
Access-Control-Expose-Headers:
- Request-Id, Stripe-Manage-Version, Stripe-Should-Retry, X-Stripe-External-Auth-Required,
X-Stripe-Privileged-Session-Required
Access-Control-Max-Age:
- '300'
Cache-Control:
- no-cache, no-store
Content-Security-Policy:
- base-uri 'none'; default-src 'none'; form-action 'none'; frame-ancestors 'none';
img-src 'self'; script-src 'self' 'report-sample'; style-src 'self'; worker-src
'none'; upgrade-insecure-requests; report-uri https://q.stripe.com/csp-violation?q=7L6NHIm4wk05H5wi0PfH951BH62utb5j2ZImtzEXvcfJgdc1v5juGoNb0oSAXIHhGQtWiGOiCmz3UG1W
Request-Id:
- req_c2n4M98HkgTk63
Stripe-Version:
- 2025-04-30.basil
Vary:
- Origin
X-Stripe-Priority-Routing-Enabled:
- 'true'
X-Stripe-Routing-Context-Priority-Tier:
- api-testmode
X-Wc:
- ABGHI
Strict-Transport-Security:
- max-age=63072000; includeSubDomains; preload
body:
encoding: UTF-8
string: |-
{
"id": "cs_test_b1RD8r6DAkSA8vrQ3grBC2QVgR5zUJ7QQFuVHZkcKoSYaEOQgCMPMOCOM5",
"object": "checkout.session",
"adaptive_pricing": null,
"after_expiration": null,
"allow_promotion_codes": true,
"amount_subtotal": 900,
"amount_total": 900,
"automatic_tax": {
"enabled": false,
"liability": null,
"provider": null,
"status": null
},
"billing_address_collection": null,
"cancel_url": "http://localhost:3000/subscription/upgrade?plan=monthly",
"client_reference_id": null,
"client_secret": null,
"collected_information": {
"shipping_details": null
},
"consent": null,
"consent_collection": null,
"created": 1746281950,
"currency": "usd",
"currency_conversion": null,
"custom_fields": [],
"custom_text": {
"after_submit": null,
"shipping_address": null,
"submit": null,
"terms_of_service_acceptance": null
},
"customer": "cus_SFBH32Bf5lsggB",
"customer_creation": "always",
"customer_details": {
"address": {
"city": null,
"country": "US",
"line1": null,
"line2": null,
"postal_code": "12345",
"state": null
},
"email": "user@maybe.local",
"name": "Test Checkout User",
"phone": null,
"tax_exempt": "none",
"tax_ids": []
},
"customer_email": "user@maybe.local",
"discounts": [],
"expires_at": 1746368350,
"invoice": "in_1RKguoQT2jbOS8G0PuBVklxw",
"invoice_creation": null,
"livemode": false,
"locale": null,
"metadata": {},
"mode": "subscription",
"payment_intent": null,
"payment_link": null,
"payment_method_collection": "always",
"payment_method_configuration_details": {
"id": "pmc_1RJyv5QT2jbOS8G0PDwTVBar",
"parent": null
},
"payment_method_options": {
"card": {
"request_three_d_secure": "automatic"
}
},
"payment_method_types": [
"card",
"link",
"cashapp",
"amazon_pay"
],
"payment_status": "paid",
"permissions": null,
"phone_number_collection": {
"enabled": false
},
"recovered_from": null,
"saved_payment_method_options": {
"allow_redisplay_filters": [
"always"
],
"payment_method_remove": null,
"payment_method_save": null
},
"setup_intent": null,
"shipping_address_collection": null,
"shipping_cost": null,
"shipping_options": [],
"status": "complete",
"submit_type": null,
"subscription": "sub_1RKguoQT2jbOS8G0Zih79ix9",
"success_url": "http://localhost:3000/subscription/success?session_id={CHECKOUT_SESSION_ID}",
"total_details": {
"amount_discount": 0,
"amount_shipping": 0,
"amount_tax": 0
},
"ui_mode": "hosted",
"url": null,
"wallet_options": null
}
recorded_at: Mon, 05 May 2025 16:09:23 GMT
recorded_with: VCR 6.3.1

View file

@ -0,0 +1,298 @@
---
http_interactions:
- request:
method: post
uri: https://api.stripe.com/v1/customers
body:
encoding: UTF-8
string: email=test%40example.com&metadata[family_id]=1
headers:
User-Agent:
- Stripe/v1 RubyBindings/15.1.0
Authorization:
- Bearer <STRIPE_SECRET_KEY>
Idempotency-Key:
- 7e129de1-324e-456a-8bd7-44382f6b4fa7
Stripe-Version:
- 2025-04-30.basil
X-Stripe-Client-User-Agent:
- '{"bindings_version":"15.1.0","lang":"ruby","lang_version":"3.4.1 p0 (2024-12-25)","platform":"arm64-darwin24","engine":"ruby","publisher":"stripe","uname":"Darwin
Zachs-MacBook-Pro.local 24.3.0 Darwin Kernel Version 24.3.0: Thu Jan 2 20:24:16
PST 2025; root:xnu-11215.81.4~3/RELEASE_ARM64_T6000 arm64","hostname":"Zachs-MacBook-Pro.local"}'
Content-Type:
- application/x-www-form-urlencoded
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
response:
status:
code: 200
message: OK
headers:
Server:
- nginx
Date:
- Mon, 05 May 2025 16:09:23 GMT
Content-Type:
- application/json
Content-Length:
- '652'
Connection:
- keep-alive
Access-Control-Allow-Credentials:
- 'true'
Access-Control-Allow-Methods:
- GET, HEAD, PUT, PATCH, POST, DELETE
Access-Control-Allow-Origin:
- "*"
Access-Control-Expose-Headers:
- Request-Id, Stripe-Manage-Version, Stripe-Should-Retry, X-Stripe-External-Auth-Required,
X-Stripe-Privileged-Session-Required
Access-Control-Max-Age:
- '300'
Cache-Control:
- no-cache, no-store
Content-Security-Policy:
- base-uri 'none'; default-src 'none'; form-action 'none'; frame-ancestors 'none';
img-src 'self'; script-src 'self' 'report-sample'; style-src 'self'; worker-src
'none'; upgrade-insecure-requests; report-uri https://q.stripe.com/csp-violation?q=o27rhODPrC4hNe4D-N6JT0tcY2dQoSKNy5hNsSJNdH_SL3lHJ8eS2959Oc-3UnIGMeyex9H3Qo_613Zp
Idempotency-Key:
- 7e129de1-324e-456a-8bd7-44382f6b4fa7
Original-Request:
- req_LIU5SlUlOEkoOg
Request-Id:
- req_LIU5SlUlOEkoOg
Stripe-Should-Retry:
- 'false'
Stripe-Version:
- 2025-04-30.basil
Vary:
- Origin
X-Stripe-Priority-Routing-Enabled:
- 'true'
X-Stripe-Routing-Context-Priority-Tier:
- api-testmode
X-Wc:
- ABGHI
Strict-Transport-Security:
- max-age=63072000; includeSubDomains; preload
body:
encoding: UTF-8
string: |-
{
"id": "cus_SFxVsSZ9enVBNC",
"object": "customer",
"address": null,
"balance": 0,
"created": 1746461363,
"currency": null,
"default_source": null,
"delinquent": false,
"description": null,
"discount": null,
"email": "test@example.com",
"invoice_prefix": "JPHCOASK",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": null,
"footer": null,
"rendering_options": null
},
"livemode": false,
"metadata": {
"family_id": "1"
},
"name": null,
"next_invoice_sequence": 1,
"phone": null,
"preferred_locales": [],
"shipping": null,
"tax_exempt": "none",
"test_clock": null
}
recorded_at: Mon, 05 May 2025 16:09:23 GMT
- request:
method: post
uri: https://api.stripe.com/v1/checkout/sessions
body:
encoding: UTF-8
string: customer=cus_SFxVsSZ9enVBNC&line_items[0][price]=price_1RJz6KQT2jbOS8G0Otv3qD01&line_items[0][quantity]=1&mode=subscription&allow_promotion_codes=true&success_url=http%3A%2F%2Flocalhost%3A3000%2Fsubscription%2Fsuccess%3Fsession_id%3D%7BCHECKOUT_SESSION_ID%7D&cancel_url=http%3A%2F%2Flocalhost%3A3000%2Fsubscription%2Fupgrade
headers:
User-Agent:
- Stripe/v1 RubyBindings/15.1.0
Authorization:
- Bearer <STRIPE_SECRET_KEY>
X-Stripe-Client-Telemetry:
- '{"last_request_metrics":{"request_id":"req_LIU5SlUlOEkoOg","request_duration_ms":307}}'
Idempotency-Key:
- d62353a3-199a-468d-9f78-a4fb10fa28bd
Stripe-Version:
- 2025-04-30.basil
X-Stripe-Client-User-Agent:
- '{"bindings_version":"15.1.0","lang":"ruby","lang_version":"3.4.1 p0 (2024-12-25)","platform":"arm64-darwin24","engine":"ruby","publisher":"stripe","uname":"Darwin
Zachs-MacBook-Pro.local 24.3.0 Darwin Kernel Version 24.3.0: Thu Jan 2 20:24:16
PST 2025; root:xnu-11215.81.4~3/RELEASE_ARM64_T6000 arm64","hostname":"Zachs-MacBook-Pro.local"}'
Content-Type:
- application/x-www-form-urlencoded
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
response:
status:
code: 200
message: OK
headers:
Server:
- nginx
Date:
- Mon, 05 May 2025 16:09:24 GMT
Content-Type:
- application/json
Content-Length:
- '2850'
Connection:
- keep-alive
Access-Control-Allow-Credentials:
- 'true'
Access-Control-Allow-Methods:
- GET, HEAD, PUT, PATCH, POST, DELETE
Access-Control-Allow-Origin:
- "*"
Access-Control-Expose-Headers:
- Request-Id, Stripe-Manage-Version, Stripe-Should-Retry, X-Stripe-External-Auth-Required,
X-Stripe-Privileged-Session-Required
Access-Control-Max-Age:
- '300'
Cache-Control:
- no-cache, no-store
Content-Security-Policy:
- base-uri 'none'; default-src 'none'; form-action 'none'; frame-ancestors 'none';
img-src 'self'; script-src 'self' 'report-sample'; style-src 'self'; worker-src
'none'; upgrade-insecure-requests; report-uri https://q.stripe.com/csp-violation?q=7L6NHIm4wk05H5wi0PfH951BH62utb5j2ZImtzEXvcfJgdc1v5juGoNb0oSAXIHhGQtWiGOiCmz3UG1W
Idempotency-Key:
- d62353a3-199a-468d-9f78-a4fb10fa28bd
Original-Request:
- req_8AIKuqTzBWcO76
Request-Id:
- req_8AIKuqTzBWcO76
Stripe-Should-Retry:
- 'false'
Stripe-Version:
- 2025-04-30.basil
Vary:
- Origin
X-Stripe-Priority-Routing-Enabled:
- 'true'
X-Stripe-Routing-Context-Priority-Tier:
- api-testmode
X-Wc:
- ABGHI
Strict-Transport-Security:
- max-age=63072000; includeSubDomains; preload
body:
encoding: UTF-8
string: |-
{
"id": "cs_test_b1lPPmtTEFw5w9GpyzQYlxz2TAN4JeUTosGjyXzfjkx9ocP59UhcF0WgWf",
"object": "checkout.session",
"adaptive_pricing": null,
"after_expiration": null,
"allow_promotion_codes": true,
"amount_subtotal": 900,
"amount_total": 900,
"automatic_tax": {
"enabled": false,
"liability": null,
"provider": null,
"status": null
},
"billing_address_collection": null,
"cancel_url": "http://localhost:3000/subscription/upgrade",
"client_reference_id": null,
"client_secret": null,
"collected_information": {
"shipping_details": null
},
"consent": null,
"consent_collection": null,
"created": 1746461364,
"currency": "usd",
"currency_conversion": null,
"custom_fields": [],
"custom_text": {
"after_submit": null,
"shipping_address": null,
"submit": null,
"terms_of_service_acceptance": null
},
"customer": "cus_SFxVsSZ9enVBNC",
"customer_creation": null,
"customer_details": {
"address": null,
"email": "test@example.com",
"name": null,
"phone": null,
"tax_exempt": "none",
"tax_ids": null
},
"customer_email": null,
"discounts": [],
"expires_at": 1746547763,
"invoice": null,
"invoice_creation": null,
"livemode": false,
"locale": null,
"metadata": {},
"mode": "subscription",
"payment_intent": null,
"payment_link": null,
"payment_method_collection": "always",
"payment_method_configuration_details": {
"id": "pmc_1RJyv5QT2jbOS8G0PDwTVBar",
"parent": null
},
"payment_method_options": {
"card": {
"request_three_d_secure": "automatic"
}
},
"payment_method_types": [
"card",
"link",
"cashapp",
"amazon_pay"
],
"payment_status": "unpaid",
"permissions": null,
"phone_number_collection": {
"enabled": false
},
"recovered_from": null,
"saved_payment_method_options": {
"allow_redisplay_filters": [
"always"
],
"payment_method_remove": null,
"payment_method_save": null
},
"setup_intent": null,
"shipping_address_collection": null,
"shipping_cost": null,
"shipping_options": [],
"status": "open",
"submit_type": null,
"subscription": null,
"success_url": "http://localhost:3000/subscription/success?session_id={CHECKOUT_SESSION_ID}",
"total_details": {
"amount_discount": 0,
"amount_shipping": 0,
"amount_tax": 0
},
"ui_mode": "hosted",
"url": "https://checkout.stripe.com/c/pay/cs_test_b1lPPmtTEFw5w9GpyzQYlxz2TAN4JeUTosGjyXzfjkx9ocP59UhcF0WgWf#fid2cGd2ZndsdXFsamtQa2x0cGBrYHZ2QGtkZ2lgYSc%2FY2RpdmApJ2R1bE5gfCc%2FJ3VuWnFgdnFaMDRXT3xwYVRRN29nSlY9QjUzNUttakJibmcxXH11Z2M8R3UzYUh3dGtNNlA8M3ExMTRKd0Z3M2p%2FdXdkcTNHcmg8NWA1YUx%2FbTRdTEM1cXI8TXNIT0FTRkY1NU5RME9EcjBpJyknY3dqaFZgd3Ngdyc%2FcXdwYCknaWR8anBxUXx1YCc%2FJ2hwaXFsWmxxYGgnKSdga2RnaWBVaWRmYG1qaWFgd3YnP3F3cGB4JSUl",
"wallet_options": null
}
recorded_at: Mon, 05 May 2025 16:09:24 GMT
recorded_with: VCR 6.3.1