stripe-rails 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eb58cd2c4b57bfb766859f950ea819137df7d25df3064654877db23a853fc181
4
- data.tar.gz: b93b09f34648ff133397a56606a37bb5067130d19aef9007c823e13b993a4cfe
3
+ metadata.gz: 15856817d5ce2ac1b96f1ad4f6f93602b5b80f0790cfbbd56ae87c0d89654571
4
+ data.tar.gz: 602743d7695c49114426acf961c7c7df1fd7e6d959cb1d00c860a729913847c3
5
5
  SHA512:
6
- metadata.gz: 39ff75adfde760950733e00324423c99506d53f9cee614a7adc0ec61a2ea14758e9d9a1dc49a4cd2ceac83ed7c7597c8c276cb0cb169ba813c7e2de9da7fe237
7
- data.tar.gz: 861343d690279311225fda2091e0d318c08c2c4c33e98d4fb73f9e528750a6751e7b0f07a2b1c43341da8a4011e4ff3618c3cee799445a38dd720bfab6d6b250
6
+ metadata.gz: 29127701ac3cccb96aa0bd215f616fa83b41d55cfc40d41aaafd56ca0ca0a479822b4c473bfbe3d9fdb3dfac6d0061dd93e237a7a412811358952d0c97f51f60
7
+ data.tar.gz: '07384662c64848e9e86bc592501b705e7879c8a8222dae408195b5b3b6fd8ef9e23cc6379d3f9dcd382dbe6806448a8978108f449984605de0232c81763c0423'
@@ -33,6 +33,9 @@ jobs:
33
33
  CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
34
34
  RUBY_VERSION: ${{ matrix.ruby }}
35
35
  run: |
36
+ if [ $RUBY_VERSION == "2.5.x" ] ;
37
+ then gem install bundler
38
+ fi
36
39
  bundle install --jobs 4 --retry 3
37
40
  bundle exec rake
38
41
  if [ `basename $BUNDLE_GEMFILE` == "Gemfile" ] && [ $RUBY_VERSION == "2.7.x" ] && [ ! -z ${CC_TEST_REPORTER_ID} ] ;
@@ -1,3 +1,7 @@
1
+ ## 2.2.0 (2020-12-06)
2
+
3
+ - Add Prices as a configuration object. Thanks @jamesprior!
4
+
1
5
  ## 2.1.0 (2020-10-18)
2
6
 
3
7
  - Added option to ignore missing API key and don't show any warning. Thanks @ndbroadbent!
data/README.md CHANGED
@@ -9,7 +9,7 @@ This gem can help your rails application integrate with Stripe in the following
9
9
 
10
10
  * manage stripe configurations in a single place.
11
11
  * makes stripe.js available from the asset pipeline.
12
- * manage plans and coupons from within your app.
12
+ * manage product, prices, plans and coupons from within your app.
13
13
  * painlessly receive and validate webhooks from stripe.
14
14
 
