Skip to content

Commit

Permalink
feat: Bill anniversary subscriptions
Browse files Browse the repository at this point in the history
  • Loading branch information
vincent-pochet committed Aug 5, 2022
1 parent 5bcab2d commit 2dd7928
Show file tree
Hide file tree
Showing 2 changed files with 250 additions and 67 deletions.
128 changes: 101 additions & 27 deletions app/services/billing_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,41 +22,115 @@ def call

private

# Retrieve list of subscription that should be billed today
def today
@today ||= Time.zone.now
end

# NOTE: Retrieve list of subscription that should be billed today
def billable_subscriptions
sql = []
today = Time.zone.now

return Subscription.none unless (today.day == 1 || today.monday?)
# NOTE: Calendar subscriptions

# For weekly interval we send invoices on Monday
if today.monday?
sql << Subscription.active.joins(:plan)
.merge(Plan.weekly)
.select(:id).to_sql
end
# NOTE: For weekly interval we send invoices on Monday
sql << weekly_calendar if today.monday?

if today.day == 1
# Billed monthly
sql << Subscription.active.joins(:plan)
.merge(Plan.monthly)
.select(:id).to_sql

# Bill charges monthly for yearly plans
sql << Subscription.active.joins(:plan)
.merge(Plan.yearly)
.merge(Plan.where(bill_charges_monthly: true))
.select(:id).to_sql

# We are on the first day of the year
if today.month == 1
# Billed yearly
sql << Subscription.active.joins(:plan)
.merge(Plan.yearly)
.select(:id).to_sql
end
# NOTE: Billed monthly
sql << monthly_calendar

# NOTE: Bill charges monthly for yearly plans
sql << yearly_with_monthly_charges_calendar

# NOTE: Billed yearly and we are on the first day of the year
sql << yearly_calendar if today.month == 1
end

# NOTE: Anniversary subscriptions
sql << weekly_anniversary
sql << monthly_anniversary
sql << yearly_with_monthly_charges_anniversary
sql << yearly_anniversary

Subscription.where("id in (#{sql.join(' UNION ')})")
end

def weekly_calendar
Subscription.active.joins(:plan)
.calendar
.merge(Plan.weekly)
.select(:id).to_sql
end

def monthly_calendar
Subscription.active.joins(:plan)
.calendar
.merge(Plan.monthly)
.select(:id).to_sql
end

def yearly_with_monthly_charges_calendar
Subscription.active.joins(:plan)
.calendar
.merge(Plan.yearly.where(bill_charges_monthly: true))
.select(:id).to_sql
end

def yearly_calendar
Subscription.active.joins(:plan)
.calendar
.merge(Plan.yearly)
.select(:id).to_sql
end

def weekly_anniversary
Subscription.active.joins(:plan)
.anniversary
.merge(Plan.weekly)
.where('EXTRACT(ISODOW FROM subscriptions.subscription_date) = ?', today.wday)
.select(:id).to_sql
end

def monthly_anniversary
days = [today.day]

# If today is the last day of the month and month count less than 31 days,
# we need to take all days up to 31 into account
((today.day + 1)..31).each { |day| days << day } if today.day == today.end_of_month.day

Subscription.active.joins(:plan)
.anniversary
.merge(Plan.monthly)
.where('DATE_PART(\'day\', subscriptions.subscription_date) IN (?)', days)
.select(:id).to_sql
end

def yearly_anniversary
# Billed yearly
days = [today.day]

# If we are not in leap year and we are on 28/02 take 29/02 into account
days << 29 if !Date.leap?(today.year) && today.day == 28 && today.month == 2

Subscription.active.joins(:plan)
.anniversary
.merge(Plan.yearly)
.where('DATE_PART(\'month\', subscriptions.subscription_date) = ?', today.month)
.where('DATE_PART(\'day\', subscriptions.subscription_date) IN (?)', days)
.select(:id).to_sql
end

def yearly_with_monthly_charges_anniversary
days = [today.day]

# If today is the last day of the month and month count less than 31 days,
# we need to take all days up to 31 into account
((today.day + 1)..31).each { |day| days << day } if today.day == today.end_of_month.day

Subscription.active.joins(:plan)
.anniversary
.merge(Plan.yearly.where(bill_charges_monthly: true))
.where('DATE_PART(\'day\', subscriptions.subscription_date) IN (?)', days)
.select(:id).to_sql
end
end
189 changes: 149 additions & 40 deletions spec/services/billing_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,25 @@
subject(:billing_service) { described_class.new }

describe '.call' do
let(:start_date) { DateTime.parse('20 Feb 2021') }
let(:plan) { create(:plan, interval: interval, bill_charges_monthly: bill_charges_monthly) }
let(:bill_charges_monthly) { false }
let(:subscription_date) { DateTime.parse('20 Feb 2021') }

let(:subscription) do
create(
:subscription,
plan: plan,
subscription_date: subscription_date,
started_at: Time.zone.now,
billing_time: billing_time,
)
end

context 'when billed weekly' do
let(:plan) { create(:plan, interval: :weekly) }
before { subscription }

let(:subscription) do
create(
:subscription,
plan: plan,
subscription_date: start_date,
started_at: Time.zone.now,
)
end
context 'when billed weekly with calendar billing time' do
let(:interval) { :weekly }
let(:billing_time) { :calendar }

