Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add pay in advance logic into date service #385

Merged
merged 4 commits into from
Aug 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions app/services/subscriptions/dates/monthly_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,28 @@ module Subscriptions
module Dates
class MonthlyService < Subscriptions::DatesService
def compute_from_date(date = base_date)
if terminated_pay_in_arrear?
if plan.pay_in_advance? || terminated_pay_in_arrear?
return subscription.anniversary? ? previous_anniversary_day(billing_date) : billing_date.beginning_of_month
end

subscription.anniversary? ? previous_anniversary_day(date) : date.beginning_of_month
end

def compute_charges_from_date
return from_date if plan.pay_in_arrear?
return base_date.beginning_of_month if calendar?

previous_anniversary_day(base_date)
end

def compute_charges_to_date
return to_date if plan.pay_in_arrear?

# NOTE: In pay in advance scenario, from_date will be the begining of the new period.
# To get the end of the previous one, we just have to take the day before
from_date - 1.day # TODO: check with upgrade on subscription day
end

private

def compute_base_date
Expand All @@ -32,10 +47,6 @@ def compute_to_date
build_date(year, month, day)
end

def compute_charges_from_date
from_date
end

def compute_next_end_of_period(date)
return date.end_of_month if calendar?

Expand All @@ -56,6 +67,12 @@ def compute_next_end_of_period(date)
build_date(year, month, day) - 1.day
end

def compute_previous_beginning_of_period(date)
return date.beginning_of_month if calendar?

previous_anniversary_day(date)
end

def previous_anniversary_day(date)
year = nil
month = nil
Expand All @@ -71,6 +88,13 @@ def previous_anniversary_day(date)

build_date(year, month, day)
end

def compute_duration(from_date:)
return Time.days_in_month(from_date.month, from_date.year) if calendar?

next_month_date = compute_to_date
(next_month_date.to_date + 1.day - from_date.to_date).to_i
end
end
end
end
26 changes: 24 additions & 2 deletions app/services/subscriptions/dates/weekly_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
module Subscriptions
module Dates
class WeeklyService < Subscriptions::DatesService
WEEK_DURATION = 7

private

def compute_base_date
billing_date - 1.week
end

def compute_from_date
if terminated_pay_in_arrear?
if plan.pay_in_advance? || terminated_pay_in_arrear?
return subscription.anniversary? ? previous_anniversary_day(billing_date) : billing_date.beginning_of_week
end

Expand All @@ -24,7 +26,17 @@ def compute_to_date
end

def compute_charges_from_date
from_date
return from_date if plan.pay_in_arrear?

from_date - 1.week
end

def compute_charges_to_date
return to_date if plan.pay_in_arrear?

# NOTE: In pay in advance scenario, from_date will be the begining of the new period.
# To get the end of the previous one, we just have to take the day before
from_date - 1.day
end

def compute_next_end_of_period(date)
Expand All @@ -35,13 +47,23 @@ def compute_next_end_of_period(date)
date.next_occurring(subscription_day_name) - 1.day
end

def compute_previous_beginning_of_period(date)
return date.beginning_of_week if calendar?

previous_anniversary_day(date)
end

def previous_anniversary_day(date)
date.prev_occurring(subscription_day_name)
end

def subscription_day_name
@subscription_day_name ||= subscription_date.strftime('%A').downcase.to_sym
end

def compute_duration(*)
WEEK_DURATION
end
end
end
end
42 changes: 38 additions & 4 deletions app/services/subscriptions/dates/yearly_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,24 @@
module Subscriptions
module Dates
class YearlyService < Subscriptions::DatesService
def first_month_in_yearly_period?
return billing_date.month == 1 if calendar?

monthly_service.compute_from_date(billing_date - 1.month).month == subscription_date.month
end

private

def compute_base_date
billing_date - 1.year
end

def monthly_service
@monthly_service ||= Subscriptions::Dates::MonthlyService.new(subscription, billing_date)
@monthly_service ||= Subscriptions::Dates::MonthlyService.new(subscription, billing_date, current_usage)
end

def compute_from_date
if terminated_pay_in_arrear?
if plan.pay_in_advance? || terminated_pay_in_arrear?
return subscription.anniversary? ? previous_anniversary_day(billing_date) : billing_date.beginning_of_year
end

Expand All @@ -32,9 +38,21 @@ def compute_to_date
end