15
15
  [📫 Sign up for the Newsletter](http://tinyletter.com/stripe-rails) to receive occasional updates.
@@ -170,6 +170,7 @@ this will generate the configuration files containing your plan and coupon defin
170
170
  ```console
171
171
  create config/stripe/products.rb
172
172
  create config/stripe/plans.rb
173
+ create config/stripe/prices.rb
173
174
  create config/stripe/coupons.rb
174
175
  ```
175
176
 
@@ -267,17 +268,46 @@ Stripe.product :primo do |product|
267
268
  end
268
269
  ```
269
270
 
270
- To upload your plans and coupons onto stripe.com, run:
271
+ And Prices:
272
+
273
+ ```ruby
274
+ Stripe.price :bronze do |price|
275
+ # Use an existing product id to prevent a new product from
276
+ # getting created
277
+ price.product_id = Stripe::Products::PRIMO.id
278
+ price.billing_scheme = 'tiered'
279
+ price.recurring = {
280
+ interval: 'month',
281
+ usage_type: 'metered'
282
+ }
283
+
284
+ # Use graduated pricing tiers
285
+ # ref: https://stripe.com/docs/api/prices/object#price_object-tiers
286
+ price.tiers = [
287
+ {
288
+ unit_amount: 1500,
289
+ up_to: 10
290
+ },
291
+ {
292
+ unit_amount: 1000,
293
+ up_to: 'inf'
294
+ }
295
+ ]
296
+ price.tiers_mode = 'graduated'
297
+ end
298
+ ````
299
+
300
+ To upload your plans, products, prices and coupons onto stripe.com, run:
271
301
 
272
302
  ```sh
273
303
  rake stripe:prepare
274
304
  ```
275
305
 
276
- This will create any plans and coupons that do not currently exist, and treat as a NOOP any
277
- plans that do, so you can run this command safely as many times as you wish. Now you can
278
- use any of these plans in your application.
306
+ This will create any plans, products, prices and coupons that do not currently exist, and treat as a NOOP any
307
+ objects that already exist, so you can run this command safely as many times as you wish. Now you can
308
+ use any of these objects in your application.
279
309
 
280
- NOTE: You must destroy plans manually from your stripe dashboard.
310
+ NOTE: You must destroy plans and prices manually from your stripe dashboard.
281
311
 
282
312
  ## Stripe Elements
283
313
 
data/Rakefile CHANGED
File without changes
@@ -5,6 +5,5 @@ Rails.application.routes.draw do
5
5
  end
6
6
 
7
7
  Stripe::Engine.routes.draw do
8
- resource :ping, only: :show
9
8
  resources :events, only: :create
10
9
  end
@@ -6,6 +6,7 @@ module Stripe
6
6
  def copy_plans_file
7
7
  copy_file "products.rb", "config/stripe/products.rb"
8
8
  copy_file "plans.rb", "config/stripe/plans.rb"
9
+ copy_file "prices.rb", "config/stripe/prices.rb"
9
10
  copy_file "coupons.rb", "config/stripe/coupons.rb"
10
11
  end
11
12
  end
@@ -0,0 +1,46 @@
1
+ # This file contains descriptions of all your stripe prices
2
+
3
+ # Example
4
+ # Stripe::Prices::LITE.lookup_key #=> 'lite'
5
+
6
+ # Prices will have a stripe generated id. The lookup_key will match the
7
+ # configuration below. You can fetch the ID or object from stripe:
8
+ #
9
+ # Stripe::Prices::LITE.stripe_id #=> 'price_0000sdfs2qfsdf'
10
+ # Stripe::Prices::LITE.stripe_object #=> #<Stripe::Price:0x3584 id=price_0000sdfs2qfsdf>...
11
+
12
+ # Prices are not deletable via the API, the `reset!` method will instead
13
+ # create a new price and transfer the lookup key to the new price.
14
+
15
+ # Stripe.price :lite do |price|
16
+ # # Prices may belong to a product, this will create a product along with the price
17
+ # price.name = 'Acme as a service LITE'
18
+
19
+ # # You can also specify an existing product ID
20
+ # # price.product_id = Stripe::Products::PRIMO.id
21
+ #
22
+ # # amount in cents. This is 6.99
23
+ # price.unit_amount = 699
24
+ #
25
+ # # currency to use for the price (default 'usd')
26
+ # price.currency = 'usd'
27
+ #
28
+ # price.recurring = {
29
+ # # interval must be either 'day', 'week', 'month' or 'year'
30
+ # interval: 'month',
31
+ # # only bill once every three months (default 1)
32
+ # interval_count: 3,
33
+ # # Must be either 'metered' or 'licensed'
34
+ # usage_type: 'metered',
35
+ # # Specifies a usage aggregation strategy for metered usage
36
+ # aggregate_usage: 'sum'
37
+ # }
38
+ #
39
+ # end
40
+
41
+ # Once you have your prices defined, you can run
42
+ #
43
+ # rake stripe:prepare
44
+ #
45
+ # This will export any new prices to stripe.com so that you can
46
+ # begin using them in your API calls.
@@ -89,7 +89,7 @@ environment file directly.
89
89
  end
90
90
 
91
91
  initializer 'stripe.plans_and_coupons' do |app|
92
- for configuration in %w(products plans coupons)
92
+ for configuration in %w(products plans coupons prices)
93
93
  path = app.root.join("config/stripe/#{configuration}.rb")
94
94
  load path if path.exist?
95
95
  end
@@ -0,0 +1,189 @@
1
+ module Stripe
2
+ module Prices
3
+ include ConfigurationBuilder
4
+ VALID_TIME_UNITS = %i(day week month year)
5
+
6
+ configuration_for :price do
7
+ attr_reader :lookup_key
8
+ attr_accessor :active,
9
+ :billing_scheme,
10
+ :constant_name,
11
+ :currency,
12
+ :metadata,
13
+ :name,
14
+ :nickname,
15
+ :object,
16
+ :product_id,
17
+ :recurring,
18
+ :statement_descriptor,
19
+ :tiers,
20
+ :tiers_mode,
21
+ :transform_quantity,
22
+ :type,
23
+ :unit_amount
24
+
25
+ validates_presence_of :id, :currency
26
+ validates_presence_of :unit_amount, unless: ->(p) { p.billing_scheme == 'tiered' }
27
+ validates_absence_of :transform_quantity, if: ->(p) { p.billing_scheme == 'tiered' }
28
+ validates_presence_of :tiers_mode, :tiers, if: ->(p) { p.billing_scheme == 'tiered' }
29
+
30
+ validates_numericality_of :recurring_interval_count, allow_nil: true
31
+
32
+ validates_inclusion_of :recurring_interval,
33
+ in: VALID_TIME_UNITS.collect(&:to_s),
34
+ message: "'%{value}' is not one of #{VALID_TIME_UNITS.to_sentence(last_word_connector: ', or ')}",
35
+ if: ->(p) { p.recurring.present? }
36
+
37
+ validates :statement_descriptor, length: { maximum: 22 }
38
+
39
+ validates :active, inclusion: { in: [true, false] }, allow_nil: true
40
+ validates :billing_scheme, inclusion: { in: %w{ per_unit tiered } }, allow_nil: true
41
+ validates :recurring_aggregate_usage, inclusion: { in: %w{ sum last_during_period last_ever max } }, allow_nil: true
42
+ validates :recurring_usage_type, inclusion: { in: %w{ metered licensed } }, allow_nil: true
43
+ validates :tiers_mode, inclusion: { in: %w{ graduated volume } }, allow_nil: true
44
+
45
+ validate :name_or_product_id
46
+ validate :recurring_aggregate_usage_must_be_metered, if: ->(p) { p.recurring_aggregate_usage.present? }
47
+ validate :recurring_interval_count_maximum, if: ->(p) { p.recurring_interval_count.present? }
48
+ validate :valid_constant_name, unless: ->(p) { p.constant_name.nil? }
49
+
50
+ # validations for when using tiered billing
51
+ validate :tiers_must_be_array, if: ->(p) { p.tiers.present? }
52
+ validate :billing_scheme_must_be_tiered, if: ->(p) { p.tiers.present? }
53
+ validate :validate_tiers, if: ->(p) { p.billing_scheme == 'tiered' }
54
+
55
+ def initialize(*args)
56
+ super(*args)
57
+ @currency = 'usd'
58
+ @lookup_key = @id.to_s
59
+ @recurring = (recurring || {}).symbolize_keys
60
+ end
61
+
62
+ # We're overriding a handful of the Configuration methods so that
63
+ # we find and create by lookup_key instead of by ID. The ID is assigned
64
+ # by stripe and out of our control
65
+ def put!
66
+ if exists?
67
+ puts "[EXISTS] - #{@stripe_class}:#{@id}:#{stripe_id}" unless Stripe::Engine.testing
68
+ else
69
+ object = @stripe_class.create({:lookup_key => @lookup_key}.merge compact_create_options)
70
+ puts "[CREATE] - #{@stripe_class}:#{object}" unless Stripe::Engine.testing
71
+ end
72
+ end
73
+
74
+ # You can't delete prices, but you can transfer the lookup key to a new price
75
+ def reset!
76
+ object = @stripe_class.create(reset_options)
77
+ puts "[RESET] - #{@stripe_class}:#{object}" unless Stripe::Engine.testing
78
+ end
79
+
80
+ def exists?
81
+ stripe_object.presence
82
+ rescue Stripe::InvalidRequestError
83
+ false
84
+ end
85
+
86
+ def stripe_object
87
+ @stripe_class.list({lookup_keys: [@lookup_key]}).data.first.presence || nil
88
+ rescue Stripe::InvalidRequestError
89
+ nil
90
+ end
91
+
92
+ def stripe_id
93
+ @stripe_id ||= stripe_object.try(:id)
94
+ end
95
+
96
+ def recurring_interval
97
+ recurring[:interval]
98
+ end
99
+
100
+ def recurring_aggregate_usage
101
+ recurring[:aggregate_usage]
102
+ end
103
+
104
+ def recurring_usage_type
105
+ recurring[:usage_type]
106
+ end
107
+
108
+ def recurring_interval_count
109
+ recurring[:interval_count]
110
+ end
111
+
112
+ private
113
+ def recurring_aggregate_usage_must_be_metered
114
+ errors.add(:recurring_aggregate_usage, 'recurring[:usage_type] must be metered') unless (recurring_usage_type == 'metered')
115
+ end
116
+
117
+ def recurring_interval_count_maximum
118
+ time_unit = recurring_interval.to_sym
119
+
120
+ return unless VALID_TIME_UNITS.include?(time_unit) && recurring_interval_count.respond_to?(time_unit)
121
+ too_long = recurring_interval_count.send(time_unit) > 1.year
122
+
123
+ errors.add(:recurring_interval_count, 'recurring[:interval_count] Maximum is one year (1 year, 12 months, or 52 weeks') if too_long
124
+ end
125
+
126
+ def name_or_product_id
127
+ errors.add(:base, 'must have a product_id or a name') unless (@product_id.present? ^ @name.present?)
128
+ end
129
+
130
+ def billing_scheme_must_be_tiered
131
+ errors.add(:billing_scheme, 'must be set to `tiered` when specifying `tiers`') unless billing_scheme == 'tiered'
132
+ end
133
+
134
+ def tiers_must_be_array
135
+ errors.add(:tiers, 'must be an Array') unless tiers.is_a?(Array)
136
+ end
137
+
138
+ def billing_tiers
139
+ @billing_tiers = tiers.map { |t| Stripe::Plans::BillingTier.new(t) } if tiers
140
+ end
141
+
142
+ def validate_tiers
143
+ billing_tiers.all?(&:valid?)
144
+ end
145
+
146
+ module ConstTester; end
147
+ def valid_constant_name
148
+ ConstTester.const_set(constant_name.to_s.upcase, constant_name)
149
+ ConstTester.send(:remove_const, constant_name.to_s.upcase.to_sym)
150
+ rescue NameError
151
+ errors.add(:constant_name, 'is not a valid Ruby constant name.')
152
+ end
153
+
154
+ def reset_options
155
+ existing_object = stripe_object
156
+ # Lookup and set the existing product ID if unset
157
+ @product_id ||= existing_object.product if existing_object.present?
158
+
159
+ { transfer_lookup_key: existing_object.present? }.merge(compact_create_options)
160
+ end
161
+
162
+ def create_options
163
+ {
164
+ currency: currency,
165
+ unit_amount: unit_amount,
166
+ active: active,
167
+ metadata: metadata,
168
+ nickname: nickname.presence || @lookup_key,
169
+ recurring: recurring.compact,
170
+ tiers: tiers ? tiers.map(&:to_h) : nil,
171
+ tiers_mode: tiers_mode,
172
+ billing_scheme: billing_scheme,
173
+ lookup_key: @lookup_key,
174
+ transform_quantity: transform_quantity,
175
+ }.merge(product_options).compact
176
+ end
177
+
178
+ def product_options
179
+ if product_id.present?
180
+ { product: product_id }
181
+ else
182
+ {
183
+ product_data: { name: name, statement_descriptor: statement_descriptor }
184
+ }
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -4,6 +4,7 @@ require 'stripe/engine'
4
4
  require 'stripe/configuration_builder'
5
5
  require 'stripe/current_api_version'
6
6
  require 'stripe/plans'
7
+ require 'stripe/prices'
7
8
  require 'stripe/billing_tier'
8
9
  require 'stripe/coupons'
9
10
  require 'stripe/products'
@@ -30,6 +30,15 @@ namespace :stripe do
30
30
  Stripe::Coupons.reset!
31
31
  end
32
32
 
33
- desc "create all plans and coupons defined in config/stripe/{products|plans|coupons}.rb"
34
- task 'prepare' => ['products:prepare', 'plans:prepare', 'coupons:prepare']
33
+ task 'prices:prepare' => 'environment' do
34
+ Stripe::Prices.put!
35
+ end
36
+
37
+ desc 'delete and redefine all prices defined in config/stripe/prices.rb'
38
+ task 'prices:reset!' => 'environment' do
39
+ Stripe::Prices.reset!
40
+ end
41
+
42
+ desc "create all plans and coupons defined in config/stripe/{products|plans|prices|coupons}.rb"
43
+ task 'prepare' => ['products:prepare', 'plans:prepare', 'prices:prepare', 'coupons:prepare']
35
44
  end
@@ -1,5 +1,5 @@
1
1
  module Stripe
2
2
  module Rails
3
- VERSION = '2.1.0'.freeze
3
+ VERSION = '2.2.0'.freeze
4
4
  end
5
5
  end
@@ -0,0 +1,59 @@
1
+ Stripe.price :gold do |price|
2
+ price.name = 'Solid Gold'
3
+ price.unit_amount = 699
4
+ price.recurring = {
5
+ interval: 'month'
6
+ }
7
+ end
8
+
9
+ Stripe.price "Solid Gold".to_sym do |price|
10
+ price.constant_name = 'SOLID_GOLD'
11
+ price.name = 'Solid Gold'
12
+ price.unit_amount = 699
13
+ price.recurring = {
14
+ interval: 'month'
15
+ }
16
+ end
17
+
18
+ Stripe.price :alternative_currency do |price|
19
+ price.name = 'Alternative Currency'
20
+ price.unit_amount = 699
21
+ price.recurring = {
22
+ interval: 'month'
23
+ }
24
+ price.currency = 'cad'
25
+ end
26
+
27
+ Stripe.price :metered do |price|
28
+ price.name = 'Metered'
29
+ price.unit_amount = 699
30
+ price.recurring = {
31
+ interval: 'month',
32
+ aggregate_usage: 'max',
33
+ usage_type: 'metered'
34
+ }
35
+ price.billing_scheme = 'per_unit'
36
+ end
37
+
38
+ Stripe.price :tiered do |price|
39
+ price.name = 'Tiered'
40
+ price.billing_scheme = 'tiered'
41
+ # interval must be either 'day', 'week', 'month' or 'year'
42
+ price.recurring = {
43
+ interval: 'month',
44
+ interval_count: 2,
45
+ aggregate_usage: 'max',
46
+ usage_type: 'metered'
47
+ }
48
+ price.tiers = [
49
+ {
50
+ unit_amount: 1500,
51
+ up_to: 10
52
+ },
53
+ {
54
+ unit_amount: 1000,
55
+ up_to: 'inf'
56
+ }
57
+ ]
58
+ price.tiers_mode = 'graduated'
59
+ end
@@ -9,7 +9,7 @@ describe ApisController do
9
9
  header 'Content-Type', 'application/json'
10
10
  end
11
11
 
12
- describe 'the ping interface' do
12
+ describe 'the apis interface' do
13
13
  subject { get '/apis/' }
14
14
 
15
15
  it { _(subject).must_be :ok? }
@@ -0,0 +1,7 @@
1
+ {
2
+ "object": "list",
3
+ "count": 0,
4
+ "data": [],
5
+ "has_more": false,
6
+ "url": "/v1/prices"
7
+ }
@@ -140,7 +140,7 @@ describe 'building plans' do
140
140
  }).must_raise Stripe::InvalidConfigurationError
