reji 1.0.0 → 1.1.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 +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +73 -0
- data/.rubocop_todo.yml +31 -0
- data/Appraisals +2 -0
- data/Gemfile +1 -1
- data/README.md +41 -17
- data/Rakefile +8 -2
- data/app/controllers/reji/payment_controller.rb +4 -4
- data/app/controllers/reji/webhook_controller.rb +51 -62
- data/app/views/payment.html.erb +4 -4
- data/app/views/receipt.html.erb +16 -16
- data/config/routes.rb +2 -0
- data/gemfiles/rails_5.0.gemfile +9 -9
- data/gemfiles/rails_5.1.gemfile +7 -9
- data/gemfiles/rails_5.2.gemfile +7 -9
- data/gemfiles/rails_6.0.gemfile +7 -9
- data/lib/generators/reji/install/install_generator.rb +20 -24
- data/lib/generators/reji/install/templates/reji.rb +2 -2
- data/lib/reji.rb +12 -8
- data/lib/reji/concerns/manages_customer.rb +25 -29
- data/lib/reji/concerns/manages_invoices.rb +37 -44
- data/lib/reji/concerns/manages_payment_methods.rb +45 -62
- data/lib/reji/concerns/manages_subscriptions.rb +13 -13
- data/lib/reji/concerns/performs_charges.rb +7 -7
- data/lib/reji/concerns/prorates.rb +1 -1
- data/lib/reji/configuration.rb +2 -2
- data/lib/reji/engine.rb +2 -0
- data/lib/reji/errors.rb +9 -9
- data/lib/reji/invoice.rb +57 -56
- data/lib/reji/invoice_line_item.rb +21 -23
- data/lib/reji/payment.rb +9 -5
- data/lib/reji/payment_method.rb +8 -4
- data/lib/reji/subscription.rb +165 -183
- data/lib/reji/subscription_builder.rb +41 -49
- data/lib/reji/subscription_item.rb +26 -26
- data/lib/reji/tax.rb +8 -10
- data/lib/reji/version.rb +1 -1
- data/reji.gemspec +5 -4
- data/spec/dummy/app/models/user.rb +3 -7
- data/spec/dummy/application.rb +3 -7
- data/spec/dummy/db/schema.rb +3 -4
- data/spec/feature/charges_spec.rb +1 -1
- data/spec/feature/customer_spec.rb +1 -1
- data/spec/feature/invoices_spec.rb +6 -6
- data/spec/feature/multiplan_subscriptions_spec.rb +51 -53
- data/spec/feature/payment_methods_spec.rb +25 -25
- data/spec/feature/pending_updates_spec.rb +26 -26
- data/spec/feature/subscriptions_spec.rb +78 -78
- data/spec/feature/webhooks_spec.rb +72 -72
- data/spec/spec_helper.rb +2 -2
- data/spec/support/feature_helpers.rb +6 -12
- data/spec/unit/customer_spec.rb +13 -13
- data/spec/unit/invoice_line_item_spec.rb +12 -14
- data/spec/unit/invoice_spec.rb +7 -9
- data/spec/unit/payment_spec.rb +3 -3
- data/spec/unit/subscription_spec.rb +29 -30
- metadata +26 -11
- data/Gemfile.lock +0 -133
data/lib/reji/payment.rb
CHANGED
@@ -8,7 +8,7 @@ module Reji
|
|
8
8
|
|
9
9
|
# Get the total amount that will be paid.
|
10
10
|
def amount
|
11
|
-
Reji.format_amount(
|
11
|
+
Reji.format_amount(raw_amount, @payment_intent.currency)
|
12
12
|
end
|
13
13
|
|
14
14
|
# Get the raw total amount that will be paid.
|
@@ -32,20 +32,20 @@ module Reji
|
|
32
32
|
end
|
33
33
|
|
34
34
|
# Determine if the payment was cancelled.
|
35
|
-
def
|
35
|
+
def cancelled?
|
36
36
|
@payment_intent.status == 'canceled'
|
37
37
|
end
|
38
38
|
|
39
39
|
# Determine if the payment was successful.
|
40
|
-
def
|
40
|
+
def succeeded?
|
41
41
|
@payment_intent.status == 'succeeded'
|
42
42
|
end
|
43
43
|
|
44
44
|
# Validate if the payment intent was successful and throw an exception if not.
|
45
45
|
def validate
|
46
|
-
raise Reji::PaymentFailureError
|
46
|
+
raise Reji::PaymentFailureError.invalid_payment_method(self) if requires_payment_method
|
47
47
|
|
48
|
-
raise Reji::PaymentActionRequiredError
|
48
|
+
raise Reji::PaymentActionRequiredError.incomplete(self) if requires_action
|
49
49
|
end
|
50
50
|
|
51
51
|
# The Stripe PaymentIntent instance.
|
@@ -57,5 +57,9 @@ module Reji
|
|
57
57
|
def method_missing(key)
|
58
58
|
@payment_intent[key]
|
59
59
|
end
|
60
|
+
|
61
|
+
def respond_to_missing?(method_name, include_private = false)
|
62
|
+
super
|
63
|
+
end
|
60
64
|
end
|
61
65
|
end
|
data/lib/reji/payment_method.rb
CHANGED
@@ -3,7 +3,9 @@
|
|
3
3
|
module Reji
|
4
4
|
class PaymentMethod
|
5
5
|
def initialize(owner, payment_method)
|
6
|
-
|
6
|
+
if owner.stripe_id != payment_method.customer
|
7
|
+
raise Reji::InvalidPaymentMethodError.invalid_owner(payment_method, owner)
|
8
|
+
end
|
7
9
|
|
8
10
|
@owner = owner
|
9
11
|
@payment_method = payment_method
|
@@ -15,9 +17,7 @@ module Reji
|
|
15
17
|
end
|
16
18
|
|
17
19
|
# Get the Stripe model instance.
|
18
|
-
|
19
|
-
@owner
|
20
|
-
end
|
20
|
+
attr_reader :owner
|
21
21
|
|
22
22
|
# Get the Stripe PaymentMethod instance.
|
23
23
|
def as_stripe_payment_method
|
@@ -28,5 +28,9 @@ module Reji
|
|
28
28
|
def method_missing(key)
|
29
29
|
@payment_method[key]
|
30
30
|
end
|
31
|
+
|
32
|
+
def respond_to_missing?(method_name, include_private = false)
|
33
|
+
super
|
34
|
+
end
|
31
35
|
end
|
32
36
|
end
|
data/lib/reji/subscription.rb
CHANGED
@@ -10,8 +10,8 @@ module Reji
|
|
10
10
|
|
11
11
|
scope :incomplete, -> { where(stripe_status: 'incomplete') }
|
12
12
|
scope :past_due, -> { where(stripe_status: 'past_due') }
|
13
|
-
scope :active,
|
14
|
-
query =
|
13
|
+
scope :active, lambda {
|
14
|
+
query = where(ends_at: nil).or(on_grace_period)
|
15
15
|
.where('stripe_status != ?', 'incomplete')
|
16
16
|
.where('stripe_status != ?', 'incomplete_expired')
|
17
17
|
.where('stripe_status != ?', 'unpaid')
|
@@ -24,171 +24,171 @@ module Reji
|
|
24
24
|
scope :cancelled, -> { where.not(ends_at: nil) }
|
25
25
|
scope :not_cancelled, -> { where(ends_at: nil) }
|
26
26
|
scope :ended, -> { cancelled.not_on_grace_period }
|
27
|
-
scope :on_trial, -> { where.not(trial_ends_at: nil).where('trial_ends_at > ?', Time.
|
28
|
-
scope :not_on_trial, -> { where(trial_ends_at: nil).or(where('trial_ends_at <= ?', Time.
|
29
|
-
scope :on_grace_period, -> { where.not(ends_at: nil).where('ends_at > ?', Time.
|
30
|
-
scope :not_on_grace_period, -> { where(ends_at: nil).or(where('ends_at <= ?', Time.
|
27
|
+
scope :on_trial, -> { where.not(trial_ends_at: nil).where('trial_ends_at > ?', Time.current) }
|
28
|
+
scope :not_on_trial, -> { where(trial_ends_at: nil).or(where('trial_ends_at <= ?', Time.current)) }
|
29
|
+
scope :on_grace_period, -> { where.not(ends_at: nil).where('ends_at > ?', Time.current) }
|
30
|
+
scope :not_on_grace_period, -> { where(ends_at: nil).or(where('ends_at <= ?', Time.current)) }
|
31
31
|
|
32
32
|
# The date on which the billing cycle should be anchored.
|
33
33
|
@billing_cycle_anchor = nil
|
34
34
|
|
35
35
|
# Get the user that owns the subscription.
|
36
36
|
def user
|
37
|
-
|
37
|
+
owner
|
38
38
|
end
|
39
39
|
|
40
40
|
# Determine if the subscription has multiple plans.
|
41
|
-
def
|
42
|
-
|
41
|
+
def multiple_plans?
|
42
|
+
stripe_plan.nil?
|
43
43
|
end
|
44
44
|
|
45
45
|
# Determine if the subscription has a single plan.
|
46
|
-
def
|
47
|
-
!
|
46
|
+
def single_plan?
|
47
|
+
!multiple_plans?
|
48
48
|
end
|
49
49
|
|
50
50
|
# Determine if the subscription has a specific plan.
|
51
|
-
def
|
52
|
-
return
|
51
|
+
def plan?(plan)
|
52
|
+
return items.any? { |item| item.stripe_plan == plan } if multiple_plans?
|
53
53
|
|
54
|
-
|
54
|
+
stripe_plan == plan
|
55
55
|
end
|
56
56
|
|
57
57
|
# Get the subscription item for the given plan.
|
58
58
|
def find_item_or_fail(plan)
|
59
|
-
|
59
|
+
items.where(stripe_plan: plan).first
|
60
60
|
end
|
61
61
|
|
62
62
|
# Determine if the subscription is active, on trial, or within its grace period.
|
63
63
|
def valid
|
64
|
-
|
64
|
+
active || on_trial || on_grace_period
|
65
65
|
end
|
66
66
|
|
67
67
|
# Determine if the subscription is incomplete.
|
68
68
|
def incomplete
|
69
|
-
|
69
|
+
stripe_status == 'incomplete'
|
70
70
|
end
|
71
71
|
|
72
72
|
# Determine if the subscription is past due.
|
73
73
|
def past_due
|
74
|
-
|
74
|
+
stripe_status == 'past_due'
|
75
75
|
end
|
76
76
|
|
77
77
|
# Determine if the subscription is active.
|
78
78
|
def active
|
79
|
-
(
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
79
|
+
(ends_at.nil? || on_grace_period) &&
|
80
|
+
stripe_status != 'incomplete' &&
|
81
|
+
stripe_status != 'incomplete_expired' &&
|
82
|
+
stripe_status != 'unpaid' &&
|
83
|
+
(!Reji.deactivate_past_due || stripe_status != 'past_due')
|
84
84
|
end
|
85
85
|
|
86
86
|
# Sync the Stripe status of the subscription.
|
87
87
|
def sync_stripe_status
|
88
|
-
subscription =
|
88
|
+
subscription = as_stripe_subscription
|
89
89
|
|
90
|
-
|
90
|
+
update({ stripe_status: subscription.status })
|
91
91
|
end
|
92
92
|
|
93
93
|
# Determine if the subscription is recurring and not on trial.
|
94
94
|
def recurring
|
95
|
-
!
|
95
|
+
!on_trial && !cancelled
|
96
96
|
end
|
97
97
|
|
98
98
|
# Determine if the subscription is no longer active.
|
99
99
|
def cancelled
|
100
|
-
!
|
100
|
+
!ends_at.nil?
|
101
101
|
end
|
102
102
|
|
103
103
|
# Determine if the subscription has ended and the grace period has expired.
|
104
104
|
def ended
|
105
|
-
!!
|
105
|
+
!!(cancelled && !on_grace_period)
|
106
106
|
end
|
107
107
|
|
108
108
|
# Determine if the subscription is within its trial period.
|
109
109
|
def on_trial
|
110
|
-
!!
|
110
|
+
!!(trial_ends_at && trial_ends_at.future?)
|
111
111
|
end
|
112
112
|
|
113
113
|
# Determine if the subscription is within its grace period after cancellation.
|
114
114
|
def on_grace_period
|
115
|
-
!!
|
115
|
+
!!(ends_at && ends_at.future?)
|
116
116
|
end
|
117
117
|
|
118
118
|
# Increment the quantity of the subscription.
|
119
119
|
def increment_quantity(count = 1, plan = nil)
|
120
|
-
|
120
|
+
guard_against_incomplete
|
121
121
|
|
122
122
|
if plan
|
123
|
-
|
124
|
-
.set_proration_behavior(
|
123
|
+
find_item_or_fail(plan)
|
124
|
+
.set_proration_behavior(proration_behavior)
|
125
125
|
.increment_quantity(count)
|
126
126
|
|
127
127
|
return self
|
128
128
|
end
|
129
129
|
|
130
|
-
|
130
|
+
guard_against_multiple_plans
|
131
131
|
|
132
|
-
|
132
|
+
update_quantity(quantity + count, plan)
|
133
133
|
end
|
134
134
|
|
135
135
|
# Increment the quantity of the subscription, and invoice immediately.
|
136
136
|
def increment_and_invoice(count = 1, plan = nil)
|
137
|
-
|
137
|
+
guard_against_incomplete
|
138
138
|
|
139
|
-
|
139
|
+
always_invoice
|
140
140
|
|
141
141
|
if plan
|
142
|
-
|
143
|
-
.set_proration_behavior(
|
142
|
+
find_item_or_fail(plan)
|
143
|
+
.set_proration_behavior(proration_behavior)
|
144
144
|
.increment_quantity(count)
|
145
145
|
|
146
146
|
return self
|
147
147
|
end
|
148
148
|
|
149
|
-
|
149
|
+
guard_against_multiple_plans
|
150
150
|
|
151
|
-
|
151
|
+
increment_quantity(count, plan)
|
152
152
|
end
|
153
153
|
|
154
154
|
# Decrement the quantity of the subscription.
|
155
155
|
def decrement_quantity(count = 1, plan = nil)
|
156
|
-
|
156
|
+
guard_against_incomplete
|
157
157
|
|
158
158
|
if plan
|
159
|
-
|
160
|
-
.set_proration_behavior(
|
159
|
+
find_item_or_fail(plan)
|
160
|
+
.set_proration_behavior(proration_behavior)
|
161
161
|
.decrement_quantity(count)
|
162
162
|
|
163
163
|
return self
|
164
164
|
end
|
165
165
|
|
166
|
-
|
166
|
+
guard_against_multiple_plans
|
167
167
|
|
168
|
-
|
168
|
+
update_quantity([1, quantity - count].max, plan)
|
169
169
|
end
|
170
170
|
|
171
171
|
# Update the quantity of the subscription.
|
172
172
|
def update_quantity(quantity, plan = nil)
|
173
|
-
|
173
|
+
guard_against_incomplete
|
174
174
|
|
175
175
|
if plan
|
176
|
-
|
177
|
-
.set_proration_behavior(
|
176
|
+
find_item_or_fail(plan)
|
177
|
+
.set_proration_behavior(proration_behavior)
|
178
178
|
.update_quantity(quantity)
|
179
179
|
|
180
180
|
return self
|
181
181
|
end
|
182
182
|
|
183
|
-
|
183
|
+
guard_against_multiple_plans
|
184
184
|
|
185
|
-
stripe_subscription =
|
185
|
+
stripe_subscription = as_stripe_subscription
|
186
186
|
stripe_subscription.quantity = quantity
|
187
|
-
stripe_subscription.payment_behavior =
|
188
|
-
stripe_subscription.proration_behavior =
|
187
|
+
stripe_subscription.payment_behavior = payment_behavior
|
188
|
+
stripe_subscription.proration_behavior = proration_behavior
|
189
189
|
stripe_subscription.save
|
190
190
|
|
191
|
-
|
191
|
+
update(quantity: quantity)
|
192
192
|
|
193
193
|
self
|
194
194
|
end
|
@@ -209,13 +209,13 @@ module Reji
|
|
209
209
|
|
210
210
|
# Extend an existing subscription's trial period.
|
211
211
|
def extend_trial(date)
|
212
|
-
raise ArgumentError
|
212
|
+
raise ArgumentError, "Extending a subscription's trial requires a date in the future." unless date.future?
|
213
213
|
|
214
|
-
subscription =
|
214
|
+
subscription = as_stripe_subscription
|
215
215
|
subscription.trial_end = date.to_i
|
216
216
|
subscription.save
|
217
217
|
|
218
|
-
|
218
|
+
update(trial_ends_at: date)
|
219
219
|
|
220
220
|
self
|
221
221
|
end
|
@@ -224,25 +224,23 @@ module Reji
|
|
224
224
|
def swap(plans, options = {})
|
225
225
|
plans = [plans] unless plans.instance_of? Array
|
226
226
|
|
227
|
-
raise ArgumentError
|
227
|
+
raise ArgumentError, 'Please provide at least one plan when swapping.' if plans.empty?
|
228
228
|
|
229
|
-
|
229
|
+
guard_against_incomplete
|
230
230
|
|
231
|
-
items =
|
232
|
-
self.parse_swap_plans(plans)
|
233
|
-
)
|
231
|
+
items = merge_items_that_should_be_deleted_during_swap(parse_swap_plans(plans))
|
234
232
|
|
235
|
-
stripe_subscription = Stripe::Subscription
|
236
|
-
|
237
|
-
|
238
|
-
|
233
|
+
stripe_subscription = Stripe::Subscription.update(
|
234
|
+
stripe_id,
|
235
|
+
get_swap_options(items, options),
|
236
|
+
owner.stripe_options
|
239
237
|
)
|
240
238
|
|
241
|
-
|
242
|
-
:
|
243
|
-
:
|
244
|
-
:
|
245
|
-
:
|
239
|
+
update({
|
240
|
+
stripe_status: stripe_subscription.status,
|
241
|
+
stripe_plan: stripe_subscription.plan ? stripe_subscription.plan.id : nil,
|
242
|
+
quantity: stripe_subscription.quantity,
|
243
|
+
ends_at: nil,
|
246
244
|
})
|
247
245
|
|
248
246
|
stripe_subscription.items.each do |item|
|
@@ -255,48 +253,46 @@ module Reji
|
|
255
253
|
# Delete items that aren't attached to the subscription anymore...
|
256
254
|
self.items.where('stripe_plan NOT IN (?)', items.values.pluck(:plan).compact).destroy_all
|
257
255
|
|
258
|
-
if
|
259
|
-
Payment.new(stripe_subscription.latest_invoice.payment_intent).validate
|
260
|
-
end
|
256
|
+
Payment.new(stripe_subscription.latest_invoice.payment_intent).validate if incomplete_payment?
|
261
257
|
|
262
258
|
self
|
263
259
|
end
|
264
260
|
|
265
261
|
# Swap the subscription to new Stripe plans, and invoice immediately.
|
266
262
|
def swap_and_invoice(plans, options = {})
|
267
|
-
|
263
|
+
always_invoice
|
268
264
|
|
269
|
-
|
265
|
+
swap(plans, options)
|
270
266
|
end
|
271
267
|
|
272
268
|
# Add a new Stripe plan to the subscription.
|
273
269
|
def add_plan(plan, quantity = 1, options = {})
|
274
|
-
|
270
|
+
guard_against_incomplete
|
275
271
|
|
276
|
-
if
|
277
|
-
raise Reji::SubscriptionUpdateFailureError
|
272
|
+
if items.any? { |item| item.stripe_plan == plan }
|
273
|
+
raise Reji::SubscriptionUpdateFailureError.duplicate_plan(self, plan)
|
278
274
|
end
|
279
275
|
|
280
|
-
subscription =
|
276
|
+
subscription = as_stripe_subscription
|
281
277
|
|
282
278
|
item = subscription.items.create({
|
283
|
-
:
|
284
|
-
:
|
285
|
-
:
|
286
|
-
:
|
287
|
-
:proration_behavior
|
279
|
+
plan: plan,
|
280
|
+
quantity: quantity,
|
281
|
+
tax_rates: get_plan_tax_rates_for_payload(plan),
|
282
|
+
payment_behavior: payment_behavior,
|
283
|
+
proration_behavior: proration_behavior,
|
288
284
|
}.merge(options))
|
289
285
|
|
290
|
-
|
291
|
-
:
|
292
|
-
:
|
293
|
-
:
|
286
|
+
items.create({
|
287
|
+
stripe_id: item.id,
|
288
|
+
stripe_plan: plan,
|
289
|
+
quantity: quantity,
|
294
290
|
})
|
295
291
|
|
296
|
-
if
|
297
|
-
|
298
|
-
:
|
299
|
-
:
|
292
|
+
if single_plan?
|
293
|
+
update({
|
294
|
+
stripe_plan: nil,
|
295
|
+
quantity: nil,
|
300
296
|
})
|
301
297
|
end
|
302
298
|
|
@@ -305,29 +301,29 @@ module Reji
|
|
305
301
|
|
306
302
|
# Add a new Stripe plan to the subscription, and invoice immediately.
|
307
303
|
def add_plan_and_invoice(plan, quantity = 1, options = {})
|
308
|
-
|
304
|
+
always_invoice
|
309
305
|
|
310
|
-
|
306
|
+
add_plan(plan, quantity, options)
|
311
307
|
end
|
312
308
|
|
313
309
|
# Remove a Stripe plan from the subscription.
|
314
310
|
def remove_plan(plan)
|
315
|
-
raise Reji::SubscriptionUpdateFailureError
|
311
|
+
raise Reji::SubscriptionUpdateFailureError.cannot_delete_last_plan(self) if single_plan?
|
316
312
|
|
317
|
-
item =
|
313
|
+
item = find_item_or_fail(plan)
|
318
314
|
|
319
315
|
item.as_stripe_subscription_item.delete({
|
320
|
-
:proration_behavior
|
316
|
+
proration_behavior: proration_behavior,
|
321
317
|
})
|
322
318
|
|
323
|
-
|
319
|
+
items.where(stripe_plan: plan).destroy_all
|
324
320
|
|
325
|
-
if
|
326
|
-
item =
|
321
|
+
if items.count < 2
|
322
|
+
item = items.first
|
327
323
|
|
328
|
-
|
329
|
-
:
|
330
|
-
:
|
324
|
+
update({
|
325
|
+
stripe_plan: item.stripe_plan,
|
326
|
+
quantity: quantity,
|
331
327
|
})
|
332
328
|
end
|
333
329
|
|
@@ -336,7 +332,7 @@ module Reji
|
|
336
332
|
|
337
333
|
# Cancel the subscription at the end of the billing period.
|
338
334
|
def cancel
|
339
|
-
subscription =
|
335
|
+
subscription = as_stripe_subscription
|
340
336
|
|
341
337
|
subscription.cancel_at_period_end = true
|
342
338
|
|
@@ -347,70 +343,62 @@ module Reji
|
|
347
343
|
# If the user was on trial, we will set the grace period to end when the trial
|
348
344
|
# would have ended. Otherwise, we'll retrieve the end of the billing period
|
349
345
|
# period and make that the end of the grace period for this current user.
|
350
|
-
|
351
|
-
self.ends_at = self.trial_ends_at
|
352
|
-
else
|
353
|
-
self.ends_at = Time.at(subscription.current_period_end)
|
354
|
-
end
|
346
|
+
self.ends_at = on_trial ? trial_ends_at : Time.zone.at(subscription.current_period_end)
|
355
347
|
|
356
|
-
|
348
|
+
save
|
357
349
|
|
358
350
|
self
|
359
351
|
end
|
360
352
|
|
361
353
|
# Cancel the subscription immediately.
|
362
354
|
def cancel_now
|
363
|
-
|
364
|
-
:
|
355
|
+
as_stripe_subscription.cancel({
|
356
|
+
prorate: proration_behavior == 'create_prorations',
|
365
357
|
})
|
366
358
|
|
367
|
-
|
359
|
+
mark_as_cancelled
|
368
360
|
|
369
361
|
self
|
370
362
|
end
|
371
363
|
|
372
364
|
# Cancel the subscription and invoice immediately.
|
373
365
|
def cancel_now_and_invoice
|
374
|
-
|
375
|
-
:
|
376
|
-
:
|
366
|
+
as_stripe_subscription.cancel({
|
367
|
+
invoice_now: true,
|
368
|
+
prorate: proration_behavior == 'create_prorations',
|
377
369
|
})
|
378
370
|
|
379
|
-
|
371
|
+
mark_as_cancelled
|
380
372
|
|
381
373
|
self
|
382
374
|
end
|
383
375
|
|
384
376
|
# Mark the subscription as cancelled.
|
385
377
|
def mark_as_cancelled
|
386
|
-
|
387
|
-
:
|
388
|
-
:
|
378
|
+
update({
|
379
|
+
stripe_status: 'canceled',
|
380
|
+
ends_at: Time.current,
|
389
381
|
})
|
390
382
|
end
|
391
383
|
|
392
384
|
# Resume the cancelled subscription.
|
393
385
|
def resume
|
394
|
-
raise ArgumentError
|
386
|
+
raise ArgumentError, 'Unable to resume subscription that is not within grace period.' unless on_grace_period
|
395
387
|
|
396
|
-
subscription =
|
388
|
+
subscription = as_stripe_subscription
|
397
389
|
|
398
390
|
subscription.cancel_at_period_end = false
|
399
391
|
|
400
|
-
|
401
|
-
subscription.trial_end = Time.at(self.trial_ends_at).to_i
|
402
|
-
else
|
403
|
-
subscription.trial_end = 'now'
|
404
|
-
end
|
392
|
+
subscription.trial_end = on_trial ? Time.zone.at(trial_ends_at).to_i : 'now'
|
405
393
|
|
406
394
|
subscription = subscription.save
|
407
395
|
|
408
396
|
# Finally, we will remove the ending timestamp from the user's record in the
|
409
397
|
# local database to indicate that the subscription is active again and is
|
410
398
|
# no longer "cancelled". Then we will save this record in the database.
|
411
|
-
|
412
|
-
:
|
413
|
-
:
|
399
|
+
update({
|
400
|
+
stripe_status: subscription.status,
|
401
|
+
ends_at: nil,
|
414
402
|
})
|
415
403
|
|
416
404
|
self
|
@@ -418,51 +406,49 @@ module Reji
|
|
418
406
|
|
419
407
|
# Determine if the subscription has pending updates.
|
420
408
|
def pending
|
421
|
-
!
|
409
|
+
!as_stripe_subscription.pending_update.nil?
|
422
410
|
end
|
423
411
|
|
424
412
|
# Invoice the subscription outside of the regular billing cycle.
|
425
413
|
def invoice(options = {})
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
raise e
|
435
|
-
end
|
414
|
+
user.invoice(options.merge({
|
415
|
+
subscription: stripe_id,
|
416
|
+
}))
|
417
|
+
rescue IncompletePaymentError => e
|
418
|
+
# Set the new Stripe subscription status immediately when payment fails...
|
419
|
+
update(stripe_status: e.payment.invoice.subscription.status)
|
420
|
+
|
421
|
+
raise e
|
436
422
|
end
|
437
423
|
|
438
424
|
# Get the latest invoice for the subscription.
|
439
425
|
def latest_invoice
|
440
|
-
stripe_subscription =
|
426
|
+
stripe_subscription = as_stripe_subscription(['latest_invoice'])
|
441
427
|
|
442
|
-
Invoice.new(
|
428
|
+
Invoice.new(user, stripe_subscription.latest_invoice)
|
443
429
|
end
|
444
430
|
|
445
431
|
# Sync the tax percentage of the user to the subscription.
|
446
432
|
def sync_tax_percentage
|
447
|
-
subscription =
|
433
|
+
subscription = as_stripe_subscription
|
448
434
|
|
449
|
-
subscription.tax_percentage =
|
435
|
+
subscription.tax_percentage = user.tax_percentage
|
450
436
|
|
451
437
|
subscription.save
|
452
438
|
end
|
453
439
|
|
454
440
|
# Sync the tax rates of the user to the subscription.
|
455
441
|
def sync_tax_rates
|
456
|
-
subscription =
|
442
|
+
subscription = as_stripe_subscription
|
457
443
|
|
458
|
-
subscription.default_tax_rates =
|
444
|
+
subscription.default_tax_rates = user.tax_rates
|
459
445
|
|
460
446
|
subscription.save
|
461
447
|
|
462
|
-
|
448
|
+
items.each do |item|
|
463
449
|
stripe_subscription_item = item.as_stripe_subscription_item
|
464
450
|
|
465
|
-
stripe_subscription_item.tax_rates =
|
451
|
+
stripe_subscription_item.tax_rates = get_plan_tax_rates_for_payload(item.stripe_plan)
|
466
452
|
|
467
453
|
stripe_subscription_item.save
|
468
454
|
end
|
@@ -470,21 +456,19 @@ module Reji
|
|
470
456
|
|
471
457
|
# Get the plan tax rates for the Stripe payload.
|
472
458
|
def get_plan_tax_rates_for_payload(plan)
|
473
|
-
tax_rates =
|
459
|
+
tax_rates = user.plan_tax_rates
|
474
460
|
|
475
|
-
|
476
|
-
tax_rates.key?(plan) ? tax_rates[plan] : nil
|
477
|
-
end
|
461
|
+
tax_rates[plan] || nil unless tax_rates.empty?
|
478
462
|
end
|
479
463
|
|
480
464
|
# Determine if the subscription has an incomplete payment.
|
481
|
-
def
|
482
|
-
|
465
|
+
def incomplete_payment?
|
466
|
+
past_due || incomplete
|
483
467
|
end
|
484
468
|
|
485
469
|
# Get the latest payment for a Subscription.
|
486
470
|
def latest_payment
|
487
|
-
payment_intent =
|
471
|
+
payment_intent = as_stripe_subscription(['latest_invoice.payment_intent'])
|
488
472
|
.latest_invoice
|
489
473
|
.payment_intent
|
490
474
|
|
@@ -493,64 +477,62 @@ module Reji
|
|
493
477
|
|
494
478
|
# Make sure a subscription is not incomplete when performing changes.
|
495
479
|
def guard_against_incomplete
|
496
|
-
raise Reji::SubscriptionUpdateFailureError.incomplete_subscription(self) if
|
480
|
+
raise Reji::SubscriptionUpdateFailureError.incomplete_subscription(self) if incomplete
|
497
481
|
end
|
498
482
|
|
499
483
|
# Make sure a plan argument is provided when the subscription is a multi plan subscription.
|
500
484
|
def guard_against_multiple_plans
|
501
|
-
|
485
|
+
return unless multiple_plans?
|
486
|
+
|
487
|
+
raise ArgumentError, 'This method requires a plan argument since the subscription has multiple plans.'
|
502
488
|
end
|
503
489
|
|
504
490
|
# Update the underlying Stripe subscription information for the model.
|
505
491
|
def update_stripe_subscription(options = {})
|
506
492
|
Stripe::Subscription.update(
|
507
|
-
|
493
|
+
stripe_id, options, owner.stripe_options
|
508
494
|
)
|
509
495
|
end
|
510
496
|
|
511
497
|
# Get the subscription as a Stripe subscription object.
|
512
498
|
def as_stripe_subscription(expand = {})
|
513
|
-
Stripe::Subscription
|
514
|
-
{:
|
499
|
+
Stripe::Subscription.retrieve(
|
500
|
+
{ id: stripe_id, expand: expand }, owner.stripe_options
|
515
501
|
)
|
516
502
|
end
|
517
503
|
|
518
|
-
protected
|
519
|
-
|
520
504
|
# Parse the given plans for a swap operation.
|
521
|
-
def parse_swap_plans(plans)
|
522
|
-
plans.map
|
523
|
-
|
524
|
-
:
|
525
|
-
:
|
526
|
-
}]
|
527
|
-
|
505
|
+
protected def parse_swap_plans(plans)
|
506
|
+
plans.map do |plan|
|
507
|
+
[plan, {
|
508
|
+
plan: plan,
|
509
|
+
tax_rates: get_plan_tax_rates_for_payload(plan),
|
510
|
+
},]
|
511
|
+
end.to_h
|
528
512
|
end
|
529
513
|
|
530
514
|
# Merge the items that should be deleted during swap into the given items collection.
|
531
|
-
def merge_items_that_should_be_deleted_during_swap(items)
|
532
|
-
|
515
|
+
protected def merge_items_that_should_be_deleted_during_swap(items)
|
516
|
+
as_stripe_subscription.items.data.each do |stripe_subscription_item|
|
533
517
|
plan = stripe_subscription_item.plan.id
|
534
518
|
|
535
519
|
item = items.key?(plan) ? items[plan] : {}
|
536
520
|
|
537
|
-
if item.empty?
|
538
|
-
item[:deleted] = true
|
539
|
-
end
|
521
|
+
item[:deleted] = true if item.empty?
|
540
522
|
|
541
|
-
items[plan] = item.merge({:
|
523
|
+
items[plan] = item.merge({ id: stripe_subscription_item.id })
|
542
524
|
end
|
543
525
|
|
544
526
|
items
|
545
527
|
end
|
546
528
|
|
547
529
|
# Get the options array for a swap operation.
|
548
|
-
def get_swap_options(items, options)
|
530
|
+
protected def get_swap_options(items, options)
|
549
531
|
payload = {
|
550
|
-
:
|
551
|
-
:
|
552
|
-
:proration_behavior
|
553
|
-
:
|
532
|
+
items: items.values,
|
533
|
+
payment_behavior: payment_behavior,
|
534
|
+
proration_behavior: proration_behavior,
|
535
|
+
expand: ['latest_invoice.payment_intent'],
|
554
536
|
}
|
555
537
|
|
556
538
|
payload[:cancel_at_period_end] = false if payload[:payment_behavior] != 'pending_if_incomplete'
|
@@ -559,7 +541,7 @@ module Reji
|
|
559
541
|
|
560
542
|
payload[:billing_cycle_anchor] = @billing_cycle_anchor unless @billing_cycle_anchor.nil?
|
561
543
|
|
562
|
-
payload[:trial_end] =
|
544
|
+
payload[:trial_end] = on_trial ? trial_ends_at : 'now'
|
563
545
|
|
564
546
|
payload
|
565
547
|
end
|