def compute_charges_from_date
return from_date unless plan.bill_charges_monthly
return monthly_service.compute_charges_from_date if plan.bill_charges_monthly
return from_date if plan.pay_in_arrear?
return base_date.beginning_of_year if calendar?

previous_anniversary_day(base_date)
end

def compute_charges_to_date
return to_date if plan.pay_in_arrear?

# NOTE: In pay in advance scenario, from_date will be the begining of the new period.
# To get the end of the previous one, we just have to take the day before
return from_date - 1.day unless plan.bill_charges_monthly

monthly_service.compute_from_date(billing_date - 1.month)
monthly_service.compute_charges_to_date
end

def compute_next_end_of_period(date)
Expand All @@ -51,13 +69,29 @@ def compute_next_end_of_period(date)
build_date(year + 1, month, day) - 1.day
end

def compute_previous_beginning_of_period(date)
return date.beginning_of_year if calendar?

previous_anniversary_day(date)
end

def previous_anniversary_day(date)
year = date.month < subscription_date.month ? date.year - 1 : date.year
month = subscription_date.month
day = subscription_date.day

build_date(year, month, day)
end

def compute_duration(from_date:)
return Time.days_in_year(from_date.year) if calendar?

year = from_date.year
# NOTE: if after February we must check if next year is a leap year
year += 1 if from_date.month > 2

Time.days_in_year(year)
end
end
end
end
53 changes: 42 additions & 11 deletions app/services/subscriptions/dates_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module Subscriptions
class DatesService
def self.new_instance(subscription, billing_date)
def self.new_instance(subscription, billing_date, current_usage: false)
klass = case subscription.plan.interval&.to_sym
when :weekly
Subscriptions::Dates::WeeklyService
Expand All @@ -14,15 +14,16 @@ def self.new_instance(subscription, billing_date)
raise NotImplementedError
end

klass.new(subscription, billing_date)
klass.new(subscription, billing_date, current_usage)
end

def initialize(subscription, billing_date)
def initialize(subscription, billing_date, current_usage)
@subscription = subscription

# NOTE: Billing date should usually be the end of the billing period + 1 day
# When subscription is terminated, it is the termination day
@billing_date = billing_date.to_date
@current_usage = current_usage
end

def from_date
Expand All @@ -42,14 +43,8 @@ def to_date
return @to_date if @to_date

@to_date = compute_to_date

@to_date = subscription.terminated_at.to_date if subscription.terminated? && @to_date > subscription.terminated_at

# NOTE: When price plan is configured as `pay_in_advance`, subscription creation will be
# billed immediatly. An invoice must be generated for it with only the subscription fee.
# The invoicing period will be only one day: the subscription day
@to_date = subscription.started_at.to_date if plan.pay_in_advance? && subscription.fees.subscription_kind.none?

@to_date
end

Expand All @@ -60,18 +55,38 @@ def charges_from_date
date
end

def charges_to_date
date = compute_charges_to_date
date = subscription.terminated_at.to_date if subscription.terminated? && date > subscription.terminated_at

date
end

def next_end_of_period(date)
compute_next_end_of_period(date)
end

# NOTE: Retrieve the beginning of the previous period based on the billing date
def previous_beginning_of_period(current_period: false)
date = base_date
date = billing_date if current_period

compute_previous_beginning_of_period(date)
end

def single_day_price(optional_from_date: nil)
duration = compute_duration(from_date: optional_from_date || compute_from_date)
plan.amount_cents.fdiv(duration.to_i)
end

private

attr_accessor :subscription, :billing_date
attr_accessor :subscription, :billing_date, :current_usage

delegate :plan, :subscription_date, :calendar?, to: :subscription

def base_date
@base_date ||= compute_base_date
@base_date ||= current_usage ? billing_date : compute_base_date
end

def terminated_pay_in_arrear?
Expand All @@ -88,6 +103,10 @@ def build_date(year, month, day)
Date.new(year, month, day)
end

def compute_base_date
raise NotImplementedError
end

def compute_from_date
raise NotImplementedError
end
Expand All @@ -100,8 +119,20 @@ def compute_charges_from_date
raise NotImplementedError
end

def compute_charges_to_date
raise NotImplementedError
end

def compute_next_end_of_period(date)
raise NotImplementedError
end

def first_month_in_yearly_period?
false
end

def compute_duration(from_date:)
raise NotImplementedError
end
end
end
Loading