From 1024a163323d9e60892462d64ce414ed9afb9b2a Mon Sep 17 00:00:00 2001 From: Nguyen Phung Date: Fri, 25 Jul 2025 13:43:25 +0700 Subject: [PATCH 1/5] Support stripe 15 --- stripe-ruby-mock.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stripe-ruby-mock.gemspec b/stripe-ruby-mock.gemspec index b212ec912..73ed9d200 100644 --- a/stripe-ruby-mock.gemspec +++ b/stripe-ruby-mock.gemspec @@ -22,7 +22,7 @@ Gem::Specification.new do |gem| gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) gem.require_paths = ['lib'] - gem.add_dependency 'stripe', '> 5', '< 14' + gem.add_dependency 'stripe', '> 5', '< 16' gem.add_dependency 'multi_json', '~> 1.0' gem.add_dependency 'dante', '>= 0.2.0' gem.add_dependency 'drb', '>= 2.0.4', '< 3' From c5b32dc575afa49ba2cf3849d4e4fd1e998da0b8 Mon Sep 17 00:00:00 2001 From: Don Nguyen Date: Thu, 13 Nov 2025 08:23:33 +0700 Subject: [PATCH 2/5] Add `ostruct` gem to gemspec to silence warning with ruby 3.5 --- stripe-ruby-mock.gemspec | 1 + 1 file changed, 1 insertion(+) diff --git a/stripe-ruby-mock.gemspec b/stripe-ruby-mock.gemspec index 73ed9d200..3375443e0 100644 --- a/stripe-ruby-mock.gemspec +++ b/stripe-ruby-mock.gemspec @@ -26,6 +26,7 @@ Gem::Specification.new do |gem| gem.add_dependency 'multi_json', '~> 1.0' gem.add_dependency 'dante', '>= 0.2.0' gem.add_dependency 'drb', '>= 2.0.4', '< 3' + gem.add_dependency 'ostruct', '~> 0.6.3' gem.add_development_dependency 'rspec', '~> 3.13.0' gem.add_development_dependency 'thin', '~> 1.8.1' From 89d5d131d07bae74c55b334474fc2bdcce43a07d Mon Sep 17 00:00:00 2001 From: Don Nguyen Date: Thu, 13 Nov 2025 17:51:11 +0700 Subject: [PATCH 3/5] Add cancel_at field handling to subscription cancellation Set cancel_at timestamp when cancel_at_period_end is enabled and clear it when disabled or canceling at period end. This aligns with Stripe API v15 subscription cancellation behavior. --- lib/stripe_mock/request_handlers/subscriptions.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/stripe_mock/request_handlers/subscriptions.rb b/lib/stripe_mock/request_handlers/subscriptions.rb index aae0f01af..82aad2547 100644 --- a/lib/stripe_mock/request_handlers/subscriptions.rb +++ b/lib/stripe_mock/request_handlers/subscriptions.rb @@ -298,9 +298,11 @@ def update_subscription(route, method_url, params, headers) if params[:cancel_at_period_end] subscription[:cancel_at_period_end] = true subscription[:canceled_at] = Time.now.utc.to_i + subscription[:cancel_at] = Time.now.utc.to_i elsif params.has_key?(:cancel_at_period_end) subscription[:cancel_at_period_end] = false subscription[:canceled_at] = nil + subscription[:cancel_at] = nil end params[:current_period_start] = subscription[:current_period_start] @@ -333,6 +335,7 @@ def cancel_subscription(route, method_url, params, headers) cancelled_at_period_end = (params[:at_period_end] == true) if cancelled_at_period_end cancel_params[:cancel_at_period_end] = true + cancel_params[:cancel_at] = nil else cancel_params[:status] = 'canceled' cancel_params[:cancel_at_period_end] = false From cf9b7c51f15c8150e8f6768541d87e444c91b8f2 Mon Sep 17 00:00:00 2001 From: Don Nguyen Date: Thu, 13 Nov 2025 19:32:03 +0700 Subject: [PATCH 4/5] Add `current_period_start` and `current_period_end` to sub_items --- lib/stripe_mock/data.rb | 4 ++- .../helpers/subscription_helpers.rb | 28 ++++++++++++--- .../completing_checkout_sessions_example.rb | 11 ++++++ .../subscription_items_examples.rb | 35 +++++++++++++++++++ 4 files changed, 72 insertions(+), 6 deletions(-) diff --git a/lib/stripe_mock/data.rb b/lib/stripe_mock/data.rb index 551a3fb46..10322fa98 100644 --- a/lib/stripe_mock/data.rb +++ b/lib/stripe_mock/data.rb @@ -1269,7 +1269,9 @@ def self.mock_subscription_item(params = {}) trial_period_days: nil }, quantity: 2, - price: mock_price + price: mock_price, + current_period_start: nil, + current_period_end: nil }.merge(params) end diff --git a/lib/stripe_mock/request_handlers/helpers/subscription_helpers.rb b/lib/stripe_mock/request_handlers/helpers/subscription_helpers.rb index 520592718..455a8b313 100644 --- a/lib/stripe_mock/request_handlers/helpers/subscription_helpers.rb +++ b/lib/stripe_mock/request_handlers/helpers/subscription_helpers.rb @@ -10,16 +10,32 @@ def resolve_subscription_changes(subscription, plans, customer, options = {}) subscription.merge!(custom_subscription_params(plans, customer, options)) items = options[:items] items = items.values if items.respond_to?(:values) + + # Calculate period fields for subscription items + # These should match the subscription's current_period_start and current_period_end + start_time = subscription[:current_period_start] + end_time = subscription[:current_period_end] + subscription[:items][:data] = plans.map do |plan| matching_item = items && items.detect { |item| [item[:price], item[:plan]].include? plan[:id] } + if matching_item matching_item[:quantity] ||= 1 matching_item[:id] ||= new_id('si') - params = matching_item.merge(plan: plan) + params = matching_item.merge( + plan: plan, + current_period_start: start_time, + current_period_end: end_time + ) params[:price] = plan if plan[:object] == "price" Data.mock_subscription_item(params) else - params = { plan: plan, id: new_id('si') } + params = { + plan: plan, + id: new_id('si'), + current_period_start: start_time, + current_period_end: end_time + } params[:price] = plan if plan[:object] == "price" Data.mock_subscription_item(params) end @@ -30,6 +46,8 @@ def resolve_subscription_changes(subscription, plans, customer, options = {}) def custom_subscription_params(plans, cus, options = {}) verify_trial_end(options[:trial_end]) if options[:trial_end] + # Use first plan for period calculation, but only set :plan attribute if single item + plan_for_period = plans.first unless plans.empty? plan = plans.first if plans.size == 1 now = Time.now.utc.to_i @@ -59,11 +77,11 @@ def custom_subscription_params(plans, cus, options = {}) # TODO: Implement coupon logic - if (((plan && plan[:trial_period_days]) || 0) == 0 && options[:trial_end].nil?) || options[:trial_end] == "now" - end_time = options[:billing_cycle_anchor] || get_ending_time(start_time, plan) + if (((plan_for_period && plan_for_period[:trial_period_days]) || 0) == 0 && options[:trial_end].nil?) || options[:trial_end] == "now" + end_time = options[:billing_cycle_anchor] || get_ending_time(start_time, plan_for_period) params.merge!({status: 'active', current_period_end: end_time, trial_start: nil, trial_end: nil, billing_cycle_anchor: options[:billing_cycle_anchor] || created_time}) else - end_time = options[:trial_end] || (Time.now.utc.to_i + plan[:trial_period_days]*86400) + end_time = options[:trial_end] || (Time.now.utc.to_i + plan_for_period[:trial_period_days]*86400) params.merge!({status: 'trialing', current_period_end: end_time, trial_start: start_time, trial_end: end_time, billing_cycle_anchor: options[:billing_cycle_anchor] || created_time}) end diff --git a/spec/integration_examples/completing_checkout_sessions_example.rb b/spec/integration_examples/completing_checkout_sessions_example.rb index 615bc26ba..50764528e 100644 --- a/spec/integration_examples/completing_checkout_sessions_example.rb +++ b/spec/integration_examples/completing_checkout_sessions_example.rb @@ -33,5 +33,16 @@ subscription = test_helper.complete_checkout_session(session, payment_method) expect(subscription.default_payment_method).to eq(payment_method.id) + expect(subscription.current_period_start).to_not be_nil + expect(subscription.current_period_end).to_not be_nil + expect(subscription.current_period_end).to be > subscription.current_period_start + + # Check subscription items also have period fields + subscription.items.data.each do |item| + expect(item.current_period_start).to_not be_nil + expect(item.current_period_end).to_not be_nil + expect(item.current_period_start).to eq(subscription.current_period_start) + expect(item.current_period_end).to eq(subscription.current_period_end) + end end end diff --git a/spec/shared_stripe_examples/subscription_items_examples.rb b/spec/shared_stripe_examples/subscription_items_examples.rb index 524577fec..c5d623cc4 100644 --- a/spec/shared_stripe_examples/subscription_items_examples.rb +++ b/spec/shared_stripe_examples/subscription_items_examples.rb @@ -73,4 +73,39 @@ } end end + + context 'subscription items have current_period fields' do + it 'items have current_period_start and current_period_end matching subscription' do + sub = Stripe::Subscription.create(customer: customer.id, items: [{ plan: plan.id }]) + + expect(sub.items.data.length).to eq(1) + item = sub.items.data.first + + expect(item.current_period_start).to_not be_nil + expect(item.current_period_end).to_not be_nil + expect(item.current_period_start).to eq(sub.current_period_start) + expect(item.current_period_end).to eq(sub.current_period_end) + expect(item.current_period_end).to be > item.current_period_start + end + + it 'multi-item subscriptions have matching period fields on all items' do + sub = Stripe::Subscription.create( + customer: customer.id, + items: [ + { plan: plan.id, quantity: 2 }, + { plan: plan2.id, quantity: 1 } + ] + ) + + expect(sub.items.data.length).to eq(2) + + sub.items.data.each do |item| + expect(item.current_period_start).to_not be_nil + expect(item.current_period_end).to_not be_nil + expect(item.current_period_start).to eq(sub.current_period_start) + expect(item.current_period_end).to eq(sub.current_period_end) + expect(item.current_period_end).to be > item.current_period_start + end + end + end end From 768b63403daafd840ba474cde0ad27d668421d1e Mon Sep 17 00:00:00 2001 From: Nguyen Phung Date: Tue, 25 Nov 2025 09:37:40 +0700 Subject: [PATCH 5/5] Support stripe 18 --- stripe-ruby-mock.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stripe-ruby-mock.gemspec b/stripe-ruby-mock.gemspec index 3375443e0..1102a78a9 100644 --- a/stripe-ruby-mock.gemspec +++ b/stripe-ruby-mock.gemspec @@ -22,7 +22,7 @@ Gem::Specification.new do |gem| gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) gem.require_paths = ['lib'] - gem.add_dependency 'stripe', '> 5', '< 16' + gem.add_dependency 'stripe', '> 5', '< 19' gem.add_dependency 'multi_json', '~> 1.0' gem.add_dependency 'dante', '>= 0.2.0' gem.add_dependency 'drb', '>= 2.0.4', '< 3'