Billing Cycle is a gem used to calculate the billing due dates and time elapsed/remaining in a recurring subscription's billing cycle.
Add this line to your application's Gemfile:
gem "billing_cycle"
And then execute:
$ bundle
Calculating due dates requires the original billing date as an anchor to determine due dates
in the future. BillingCycle::BillingCycle
does the work to handle edge cases like having
a monthly subscription that started on the 31st when there's not 31 days in every month.
original_billing_date = Time.zone.parse("2018-01-31 00:00:00")
billing_interval = 1.month
billing_cycle = BillingCycle::BillingCycle.new(original_billing_date, billing_interval)
Time.zone.now
# => Tue, 26 Jun 2018 00:00:00 CDT -05:00
billing_cycle.next_due_at
# => Sat, 30 Jun 2018 00:00:00 CDT -05:00
billing_cycle.previous_due_at
# => Thu, 31 May 2018 00:00:00 CDT -05:00
A time can be passed in as "now" instead of implicitly using the current time.
billing_cycle.next_due_at(Time.zone.parse("2020-02-01 00:00:00")
# => Sat, 29 Feb 2020 00:00:00 CST -06:00
billing_cycle.previous_due_at(Time.zone.parse("2020-02-01 00:00:00")
# => Fri, 31 Jan 2020 00:00:00 CST -06:00
If the original billing date is in the future, the next_due_at
will always be the
original billing date, and the previous_due_at
will be nil
.
original_billing_date = Time.zone.parse("9999-12-31 00:00:00")
billing_interval = 1.month
billing_cycle = BillingCycle::BillingCycle.new(original_billing_date, billing_interval)
billing_cycle.next_due_at
# => Fri, 31 Dec 9999 00:00:00 CST -06:00
billing_cycle.previous_due_at
# => nil
The time elapsed and remaining can be used for displaying how much time has been used and how much time is left in a billing cycle.
original_billing_date = Time.zone.parse("2019-06-01 00:00:00")
billing_interval = 1.month
billing_cycle = BillingCycle::BillingCycle.new(original_billing_date, billing_interval)
Time.zone.now
# => Sun, 16 Jun 2019 00:00:00 CDT -05:00
billing_cycle.time_elapsed
# => 1296000.0 (seconds)
billing_cycle.time_remaining
# => 1296000.0 (seconds)
An interval can be passed in instead of implicitly using seconds and time can be passed in as "now" instead of implicitly using the current time.
billing_cycle.time_elapsed(1.day, Time.zone.parse("2019-06-07 00:00:00"))
# => 6.0 (days)
billing_cycle.time_remaining(1.day, Time.zone.parse("2019-06-07 00:00:00"))
# => 24.0 (days)
The percent elapsed and remaining can be used for calculating subscription pricing when charging or refunding for a partial billing cycle. The percentages are returned as a fraction of 1.0.
original_billing_date = Time.zone.parse("2019-06-01 00:00:00")
billing_interval = 1.month
billing_cycle = BillingCycle::BillingCycle.new(original_billing_date, billing_interval)
Time.zone.now
# => Sun, 16 Jun 2019 00:00:00 CDT -05:00
billing_cycle.percent_elapsed
# => 0.5
billing_cycle.percent_remaining
# => 0.5
A time can be passed in as "now" instead of implicitly using the current time.
billing_cycle.percent_elapsed(Time.zone.parse("2019-06-07 00:00:00"))
# => 0.2
billing_cycle.percent_remaining(Time.zone.parse("2019-06-07 00:00:00"))
# => 0.8
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request
The gem is available as open source under the terms of the MIT License.