Skip to content

Commit

Permalink
Merge pull request #2 from simplymadeapps/time-elasped-remaining
Browse files Browse the repository at this point in the history
Add ability to get the time and percentage elapsed/remaining in a billing cycle
  • Loading branch information
brianpattison authored Aug 9, 2019
2 parents f98d7b4 + ab9eccb commit a9e595f
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 5 deletions.
81 changes: 77 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
[![Test Coverage](https://codeclimate.com/github/simplymadeapps/billing_cycle/badges/coverage.svg)](https://codeclimate.com/github/simplymadeapps/billing_cycle/coverage)
[![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/github/simplymadeapps/billing_cycle/)

Billing Cycle is a gem used to calculate the next billing date for a recurring subscription.
Billing Cycle is a gem used to calculate the billing due dates and time elapsed/remaining in
a recurring subscription's billing cycle.

## Installation

Expand All @@ -23,6 +24,12 @@ $ bundle

## Usage

### Due Dates

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.

```ruby
original_billing_date = Time.zone.parse("2018-01-31 00:00:00")
billing_interval = 1.month
Expand All @@ -34,11 +41,15 @@ Time.zone.now
billing_cycle.next_due_at
# => Sat, 30 Jun 2018 00:00:00 CDT -05:00

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
# => 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.

```ruby
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
Expand All @@ -59,6 +70,67 @@ billing_cycle.previous_due_at
# => nil
```

### Time Elapsed and Remaining

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.

```ruby
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.

```ruby
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)
```

### Percent Elapsed and Remaining

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.

```ruby
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.

```ruby
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
```

## Contributing

1. Fork it
Expand All @@ -68,4 +140,5 @@ billing_cycle.previous_due_at
5. Create new Pull Request

## License

The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
37 changes: 37 additions & 0 deletions lib/billing_cycle/billing_cycle.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@ def next_due_at(now = Time.zone.now)
next_due_at
end

# Returns the percentage of time that's elapsed between the previous due date and the next due date.
# @param now [Time] The current time
# @return [Float] The percentage as a fraction of 1.0
def percent_elapsed(now = Time.zone.now)
time_elapsed(1.second, now) / seconds_in_cycle(now)
end

# Returns the percentage of time that's remaining between the previous due date and the next due date.
# @param now [Time] The current time
# @return [Float] The percentage as a fraction of 1.0
def percent_remaining(now = Time.zone.now)
time_remaining(1.second, now) / seconds_in_cycle(now)
end

# Returns the previous billing date based on the subscription
# @param now [Time] The current time
# @return [Time] The previous billing date/time
Expand All @@ -55,6 +69,21 @@ def previous_due_at(now = Time.zone.now)
previous_due_at
end

# Returns the time elapsed since the previous due date.
# @param interval [ActiveSupport::Duration] The duration
# @param now [Time] The current time
# @return [Float] The number of intervals since the previous due date
def time_elapsed(interval = 1.second, now = Time.zone.now)
(now - previous_due_at(now)) / interval
end

# Returns the time remaining until the next due date.
# @param now [Time] The current time
# @return [Float] The number of intervals until the next due date
def time_remaining(interval = 1.second, now = Time.zone.now)
(next_due_at(now) - now) / interval
end

private

# Calculate the due date based on number of billing cycles since
Expand Down Expand Up @@ -86,9 +115,17 @@ def interval_units
end

# Returns the number billing cycles that have occurred between the created date and "now".
# @param now [Time] The current time
# @return [Integer]
def number_of_cycles_since_created(now)
(interval_value * ((now - created_at).to_i / interval)).to_i
end

# Returns the total number of seconds in the current billing cycle.
# @param now [Time] The current time
# @return [Float]
def seconds_in_cycle(now)
next_due_at(now) - previous_due_at(now)
end
end
end
2 changes: 1 addition & 1 deletion lib/billing_cycle/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module BillingCycle
VERSION = "1.0.0".freeze
VERSION = "1.1.0".freeze
end
72 changes: 72 additions & 0 deletions spec/billing_cycle_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,36 @@
end
end

describe "percent_elapsed" do
let(:created_at) { Time.zone.parse("2019-06-01 00:00:00") }

it "returns the percentage of time that's elapsed in the billing cycle based on the current date/time" do
travel_to Time.zone.parse("2019-06-16 00:00:00") do
expect(billing_cycle.percent_elapsed).to eq(0.5)
end
end

it "returns the percentage of time that's elapsed in the billing cycle based on a given date/time" do
now = Time.zone.parse("2019-06-07 00:00:00")
expect(billing_cycle.percent_elapsed(now)).to eq(0.2)
end
end

describe "percent_remaining" do
let(:created_at) { Time.zone.parse("2019-06-01 00:00:00") }

it "returns the percentage of time that's remaining in the billing cycle based on the current date/time" do
travel_to Time.zone.parse("2019-06-16 00:00:00") do
expect(billing_cycle.percent_remaining).to eq(0.5)
end
end

it "returns the percentage of time that's remaining in the billing cycle based on a given date/time" do
now = Time.zone.parse("2019-06-07 00:00:00")
expect(billing_cycle.percent_remaining(now)).to eq(0.8)
end
end

describe "previous_due_at" do
it "returns the previous date/time in the billing cycle based on the current date/time" do
travel_to Time.zone.parse("2018-08-20 00:00:00") do
Expand Down Expand Up @@ -332,6 +362,48 @@
end
end

describe "time_elapsed" do
let(:created_at) { Time.zone.parse("2019-06-01 00:00:00") }

it "returns the number of seconds elapsed in the billing cycle based on the current date/time" do
travel_to Time.zone.parse("2019-06-16 00:00:00") do
expect(billing_cycle.time_elapsed).to eq(1_296_000.0)
end
end

it "returns the number of intervals elapsed in the billing cycle based on the current date/time" do
travel_to Time.zone.parse("2019-06-16 00:00:00") do
expect(billing_cycle.time_elapsed(1.day)).to eq(15.0)
end
end

it "returns the number of intervals elapsed in the billing cycle based on a given current date/time" do
now = Time.zone.parse("2019-06-07 00:00:00")
expect(billing_cycle.time_elapsed(1.day, now)).to eq(6.0)
end
end

describe "time_remaining" do
let(:created_at) { Time.zone.parse("2019-06-01 00:00:00") }

it "returns the number of seconds remaining in the billing cycle based on the current date/time" do
travel_to Time.zone.parse("2019-06-16 00:00:00") do
expect(billing_cycle.time_remaining).to eq(1_296_000.0)
end
end

it "returns the number of intervals remaining in the billing cycle based on the current date/time" do
travel_to Time.zone.parse("2019-06-16 00:00:00") do
expect(billing_cycle.time_remaining(1.day)).to eq(15.0)
end
end

it "returns the number of intervals remaining in the billing cycle based on a given current date/time" do
now = Time.zone.parse("2019-06-07 00:00:00")
expect(billing_cycle.time_remaining(1.day, now)).to eq(24.0)
end
end

describe "interval_units" do
context "when the interval is a duration from ActiveSupport 4" do
let(:interval) { double("ActiveSupport::Duration", parts: parts) }
Expand Down

0 comments on commit a9e595f

Please sign in to comment.