From 20c6d61a3f03719676c0430c6a36d2bb7591477f Mon Sep 17 00:00:00 2001 From: Vincent Pochet Date: Thu, 12 Sep 2024 18:29:55 +0200 Subject: [PATCH] fix(subscription): Fix generating invoice with upgrade and timezone --- app/services/subscriptions/dates_service.rb | 9 +++-- spec/models/invoice_spec.rb | 4 +- .../subscriptions/terminate_ended_spec.rb | 6 +-- .../dates/monthly_service_spec.rb | 39 ++++++++++++++----- .../dates/quarterly_service_spec.rb | 6 +-- .../dates/weekly_service_spec.rb | 6 +-- .../dates/yearly_service_spec.rb | 6 +-- 7 files changed, 50 insertions(+), 26 deletions(-) diff --git a/app/services/subscriptions/dates_service.rb b/app/services/subscriptions/dates_service.rb index 9a270d9f82f..4a1fcef2543 100644 --- a/app/services/subscriptions/dates_service.rb +++ b/app/services/subscriptions/dates_service.rb @@ -64,13 +64,13 @@ def to_datetime return @to_datetime if @to_datetime @to_datetime = customer_timezone_shift(compute_to_date, end_of_day: true) - terminated_at = subscription.terminated_at&.change(usec: 0) - bill_at = billing_at&.change(usec: 0) + terminated_at = subscription.terminated_at&.to_time&.round - if subscription.terminated? && @to_datetime > terminated_at && bill_at && bill_at >= terminated_at + if subscription.terminated_at?(billing_at) && @to_datetime > terminated_at @to_datetime = terminated_at end + @to_datetime = subscription.started_at if @to_datetime < subscription.started_at @to_datetime end @@ -96,6 +96,7 @@ def charges_from_datetime def charges_to_datetime datetime = customer_timezone_shift(compute_charges_to_date, end_of_day: true) datetime = subscription.terminated_at if subscription.terminated_at?(datetime) + datetime = subscription.started_at if datetime < subscription.started_at datetime end @@ -151,6 +152,8 @@ def subscription_at subscription.subscription_at.in_time_zone(customer.applicable_timezone) end + # NOTE: This method converts a DAY epress in the customer timezone into a proper UTC datetime + # Example: `2024-03-01` in `America/New_York` will be converted to `2024-03-01T05:00:00 UTC` def customer_timezone_shift(date, end_of_day: false) result = date.in_time_zone(customer.applicable_timezone) result = result.end_of_day if end_of_day diff --git a/spec/models/invoice_spec.rb b/spec/models/invoice_spec.rb index f892910380c..7361421eef2 100644 --- a/spec/models/invoice_spec.rb +++ b/spec/models/invoice_spec.rb @@ -633,9 +633,9 @@ end describe '#charge_pay_in_advance_proration_range' do - let(:invoice_subscription) { create(:invoice_subscription) } + let(:invoice_subscription) { create(:invoice_subscription, subscription:) } let(:invoice) { invoice_subscription.invoice } - let(:subscription) { invoice_subscription.subscription } + let(:subscription) { create(:subscription, started_at: timestamp - 1.year) } let(:timestamp) { DateTime.parse('2023-07-25 00:00:00 UTC') } let(:event) { create(:event, subscription_id: subscription.id, timestamp:) } let(:billable_metric) { create(:sum_billable_metric, organization: subscription.organization, recurring: true) } diff --git a/spec/scenarios/subscriptions/terminate_ended_spec.rb b/spec/scenarios/subscriptions/terminate_ended_spec.rb index af4bf826137..ebab708e74b 100644 --- a/spec/scenarios/subscriptions/terminate_ended_spec.rb +++ b/spec/scenarios/subscriptions/terminate_ended_spec.rb @@ -18,9 +18,9 @@ ) end - let(:creation_time) { DateTime.new(2023, 9, 5, 0, 0) } - let(:subscription_at) { DateTime.new(2023, 9, 5, 0, 0) } - let(:ending_at) { DateTime.new(2023, 9, 6, 0, 0) } + let(:creation_time) { Time.zone.parse('2023-09-05T00:00:00') } + let(:subscription_at) { Time.zone.parse('2023-09-05T00:00:00') } + let(:ending_at) { Time.zone.parse('2023-09-06T00:00:00') } context 'when timezone is Europe/Paris' do it 'terminates the subscription when it reaches its ending date' do diff --git a/spec/services/subscriptions/dates/monthly_service_spec.rb b/spec/services/subscriptions/dates/monthly_service_spec.rb index c4071d2972c..4ce6d117ec4 100644 --- a/spec/services/subscriptions/dates/monthly_service_spec.rb +++ b/spec/services/subscriptions/dates/monthly_service_spec.rb @@ -205,23 +205,44 @@ context 'when subscription is just terminated' do let(:billing_at) { Time.zone.parse('10 Mar 2022') } + let(:terminated_at) { Time.zone.parse('09 Mar 2022') } - before do - subscription.update!( - status: :terminated, - terminated_at: Time.zone.parse('02 Mar 2022') - ) - end + before { subscription.update!(status: :terminated, terminated_at:) } it 'returns the termination date' do - expect(result).to eq(subscription.terminated_at.utc.to_s) + expect(result).to match_datetime(subscription.terminated_at.utc) + end + + context 'with pending next subscription' do + let(:subscription_at) { Time.zone.parse('2024-09-09T14:00:01') } + let(:terminated_at) { Time.zone.parse('2024-09-09T16:00:01') } + + let(:downgraded_plan) { create(:plan, interval: :monthly, pay_in_advance: false, amount_cents: 0) } + + before do + create( + :subscription, + status: :pending, + external_id: subscription.external_id, + plan: downgraded_plan, + customer:, + subscription_at:, + billing_time:, + started_at: nil, + previous_subscription: subscription + ) + end + + it 'makes sure the to_datetime is not before start date' do + expect(result).to match_datetime(subscription.started_at.utc) + end end context 'with customer timezone' do let(:timezone) { 'America/New_York' } it 'returns the termination date' do - expect(result).to eq(subscription.terminated_at.utc.to_s) + expect(result).to match_datetime(subscription.terminated_at.utc) end end end @@ -288,7 +309,7 @@ end it 'returns the termination date' do - expect(result).to eq(subscription.terminated_at.utc.to_s) + expect(result).to match_datetime(subscription.terminated_at.utc) end end end diff --git a/spec/services/subscriptions/dates/quarterly_service_spec.rb b/spec/services/subscriptions/dates/quarterly_service_spec.rb index 17c4653dba0..eb5383d69f4 100644 --- a/spec/services/subscriptions/dates/quarterly_service_spec.rb +++ b/spec/services/subscriptions/dates/quarterly_service_spec.rb @@ -222,14 +222,14 @@ end it 'returns the termination date' do - expect(result).to eq(subscription.terminated_at.utc.to_s) + expect(result).to match_datetime(subscription.terminated_at.utc) end context 'with customer timezone' do let(:timezone) { 'America/New_York' } it 'returns the termination date' do - expect(result).to eq(subscription.terminated_at.utc.to_s) + expect(result).to match_datetime(subscription.terminated_at.utc) end end end @@ -295,7 +295,7 @@ end it 'returns the termination date' do - expect(result).to eq(subscription.terminated_at.utc.to_s) + expect(result).to match_datetime(subscription.terminated_at.utc) end end end diff --git a/spec/services/subscriptions/dates/weekly_service_spec.rb b/spec/services/subscriptions/dates/weekly_service_spec.rb index 5589b6c2841..1b25f0e80ad 100644 --- a/spec/services/subscriptions/dates/weekly_service_spec.rb +++ b/spec/services/subscriptions/dates/weekly_service_spec.rb @@ -159,14 +159,14 @@ end it 'returns the termination date' do - expect(result).to eq(subscription.terminated_at.utc.to_s) + expect(result).to match_datetime(subscription.terminated_at.utc) end context 'with customer timezone' do let(:timezone) { 'America/New_York' } it 'returns the termination date' do - expect(result).to eq(subscription.terminated_at.utc.to_s) + expect(result).to match_datetime(subscription.terminated_at.utc) end end end @@ -197,7 +197,7 @@ end it 'returns the termination date' do - expect(result).to eq(subscription.terminated_at.utc.to_s) + expect(result).to match_datetime(subscription.terminated_at.utc) end end end diff --git a/spec/services/subscriptions/dates/yearly_service_spec.rb b/spec/services/subscriptions/dates/yearly_service_spec.rb index 9e19452edfb..f1f223001a3 100644 --- a/spec/services/subscriptions/dates/yearly_service_spec.rb +++ b/spec/services/subscriptions/dates/yearly_service_spec.rb @@ -181,14 +181,14 @@ end it 'returns the termination date' do - expect(result).to eq(subscription.terminated_at.utc.to_s) + expect(result).to match_datetime(subscription.terminated_at.utc) end context 'with customer timezone' do let(:timezone) { 'America/New_York' } it 'returns the termination date' do - expect(result).to eq(subscription.terminated_at.utc.to_s) + expect(result).to match_datetime(subscription.terminated_at.utc) end end end @@ -246,7 +246,7 @@ end it 'returns the termination date' do - expect(result).to eq(subscription.terminated_at.utc.to_s) + expect(result).to match_datetime(subscription.terminated_at.utc) end end end