Skip to content

Commit

Permalink
feat(tests): Test dates service with tz change during grace period (#…
Browse files Browse the repository at this point in the history
…2016)

I tried to reproduce a bug but couldn't. I believe this test is useful
tho.
  • Loading branch information
julienbourdeau authored May 20, 2024
1 parent d9ff1a8 commit 9b716ad
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 9 deletions.
2 changes: 1 addition & 1 deletion app/services/subscriptions/dates_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def charges_from_datetime

# NOTE: If customer applicable timezone changes during a billing period, there is a risk to double count events
# or to miss some. To prevent it, we have to ensure that invoice bounds does not overlap or that there is no
# hole bewtween a charges_from_datetime and the charges_to_datetime of the previous period
# hole between a charges_from_datetime and the charges_to_datetime of the previous period
if timezone_has_changed? && previous_charge_to_datetime
new_datetime = previous_charge_to_datetime + 1.second

Expand Down
84 changes: 76 additions & 8 deletions spec/services/subscriptions/dates/monthly_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

let(:previous_subscription) { nil }

describe 'from_datetime' do
describe '#from_datetime' do
let(:result) { date_service.from_datetime.to_s }

context 'when billing_time is calendar' do
Expand Down Expand Up @@ -174,7 +174,7 @@
end
end

describe 'to_datetime' do
describe '#to_datetime' do
let(:result) { date_service.to_datetime.to_s }

context 'when billing_time is calendar' do
Expand Down Expand Up @@ -292,7 +292,7 @@
end
end

describe 'charges_from_datetime' do
describe '#charges_from_datetime' do
let(:result) { date_service.charges_from_datetime.to_s }

context 'when billing_time is calendar' do
Expand Down Expand Up @@ -405,7 +405,7 @@
end
end

describe 'charges_to_datetime' do
describe '#charges_to_datetime' do
let(:result) { date_service.charges_to_datetime.to_s }

context 'when billing_time is calendar' do
Expand Down Expand Up @@ -497,7 +497,7 @@
end
end

describe 'next_end_of_period' do
describe '#next_end_of_period' do
let(:result) { date_service.next_end_of_period.to_s }

context 'when billing_time is calendar' do
Expand Down Expand Up @@ -547,7 +547,7 @@
end
end

describe 'previous_beginning_of_period' do
describe '#previous_beginning_of_period' do
let(:result) { date_service.previous_beginning_of_period(current_period:).to_s }

let(:current_period) { false }
Expand Down Expand Up @@ -601,7 +601,7 @@
end
end

describe 'single_day_price' do
describe '#single_day_price' do
let(:result) { date_service.single_day_price }

context 'when billing_time is calendar' do
Expand Down Expand Up @@ -647,7 +647,7 @@
end
end

describe 'charges_duration_in_days' do
describe '#charges_duration_in_days' do
let(:result) { date_service.charges_duration_in_days }

context 'when billing_time is calendar' do
Expand Down Expand Up @@ -684,4 +684,72 @@
end
end
end

# In February 2022, customer changed timezone from Asia/Tokyo to America/Los_Angeles.
# The invoice generated in February 2022 had subscription from Feb 1st to Feb 28th
# and usage-based charges from Jan 1st to Jan 31st.
# The dates are correct but *they are in customer timezone*: "2022-01-31 23:59:59 Asia/Tokyo" is "2022-01-31 14:59:59 UTC"
# In Feb, if we use "2022-02-01 00:00:00 America/Los_Angeles", which is "2022-02-01 07:00:00 UTC" we're now missing
# all events between "2022-01-31 14:59:59 UTC" and "2022-02-01 07:00:00 UTC", which is a 16h gap.
#
# We need to use the previous invoice charges_to_datetime as the new invoice charges_from_datetime.
#
#
context 'when customer changed timezone' do
let(:organization) { create(:organization, invoice_grace_period: 3) }
let(:customer) { create(:customer, timezone:, organization:) }
let(:plan) { create(:plan, interval: :monthly, pay_in_advance:, organization:) }

let(:billing_time) { :calendar }
let(:timezone) { 'Asia/Tokyo' }
let(:new_timezone) { 'America/Los_Angeles' }

# Clock::SubscriptionsBillerJob will find this subscription as soon as it's March in the customer timezone
let(:billing_at) { Time.new(2022, 3, 1, 0, 10, 0, Time.new(2022, 3, 1, 0, 10, 0).in_time_zone(new_timezone).formatted_offset) }
let(:previous_invoice_charges_to_datetime) { Time.new(2022, 1, 31, 23, 59, 59, Time.new(2022, 1, 31, 23, 59, 59).in_time_zone(timezone).formatted_offset) }

let(:previous_invoice_subscription) do
create(
:invoice_subscription,
subscription:,
charges_to_datetime: previous_invoice_charges_to_datetime,
invoice: create(:invoice, timezone: timezone),
)
end

before do
previous_invoice_subscription
end

it 'takes previous invoice charges_to_datetime into account and compute correct following month' do
expect(previous_invoice_subscription.charges_to_datetime).to be_utc
expect(date_service.send(:timezone_has_changed?)).to be_falsey

result = Invoices::SubscriptionService.call(
subscriptions: [subscription],
timestamp: billing_at.to_i,
invoicing_reason: 'subscription_periodic',
invoice: nil,
skip_charges: false,
)

expect(result.invoice.status).to eq 'draft'
invoice_sub = result.invoice.invoice_subscriptions.first
aggregate_failures do
expect(invoice_sub.charges_from_datetime.to_s).to match_datetime("2022-01-31 15:00:00 UTC")
expect(invoice_sub.charges_to_datetime.to_s).to match_datetime("2022-02-28 14:59:59 UTC")
end

subscription.customer.update!(timezone: new_timezone)

finalized_result = Invoices::FinalizeService.call(invoice: result.invoice.reload)
invoice_sub = finalized_result.invoice.reload.invoice_subscriptions.first

aggregate_failures do
expect(invoice_sub.charges_from_datetime.to_s).to match_datetime("2022-01-31 15:00:00 UTC")
# "2022-02-28 23:59:59 America/Los_Angeles" which is "2022-03-01 07:59:59 UTC"
expect(invoice_sub.charges_to_datetime.to_s).to match_datetime("2022-03-01 07:59:59 UTC")
end
end
end
end

0 comments on commit 9b716ad

Please sign in to comment.