141
141
  end
142
142
 
143
- it 'denies aggregate_usage if usage type is liecensed' do
143
+ it 'denies aggregate_usage if usage type is licensed' do
144
144
  _(lambda {
145
145
  Stripe.plan :broken do |plan|
146
146
  plan.name = 'Acme as a service'
@@ -0,0 +1,594 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'building prices' do
4
+ describe 'simply' do
5
+ before do
6
+ Stripe.price :lite do |price|
7
+ price.name = 'Acme as a service LITE'
8
+ price.unit_amount = 699
9
+ price.recurring = {
10
+ interval: 'month',
11
+ interval_count: 3,
12
+ usage_type: 'metered',
13
+ aggregate_usage: 'sum'
14
+ }
15
+ price.metadata = {:number_of_awesome_things => 5}
16
+ price.statement_descriptor = 'Acme Lite'
17
+ price.active = true
18
+ price.nickname = 'lite'
19
+ price.billing_scheme = 'per_unit'
20
+ price.tiers_mode = 'graduated'
21
+ end
22
+ end
23
+
24
+ after { Stripe::Prices.send(:remove_const, :LITE) }
25
+
26
+ it 'is accessible via id' do
27
+ _(Stripe::Prices::LITE).wont_be_nil
28
+ end
29
+
30
+ it 'is accessible via collection' do
31
+ _(Stripe::Prices.all).must_include Stripe::Prices::LITE
32
+ end
33
+
34
+ it 'is accessible via hash lookup (symbol/string agnostic)' do
35
+ _(Stripe::Prices[:lite]).must_equal Stripe::Prices::LITE
36
+ _(Stripe::Prices['lite']).must_equal Stripe::Prices::LITE
37
+ end
38
+
39
+ it 'sets the lookup key' do
40
+ _(Stripe::Prices::LITE.lookup_key).must_equal 'lite'
41
+ end
42
+
43
+ it 'accepts a billing interval of a day' do
44
+ Stripe.price :daily do |price|
45
+ price.name = 'Acme as a service daily'
46
+ price.unit_amount = 100
47
+ price.recurring = {
48
+ interval: 'day'
49
+ }
50
+ end
51
+
52
+ _(Stripe::Prices::DAILY).wont_be_nil
53
+ end
54
+
55
+ it 'denies a billing interval of a day and excessive intervals' do
56
+ _(lambda {
57
+ Stripe.price :broken do |price|
58
+ price.name = 'Acme as a service daily'
59
+ price.unit_amount = 100
60
+ price.recurring = {
61
+ interval: 'day',
62
+ interval_count: 366
63
+ }
64
+ end
65
+ }).must_raise Stripe::InvalidConfigurationError
66
+ end
67
+
68
+ it 'accepts a billing interval of a week' do
69
+ Stripe.price :weekly do |price|
70
+ price.name = 'Acme as a service weekly'
71
+ price.unit_amount = 100
72
+ price.recurring = {
73
+ interval: 'week'
74
+ }
75
+ end
76
+
77
+ _(Stripe::Prices::WEEKLY).wont_be_nil
78
+ end
79
+
80
+ it 'denies a billing interval of a week and excessive intervals' do
81
+ _(lambda {
82
+ Stripe.price :broken do |price|
83
+ price.name = 'Acme as a service weekly'
84
+ price.unit_amount = 100
85
+ price.recurring = {
86
+ interval: 'week',
87
+ interval_count: 53
88
+ }
89
+ end
90
+ }).must_raise Stripe::InvalidConfigurationError
91
+ end
92
+
93
+ it 'accepts a billing interval of a month' do
94
+ Stripe.price :monthly do |price|
95
+ price.name = 'Acme as a service monthly'
96
+ price.unit_amount = 400
97
+ price.recurring = {
98
+ interval: 'month'
99
+ }
100
+ end
101
+
102
+ _(Stripe::Prices::MONTHLY).wont_be_nil
103
+ end
104
+
105
+ it 'denies a billing interval of a month and excessive intervals' do
106
+ _(lambda {
107
+ Stripe.price :broken do |price|
108
+ price.name = 'Acme as a service monthly'
109
+ price.unit_amount = 400
110
+ price.recurring = {
111
+ interval: 'month',
112
+ interval_count: 13
113
+ }
114
+ end
115
+ }).must_raise Stripe::InvalidConfigurationError
116
+ end
117
+
118
+ it 'accepts a billing interval of a year' do
119
+ Stripe.price :yearly do |price|
120
+ price.name = 'Acme as a service yearly'
121
+ price.unit_amount = 4800
122
+ price.recurring = {
123
+ interval: 'year'
124
+ }
125
+ end
126
+
127
+ _(Stripe::Prices::YEARLY).wont_be_nil
128
+ end
129
+
130
+ it 'denies a billing interval of a year and excessive intervals' do
131
+ _(lambda {
132
+ Stripe.price :broken do |price|
133
+ price.name = 'Acme as a service yearly'
134
+ price.unit_amount = 4800
135
+ price.recurring = {
136
+ interval: 'year',
137
+ interval_count: 2
138
+ }
139
+ end
140
+ }).must_raise Stripe::InvalidConfigurationError
141
+ end
142
+
143
+ it 'denies arbitrary billing intervals' do
144
+ _(lambda {
145
+ Stripe.price :broken do |price|
146
+ price.name = 'Acme as a service BROKEN'
147
+ price.unit_amount = 999
148
+ price.recurring = {
149
+ interval: 'anything'
150
+ }
151
+ end
152
+ }).must_raise Stripe::InvalidConfigurationError
153
+ end
154
+
155
+ it 'accepts empty recurring options' do
156
+ Stripe.price :singular do |price|
157
+ price.name = 'Acme as a service one time'
158
+ price.unit_amount = 888
159
+ end
160
+
161
+ _(Stripe::Prices::SINGULAR).wont_be_nil
162
+ end
163
+
164
+ it 'accepts a statement descriptor' do
165
+ Stripe.price :described do |price|
166
+ price.name = 'Acme as a service'
167
+ price.unit_amount = 999
168
+ price.recurring = {
169
+ interval: 'month'
170
+ }
171
+ price.statement_descriptor = 'ACME Monthly'
172
+ end
173
+
174
+ _(Stripe::Prices::DESCRIBED).wont_be_nil
175
+ end
176
+
177
+ it 'denies statement descriptors that are too long' do
178
+ _(lambda {
179
+ Stripe.price :described do |price|
180
+ price.name = 'Acme as a service'
181
+ price.unit_amount = 999
182
+ price.recurring = {
183
+ interval: 'month'
184
+ }
185
+ price.statement_descriptor = 'ACME as a Service Monthly'
186
+ end
187
+ }).must_raise Stripe::InvalidConfigurationError
188
+ end
189
+
190
+ it 'denies invalid values for active' do
191
+ _(lambda {
192
+ Stripe.price :broken do |price|
193
+ price.name = 'Acme as a service'
194
+ price.unit_amount = 999
195
+ price.recurring = {
196
+ interval: 'month'
197
+ }
198
+ price.active = 'whatever'
199
+ end
200
+ }).must_raise Stripe::InvalidConfigurationError
201
+ end
202
+
203
+ it 'denies invalid values for usage_type' do
204
+ _(lambda {
205
+ Stripe.price :broken do |price|
206
+ price.name = 'Acme as a service'
207
+ price.unit_amount = 999
208
+ price.recurring = {
209
+ interval: 'month',
210
+ usage_type: 'whatever'
211
+ }
212
+ end
213
+ }).must_raise Stripe::InvalidConfigurationError
214
+ end
215
+
216
+ it 'denies invalid values for aggregate_usage' do
217
+ _(lambda {
218
+ Stripe.price :broken do |price|
219
+ price.name = 'Acme as a service'
220
+ price.unit_amount = 999
221
+ price.recurring = {
222
+ interval: 'month',
223
+ aggregate_usage: 'whatever'
224
+ }
225
+ end
226
+ }).must_raise Stripe::InvalidConfigurationError
227
+ end
228
+
229
+ it 'denies aggregate_usage if usage type is licensed' do
230
+ _(lambda {
231
+ Stripe.price :broken do |price|
232
+ price.name = 'Acme as a service'
233
+ price.unit_amount = 999
234
+ price.recurring = {
235
+ interval: 'month',
236
+ usage_type: 'licensed',
237
+ aggregate_usage: 'sum'
238
+ }
239
+ end
240
+ }).must_raise Stripe::InvalidConfigurationError
241
+ end
242
+
243
+
244
+ it 'denies invalid values for billing_scheme' do
245
+ _(lambda {
246
+ Stripe.price :broken do |price|
247
+ price.name = 'Acme as a service'
248
+ price.unit_amount = 999
249
+ price.recurring = {
250
+ interval: 'month'
251
+ }
252
+ price.billing_scheme = 'whatever'
253
+ end
254
+ }).must_raise Stripe::InvalidConfigurationError
255
+ end
256
+
257
+ it 'denies invalid values for tiers_mode' do
258
+ _(lambda {
259
+ Stripe.price :broken do |price|
260
+ price.name = 'Acme as a service'
261
+ price.unit_amount = 999
262
+ price.recurring = {
263
+ interval: 'month'
264
+ }
265
+ price.tiers_mode = 'whatever'
266
+ end
267
+ }).must_raise Stripe::InvalidConfigurationError
268
+ end
269
+
270
+ describe 'name and product id validation' do
271
+ it 'should be valid when using just the product id' do
272
+ Stripe.price :prodded do |price|
273
+ price.product_id = 'acme'
274
+ price.unit_amount = 999
275
+ price.recurring = {
276
+ interval: 'month'
277
+ }
278
+ end
279
+ _(Stripe::Prices::PRODDED).wont_be_nil
280
+ end
281
+
282
+ it 'should be invalid when using both name and product id' do
283
+ _(lambda {
284
+ Stripe.price :broken do |price|
285
+ price.name = 'Acme as a service'
286
+ price.product_id = 'acme'
287
+ price.unit_amount = 999
288
+ price.recurring = {
289
+ interval: 'month'
290
+ }
291
+ end
292
+ }).must_raise Stripe::InvalidConfigurationError
293
+ end
294
+ end
295
+
296
+ describe 'uploading' do
297
+ include FixtureLoader
298
+
299
+ describe 'when none exists on stripe.com' do
300
+ before do
301
+ Stripe::Price.stubs(:list).returns(Stripe::Price.construct_from(data: []))
302
+
303
+ stub_request(:get, "https://api.stripe.com/v1/prices").
304
+ with(headers: { 'Authorization'=>'Bearer XYZ',}).
305
+ to_return(status: 200, body: load_request_fixture('stripe_prices.json'))
306
+ end
307
+
308
+ it 'creates the price online' do
309
+ Stripe::Price.expects(:create).with(
310
+ :lookup_key => 'gold',
311
+ :nickname => 'gold',
312
+ :currency => 'usd',
313
+ :product_data => {
314
+ :name => 'Solid Gold',
315
+ :statement_descriptor => nil
316
+ },
317
+ :unit_amount => 699,
318
+ :recurring => {
319
+ :interval => 'month'
320
+ }
321
+ )
322
+ Stripe::Prices::GOLD.put!
323
+ end
324
+
325
+ it 'creates a price with an alternative currency' do
326
+ Stripe::Price.expects(:create).with(
327
+ :lookup_key => 'alternative_currency',
328
+ :nickname => 'alternative_currency',
329
+ :currency => 'cad',
330
+ :product_data => {
331
+ :name => 'Alternative Currency',
332
+ :statement_descriptor => nil
333
+ },
334
+ :unit_amount => 699,
335
+ :recurring => {
336
+ :interval => 'month'
337
+ }
338
+ )
339
+ Stripe::Prices::ALTERNATIVE_CURRENCY.put!
340
+ end
341
+
342
+ it 'creates a metered price' do
343
+ Stripe::Price.expects(:create).with(
344
+ :lookup_key => 'metered',
345
+ :nickname => 'metered',
346
+ :currency => 'usd',
347
+ :product_data => {
348
+ :name => 'Metered',
349
+ :statement_descriptor => nil,
350
+ },
351
+ :unit_amount => 699,
352
+ :recurring => {
353
+ :interval => 'month',
354
+ :usage_type => 'metered',
355
+ :aggregate_usage => 'max',
356
+ },
357
+ :billing_scheme => 'per_unit'
358
+ )
359
+ Stripe::Prices::METERED.put!
360
+ end
361
+
362
+ it 'creates a tiered price' do
363
+ Stripe::Price.expects(:create).with(
364
+ :lookup_key => 'tiered',
365
+ :nickname => 'tiered',
366
+ :currency => 'usd',
367
+ :product_data => {
368
+ :name => 'Tiered',
369
+ :statement_descriptor => nil,
370
+ },
371
+ :recurring => {
372
+ :interval => 'month',
373
+ :interval_count => 2,
374
+ :usage_type => 'metered',
375
+ :aggregate_usage => 'max'
376
+ },
377
+ :billing_scheme => 'tiered',
378
+ :tiers => [
379
+ {
380
+ :unit_amount => 1500,
381
+ :up_to => 10
382
+ },
383
+ {
384
+ :unit_amount => 1000,
385
+ :up_to => 'inf'
386
+ }
387
+ ],
388
+ :tiers_mode => 'graduated'
389
+ )
390
+ Stripe::Prices::TIERED.put!
391
+ end
392
+
393
+ describe 'when passed invalid arguments for tiered pricing' do
394
+ it 'raises a Stripe::InvalidConfigurationError when billing tiers are invalid' do
395
+ lambda {
396
+ Stripe.price "Bad Tiers".to_sym do |price|
397
+ price.name = 'Acme as a service BAD TIERS'
398
+ price.constant_name = 'BAD_TIERS'
399
+ price.recurring = {
400
+ interval: 'month',
401
+ interval_count: 1,
402
+ usage_type: 'metered',
403
+ aggregate_usage: 'sum'
404
+ }
405
+ price.tiers_mode = 'graduated'
406
+ price.billing_scheme = 'per_unit'
407
+ price.tiers = [
408
+ {
409
+ unit_amount: 1500,
410
+ up_to: 10
411
+ },
412
+ {
413
+ unit_amount: 1000,
414
+ }
415
+ ]
416
+ end
417
+ }.must_raise Stripe::InvalidConfigurationError
418
+ end
419
+
420
+ it 'raises a Stripe::InvalidConfigurationError when billing tiers is not an array' do
421
+ lambda {
422
+ Stripe.price "Bad Tiers".to_sym do |price|
423
+ price.name = 'Acme as a service BAD TIERS'
424
+ price.constant_name = 'BAD_TIERS'
425
+ price.recurring = {
426
+ interval: 'month',
427
+ interval_count: 1,
428
+ usage_type: 'metered',
429
+ aggregate_usage: 'sum'
430
+ }
431
+ price.tiers_mode = 'graduated'
432
+ price.billing_scheme = 'per_unit'
433
+ price.tiers = {
434
+ unit_amount: 1500,
435
+ up_to: 10
436
+ }
437
+ end
438
+ }.must_raise Stripe::InvalidConfigurationError
439
+ end
440
+ end
441
+
442
+ describe 'when using a product id' do
443
+ before do
444
+ Stripe::Prices::GOLD.product_id = 'prod_XXXXXXXXXXXXXX'
445
+ Stripe::Prices::GOLD.name = nil
446
+ end
447
+ after do
448
+ Stripe::Prices::GOLD.product_id = nil
449
+ Stripe::Prices::GOLD.name = 'Solid Gold'
450
+ end
451
+
452
+ it 'creates the price online with the product id' do
453
+ Stripe::Price.expects(:create).with(
454
+ :lookup_key => 'gold',
455
+ :nickname => 'gold',
456
+ :currency => 'usd',
457
+ :product => 'prod_XXXXXXXXXXXXXX',
458
+ :unit_amount => 699,
459
+ :recurring => {
460
+ :interval => 'month'
461
+ }
462
+ )
463
+ Stripe::Prices::GOLD.put!
464
+ end
465
+ end
466
+ end
467
+
468
+ describe 'when it is already present on stripe.com' do
469
+ before do
470
+ Stripe::Prices::GOLD.product_id = nil
471
+ Stripe::Price.stubs(:list).returns(Stripe::Price.construct_from(
472
+ data: [{
473
+ :lookup_key => 'gold',
474
+ :product => 'prod_XXXXXXXXXXXXXX'
475
+ }]))
476
+ end
477
+ after do
478
+ Stripe::Prices::GOLD.product_id = nil
479
+ end
480
+
481
+
482
+ it 'is a no-op on put!' do
483
+ Stripe::Price.expects(:create).never
484
+ Stripe::Prices::GOLD.put!
485
+ end
486
+
487
+ it 'transfers lookup key on reset!' do
488
+ Stripe::Price.expects(:create).with(
489
+ :lookup_key => 'gold',
490
+ :transfer_lookup_key => true,
491
+ :nickname => 'gold',
492
+ :currency => 'usd',
493
+ :product => 'prod_XXXXXXXXXXXXXX',
494
+ :unit_amount => 699,
495
+ :recurring => {
496
+ :interval => 'month'
497
+ }
498
+ )
499
+
500
+ Stripe::Prices::GOLD.reset!
501
+ end
502
+ end
503
+ end
504
+ end
505
+
506
+ describe 'with missing mandatory values' do
507
+ it 'raises an exception after configuring it' do
508
+ _(-> { Stripe.price(:bad) {} }).must_raise Stripe::InvalidConfigurationError
509
+ end
510
+ end
511
+
512
+ describe 'with custom constant name' do
513
+ before do
514
+ Stripe.price "Lite price".to_sym do |price|
515
+ price.name = 'Acme as a service LITE'
516
+ price.constant_name = 'LITE_PRICE'
517
+ price.unit_amount = 699
518
+ price.recurring = {
519
+ interval: 'month',
520
+ interval_count: 3,
521
+ usage_type: 'metered',
522
+ aggregate_usage: 'sum'
523
+ }
524
+ price.metadata = {:number_of_awesome_things => 5}
525
+ price.statement_descriptor = 'Acme Lite'
526
+ price.active = true
527
+ price.nickname = 'lite'
528
+ price.billing_scheme = 'per_unit'
529
+ price.tiers_mode = 'graduated'
530
+ end
531
+ end
532
+
533
+ after { Stripe::Prices.send(:remove_const, :LITE_PRICE) }
534
+
535
+ it 'is accessible via upcased constant_name' do
536
+ _(Stripe::Prices::LITE_PRICE).wont_be_nil
537
+ end
538
+
539
+ it 'is accessible via collection' do
540
+ _(Stripe::Prices.all).must_include Stripe::Prices::LITE_PRICE
541
+ end
542
+
543
+ it 'is accessible via hash lookup (symbol/string agnostic)' do
544
+ _(Stripe::Prices[:lite_price]).must_equal Stripe::Prices::LITE_PRICE
545
+ _(Stripe::Prices['lite_price']).must_equal Stripe::Prices::LITE_PRICE
546
+ end
547
+
548
+ describe 'constant name validation' do
549
+ it 'should be invalid when providing a constant name that can not be used for Ruby constant' do
550
+ _(lambda {
551
+ Stripe.price "Lite price".to_sym do |price|
552
+ price.name = 'Acme as a service LITE'
553
+ price.constant_name = 'LITE PRICE'
554
+ price.unit_amount = 999
555
+ price.recurring = {
556
+ interval: 'month'
557
+ }
558
+ end
559
+ }).must_raise Stripe::InvalidConfigurationError
560
+ end
561
+ end
562
+
563
+ describe 'uploading' do
564
+ include FixtureLoader
565
+
566
+ describe 'when none exists on stripe.com' do
567
+ before do
568
+ Stripe::Price.stubs(:list).returns(Stripe::Price.construct_from(data: []))
569
+
570
+ stub_request(:get, "https://api.stripe.com/v1/prices").
571
+ with(headers: { 'Authorization'=>'Bearer XYZ',}).
572
+ to_return(status: 200, body: load_request_fixture('stripe_prices.json'))
573
+ end
574
+
575
+ it 'creates the price online' do
576
+ Stripe::Price.expects(:create).with(
577
+ :lookup_key => "Solid Gold",
578
+ :nickname => "Solid Gold",
579
+ :currency => 'usd',
580
+ :product_data => {
581
+ :name => 'Solid Gold',
582
+ :statement_descriptor => nil
583
+ },
584
+ :unit_amount => 699,
585
+ :recurring => {
586
+ :interval => 'month'
587
+ }
588
+ )
589
+ Stripe::Prices::SOLID_GOLD.put!
590
+ end
591
+ end
592
+ end
593
+ end
594
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stripe-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Charles Lowell
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2020-10-18 00:00:00.000000000 Z
13
+ date: 2020-12-06 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rails
@@ -90,6 +90,7 @@ files:
90
90
  - lib/generators/stripe/install_generator.rb
91
91
  - lib/generators/templates/coupons.rb
92
92
  - lib/generators/templates/plans.rb
93
+ - lib/generators/templates/prices.rb
93
94
  - lib/generators/templates/products.rb
94
95
  - lib/stripe-rails.rb
95
96
  - lib/stripe/billing_tier.rb
@@ -100,6 +101,7 @@ files:
100
101
  - lib/stripe/current_api_version.rb
101
102
  - lib/stripe/engine.rb
102
103
  - lib/stripe/plans.rb
104
+ - lib/stripe/prices.rb
103
105
  - lib/stripe/products.rb
104
106
  - lib/stripe/rails.rb
105
107
  - lib/stripe/rails/tasks.rake
@@ -138,6 +140,7 @@ files:
138
140
  - test/dummy/config/locales/en.yml
139
141
  - test/dummy/config/routes.rb
140
142
  - test/dummy/config/stripe/plans.rb
143
+ - test/dummy/config/stripe/prices.rb
141
144
  - test/dummy/lib/assets/.gitkeep
142
145
  - test/dummy/lib/dummy/module_with_callbacks.rb
143
146
  - test/dummy/log/.gitkeep
@@ -153,9 +156,11 @@ files:
153
156
  - test/fixtures/stripe_plans.json
154
157
  - test/fixtures/stripe_plans_headers.json
155
158
  - test/fixtures/stripe_plans_headers_2017.json
159
+ - test/fixtures/stripe_prices.json
156
160
  - test/invoice.json
157
161
  - test/javascript_helper_spec.rb
158
162
  - test/plan_builder_spec.rb
163
+ - test/price_builder_spec.rb
159
164
  - test/product_builder_spec.rb
160
165
  - test/spec_helper.rb
161
166
  - test/stripe_initializers_spec.rb
@@ -182,7 +187,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
182
187
  - !ruby/object:Gem::Version
183
188
  version: '0'
184
189
  requirements: []
185
- rubygems_version: 3.1.2
190
+ rubygems_version: 3.1.4
186
191
  signing_key:
187
192
  specification_version: 4
188
193
  summary: A gem to integrate stripe into your rails app
@@ -219,6 +224,7 @@ test_files:
219
224
  - test/dummy/config/locales/en.yml
220
225
  - test/dummy/config/routes.rb
221
226
  - test/dummy/config/stripe/plans.rb
227
+ - test/dummy/config/stripe/prices.rb
222
228
  - test/dummy/lib/assets/.gitkeep
223
229
  - test/dummy/lib/dummy/module_with_callbacks.rb
224
230
  - test/dummy/log/.gitkeep
@@ -234,9 +240,11 @@ test_files:
234
240
  - test/fixtures/stripe_plans.json
235
241
  - test/fixtures/stripe_plans_headers.json
236
242
  - test/fixtures/stripe_plans_headers_2017.json
243
+ - test/fixtures/stripe_prices.json
237
244
  - test/invoice.json
238
245
  - test/javascript_helper_spec.rb
239
246
  - test/plan_builder_spec.rb
247
+ - test/price_builder_spec.rb
240
248
  - test/product_builder_spec.rb
241
249
  - test/spec_helper.rb
242
250
  - test/stripe_initializers_spec.rb