stripe-rails 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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