before { subscription }

Expand All @@ -42,19 +48,9 @@
end
end

context 'when billed monthly' do
let(:plan) { create(:plan, interval: :monthly) }

let(:subscription) do
create(
:subscription,
plan: plan,
subscription_date: start_date,
started_at: Time.zone.now,
)
end

before { subscription }
context 'when billed monthly with calendar billing time' do
let(:interval) { :monthly }
let(:billing_time) { :calendar }

it 'enqueue a job on billing day' do
current_date = DateTime.parse('01 Feb 2022')
Expand All @@ -76,19 +72,9 @@
end
end

context 'when billed yearly' do
let(:plan) { create(:plan, interval: :yearly) }

let(:subscription) do
create(
:subscription,
plan: plan,
subscription_date: start_date,
started_at: Time.zone.now,
)
end

before { subscription }
context 'when billed yearly with calendar billing time' do
let(:interval) { :yearly }
let(:billing_time) { :calendar }

it 'enqueue a job on billing day' do
current_date = DateTime.parse('01 Jan 2022')
Expand All @@ -110,7 +96,7 @@
end

context 'when charges are billed monthly' do
before { plan.update(bill_charges_monthly: true) }
let(:bill_charges_monthly) { true }

it 'enqueues a job on billing day' do
current_date = DateTime.parse('01 Feb 2022')
Expand All @@ -125,11 +111,134 @@
end
end

context 'when billed weekly with anniversary billing time' do
let(:interval) { :weekly }
let(:billing_time) { :anniversary }

let(:subscription_date) { DateTime.now.prev_occurring(DateTime.now.strftime('%A').downcase.to_sym) }

let(:current_date) { DateTime.parse('20 Jun 2022').prev_occurring(subscription_date.strftime('%A').downcase.to_sym) }

it 'enqueue a job on billing day' do
travel_to(current_date) do
billing_service.call

expect(BillSubscriptionJob).to have_been_enqueued
.with(subscription, current_date.to_i)
end
end

it 'does not enqueue a job on other day' do
travel_to(current_date + 1.day) do
expect { billing_service.call }.not_to have_enqueued_job
end
end
end

context 'when billed monthly with anniversary billing time' do
let(:interval) { :monthly }
let(:billing_time) { :anniversary }
let(:current_date) { subscription_date.next_month }

it 'enqueue a job on billing day' do
travel_to(current_date) do
billing_service.call

expect(BillSubscriptionJob).to have_been_enqueued
.with(subscription, current_date.to_i)
end
end

it 'does not enqueue a job on other day' do
travel_to(current_date + 1.day) do
expect { billing_service.call }.not_to have_enqueued_job
end
end

context 'when subscription anniversary is on a 31st' do
let(:subscription_date) { DateTime.parse('31 Mar 2021') }
let(:current_date) { DateTime.parse('28 Feb 2022') }

it 'enqueue a job if the month count less than 31 days' do
travel_to(current_date) do
billing_service.call

expect(BillSubscriptionJob).to have_been_enqueued
.with(subscription, current_date.to_i)
end
end
end
end

context 'when billed yearly with anniversary billing time' do
let(:interval) { :yearly }
let(:billing_time) { :anniversary }

let(:current_date) { subscription_date.next_year }

it 'enqueue a job on billing day' do
travel_to(current_date) do
billing_service.call

expect(BillSubscriptionJob).to have_been_enqueued
.with(subscription, current_date.to_i)
end
end

it 'does not enqueue a job on other day' do
travel_to(current_date + 1.day) do
expect { billing_service.call }.not_to have_enqueued_job
end
end

context 'when subscription anniversary is on 29th of february' do
let(:subscription_date) { DateTime.parse('29 Feb 2020') }
let(:current_date) { DateTime.parse('28 Feb 2022') }

it 'enqueue a job on 28th of february when year is not a leap year' do
travel_to(current_date) do
billing_service.call

expect(BillSubscriptionJob).to have_been_enqueued
.with(subscription, current_date.to_i)
end
end
end

context 'when charges are billed monthly' do
let(:bill_charges_monthly) { true }
let(:current_date) { subscription_date.next_month }

it 'enqueues a job on billing day' do
travel_to(current_date.next_month) do
billing_service.call

expect(BillSubscriptionJob).to have_been_enqueued
.with(subscription, current_date.next_month.to_i)
end
end

context 'when subscription anniversary is on a 31st' do
let(:subscription_date) { DateTime.parse('31 Mar 2021') }
let(:current_date) { DateTime.parse('28 Feb 2022') }

it 'enqueue a job if the month count less than 31 days' do
travel_to(current_date) do
billing_service.call

expect(BillSubscriptionJob).to have_been_enqueued
.with(subscription, current_date.to_i)
end
end
end
end
end

context 'when downgraded' do
let(:subscription) do
create(
:subscription,
subscription_date: start_date,
subscription_date: subscription_date,
started_at: Time.zone.now,
previous_subscription: previous_subscription,
status: :pending,
Expand All @@ -139,7 +248,7 @@
let(:previous_subscription) do
create(
:subscription,
subscription_date: start_date,
subscription_date: subscription_date,
started_at: Time.zone.now,
)
end
Expand Down

0 comments on commit 2dd7928

Please sign in to comment.