Skip to content

Commit

Permalink
Merge pull request #180 from streetsmartslabs/plan_tiers
Browse files Browse the repository at this point in the history
Plan tiers
  • Loading branch information
tansengming authored Mar 31, 2020
2 parents 6905229 + 2361c4e commit f8cde71
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 6 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,23 @@ Stripe.plan :bronze do |plan|
plan.product_id = 'prod_XXXXXXXXXXXXXX'
plan.amount = 999 # $9.99
plan.interval = 'month'

# Use graduated pricing tiers
# ref: https://stripe.com/docs/api/plans/object#plan_object-tiers
plan.tiers = [
{
unit_amount: 1500,
up_to: 10
},
{
unit_amount: 1000,
up_to: 'inf'
}
]
plan.tiers_mode = 'graduated'

# set the usage type to 'metered'
plan.usage_type = 'metered'
end
```

Expand Down
34 changes: 34 additions & 0 deletions lib/stripe/billing_tier.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module Stripe
module Plans
class BillingTier
include ActiveModel::Validations

validates_presence_of :up_to
validates_presence_of :flat_amount, if: ->(tier) { tier.unit_amount.nil? },
message: 'one of `flat_amount` or `unit_amount` must be specified!'
validates_presence_of :unit_amount, if: ->(tier) { tier.flat_amount.nil? },
message: 'one of `flat_amount` or `unit_amount` must be specified!'
validates_absence_of :flat_amount, if: ->(tier) { tier.unit_amount.present? },
message: 'only one of `flat_amount` or `unit_amount` should be specified!'
validates_absence_of :unit_amount, if: ->(tier) { tier.flat_amount.present? },
message: 'only one of `flat_amount` or `unit_amount` should be specified!'

attr_accessor :up_to, :flat_amount, :unit_amount

def initialize(attrs)
@up_to = attrs[:up_to]
@flat_amount = attrs[:flat_amount]
@unit_amount = attrs[:unit_amount]
end

def to_h
{
up_to: up_to,
flat_amount: flat_amount,
unit_amount: unit_amount
}.compact
end

end
end
end
38 changes: 33 additions & 5 deletions lib/stripe/plans.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,20 @@ module Plans
:interval,
:interval_count,
:metadata,
:name,
:name,
:nickname,
:product_id,
:statement_descriptor,
:tiers,
:tiers_mode,
:transform_usage,
:trial_period_days,
:usage_type

validates_presence_of :id, :amount, :currency

validates_presence_of :id, :currency
validates_presence_of :amount, unless: ->(p) { p.billing_scheme == 'tiered' }
validates_absence_of :transform_usage, if: ->(p) { p.billing_scheme == 'tiered' }
validates_presence_of :tiers_mode, if: ->(p) { p.billing_scheme == 'tiered' }
validates_inclusion_of :interval,
in: %w(day week month year),
message: "'%{value}' is not one of 'day', 'week', 'month' or 'year'"
Expand All @@ -38,6 +42,11 @@ module Plans
validate :aggregate_usage_must_be_metered, if: ->(p) { p.aggregate_usage.present? }
validate :valid_constant_name, unless: ->(p) { p.constant_name.nil? }

# validations for when using tiered billing
validate :tiers_must_be_array, if: ->(p) { p.tiers.present? }
validate :billing_scheme_must_be_tiered, if: ->(p) { p.tiers.present? }
validate :validate_tiers, if: ->(p) { p.billing_scheme == 'tiered' }

def initialize(*args)
super(*args)
@currency = 'usd'
Expand All @@ -54,6 +63,22 @@ def name_or_product_id
errors.add(:base, 'must have a product_id or a name') unless (@product_id.present? ^ @name.present?)
end

def billing_scheme_must_be_tiered
errors.add(:billing_scheme, 'must be set to `tiered` when specifying `tiers`') unless billing_scheme == 'tiered'
end

def tiers_must_be_array
errors.add(:tiers, 'must be an Array') unless tiers.is_a?(Array)
end

def billing_tiers
@billing_tiers = tiers.map { |t| Stripe::Plans::BillingTier.new(t) } if tiers
end

def validate_tiers
billing_tiers.all?(&:valid?)
end

module ConstTester; end
def valid_constant_name
ConstTester.const_set(constant_name.to_s.upcase, constant_name)
Expand Down Expand Up @@ -82,8 +107,11 @@ def default_create_options
usage_type: usage_type,
aggregate_usage: aggregate_usage,
billing_scheme: billing_scheme,
nickname: nickname
}
nickname: nickname,
tiers: tiers ? tiers.map(&:to_h) : nil,
tiers_mode: tiers_mode,
transform_usage: transform_usage
}.compact
end

def product_options
Expand Down
2 changes: 2 additions & 0 deletions lib/stripe/products.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module Products
:caption,
:metadata,
:shippable,
:unit_label,
:url,
:statement_descriptor

Expand Down Expand Up @@ -42,6 +43,7 @@ def create_options
caption: caption,
metadata: metadata,
shippable: shippable,
unit_label: unit_label,
url: url,
statement_descriptor: statement_descriptor
}
Expand Down
1 change: 1 addition & 0 deletions lib/stripe/rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require 'stripe/configuration_builder'
require 'stripe/current_api_version'
require 'stripe/plans'
require 'stripe/billing_tier'
require 'stripe/coupons'
require 'stripe/products'
require 'stripe/callbacks'
21 changes: 21 additions & 0 deletions test/dummy/config/stripe/plans.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,24 @@
plan.aggregate_usage = 'max'
plan.billing_scheme = 'per_unit'
end

Stripe.plan :tiered do |plan|
plan.name = 'Tiered'
plan.aggregate_usage = 'max'
plan.billing_scheme = 'tiered'
# interval must be either 'day', 'week', 'month' or 'year'
plan.interval = 'month'
plan.interval_count = 1
plan.tiers = [
{
unit_amount: 1500,
up_to: 10
},
{
unit_amount: 1000,
up_to: 'inf'
}
]
plan.tiers_mode = 'graduated'
plan.usage_type = 'metered'
end
78 changes: 77 additions & 1 deletion test/plan_builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,82 @@
Stripe::Plans::METERED.put!
end

it 'creates a tiered plan' do
Stripe::Plan.expects(:create).with(
:id => :tiered,
:currency => 'usd',
:product => {
:name => 'Tiered',
:statement_descriptor => nil,
},
:interval => 'month',
:interval_count => 1,
:trial_period_days => 0,
:usage_type => 'metered',
:aggregate_usage => 'max',
:billing_scheme => 'tiered',
:tiers => [
{
:unit_amount => 1500,
:up_to => 10
},
{
:unit_amount => 1000,
:up_to => 'inf'
}
],
:tiers_mode => 'graduated'
)
plan = Stripe::Plans::TIERED
Stripe::Plans::TIERED.put!
end

describe 'when passed invalid arguments for tiered pricing' do
it 'raises a Stripe::InvalidConfigurationError when billing tiers are invalid' do
lambda {
Stripe.plan "Bad Tiers".to_sym do |plan|
plan.name = 'Acme as a service BAD TIERS'
plan.constant_name = 'BAD_TIERS'
plan.interval = 'month'
plan.interval_count = 1
plan.trial_period_days = 30
plan.usage_type = 'metered'
plan.tiers_mode = 'graduated'
plan.billing_scheme = 'per_unit'
plan.aggregate_usage = 'sum'
plan.tiers = [
{
unit_amount: 1500,
up_to: 10
},
{
unit_amount: 1000,
}
]
end
}.must_raise Stripe::InvalidConfigurationError
end

it 'raises a Stripe::InvalidConfigurationError when billing tiers is not an array' do
lambda {
Stripe.plan "Bad Tiers".to_sym do |plan|
plan.name = 'Acme as a service BAD TIERS'
plan.constant_name = 'BAD_TIERS'
plan.interval = 'month'
plan.interval_count = 1
plan.trial_period_days = 30
plan.usage_type = 'metered'
plan.tiers_mode = 'graduated'
plan.billing_scheme = 'per_unit'
plan.aggregate_usage = 'sum'
plan.tiers = {
unit_amount: 1500,
up_to: 10
}
end
}.must_raise Stripe::InvalidConfigurationError
end
end

describe 'when using a product id' do
before do
Expand Down Expand Up @@ -346,7 +422,7 @@
proc {Stripe.plan(:bad) {}}.must_raise Stripe::InvalidConfigurationError
end
end

describe 'with custom constant name' do
before do
Stripe.plan "Primo Plan".to_sym do |plan|
Expand Down

0 comments on commit f8cde71

Please sign in to comment.