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
@@ -6,64 +6,61 @@ module Reji
|
|
6
6
|
|
7
7
|
# Add an invoice item to the customer's upcoming invoice.
|
8
8
|
def tab(description, amount, options = {})
|
9
|
-
|
9
|
+
assert_customer_exists
|
10
10
|
|
11
11
|
options = {
|
12
|
-
:
|
13
|
-
:
|
14
|
-
:
|
15
|
-
:
|
12
|
+
customer: stripe_id,
|
13
|
+
amount: amount,
|
14
|
+
currency: preferred_currency,
|
15
|
+
description: description,
|
16
16
|
}.merge(options)
|
17
17
|
|
18
|
-
Stripe::InvoiceItem.create(options,
|
18
|
+
Stripe::InvoiceItem.create(options, stripe_options)
|
19
19
|
end
|
20
20
|
|
21
21
|
# Invoice the customer for the given amount and generate an invoice immediately.
|
22
22
|
def invoice_for(description, amount, tab_options = {}, invoice_options = {})
|
23
|
-
|
23
|
+
tab(description, amount, tab_options)
|
24
24
|
|
25
|
-
|
25
|
+
invoice(invoice_options)
|
26
26
|
end
|
27
27
|
|
28
28
|
# Invoice the billable entity outside of the regular billing cycle.
|
29
29
|
def invoice(options = {})
|
30
|
-
|
31
|
-
|
32
|
-
parameters = options.merge({:customer => self.stripe_id})
|
30
|
+
assert_customer_exists
|
33
31
|
|
34
32
|
begin
|
35
|
-
stripe_invoice = Stripe::Invoice.create(
|
33
|
+
stripe_invoice = Stripe::Invoice.create(options.merge({ customer: stripe_id }), stripe_options)
|
36
34
|
|
37
|
-
|
38
|
-
stripe_invoice
|
39
|
-
|
40
|
-
|
41
|
-
|
35
|
+
stripe_invoice =
|
36
|
+
if stripe_invoice.collection_method == 'charge_automatically'
|
37
|
+
stripe_invoice.pay
|
38
|
+
else
|
39
|
+
stripe_invoice.send_invoice
|
40
|
+
end
|
42
41
|
|
43
42
|
Invoice.new(self, stripe_invoice)
|
44
|
-
rescue Stripe::InvalidRequestError =>
|
43
|
+
rescue Stripe::InvalidRequestError => _e
|
45
44
|
false
|
46
|
-
rescue Stripe::CardError =>
|
47
|
-
|
45
|
+
rescue Stripe::CardError => _e
|
46
|
+
Payment.new(
|
48
47
|
Stripe::PaymentIntent.retrieve(
|
49
|
-
{:
|
50
|
-
|
48
|
+
{ id: stripe_invoice.payment_intent, expand: ['invoice.subscription'] },
|
49
|
+
stripe_options
|
51
50
|
)
|
52
|
-
)
|
53
|
-
|
54
|
-
payment.validate
|
51
|
+
).validate
|
55
52
|
end
|
56
53
|
end
|
57
54
|
|
58
55
|
# Get the entity's upcoming invoice.
|
59
56
|
def upcoming_invoice
|
60
|
-
return unless
|
57
|
+
return unless stripe_id?
|
61
58
|
|
62
59
|
begin
|
63
|
-
stripe_invoice = Stripe::Invoice.upcoming({:
|
60
|
+
stripe_invoice = Stripe::Invoice.upcoming({ customer: stripe_id }, stripe_options)
|
64
61
|
|
65
62
|
Invoice.new(self, stripe_invoice)
|
66
|
-
rescue Stripe::InvalidRequestError =>
|
63
|
+
rescue Stripe::InvalidRequestError => _e
|
67
64
|
#
|
68
65
|
end
|
69
66
|
end
|
@@ -73,8 +70,8 @@ module Reji
|
|
73
70
|
stripe_invoice = nil
|
74
71
|
|
75
72
|
begin
|
76
|
-
stripe_invoice = Stripe::Invoice.retrieve(id,
|
77
|
-
rescue =>
|
73
|
+
stripe_invoice = Stripe::Invoice.retrieve(id, stripe_options)
|
74
|
+
rescue StandardError => _e
|
78
75
|
#
|
79
76
|
end
|
80
77
|
|
@@ -84,9 +81,9 @@ module Reji
|
|
84
81
|
# Find an invoice or throw a 404 or 403 error.
|
85
82
|
def find_invoice_or_fail(id)
|
86
83
|
begin
|
87
|
-
invoice =
|
84
|
+
invoice = find_invoice(id)
|
88
85
|
rescue InvalidInvoiceError => e
|
89
|
-
raise Reji::AccessDeniedHttpError
|
86
|
+
raise Reji::AccessDeniedHttpError, e.message
|
90
87
|
end
|
91
88
|
|
92
89
|
raise ActiveRecord::RecordNotFound if invoice.nil?
|
@@ -96,33 +93,29 @@ module Reji
|
|
96
93
|
|
97
94
|
# Create an invoice download response.
|
98
95
|
def download_invoice(id, data, filename = nil)
|
99
|
-
invoice =
|
96
|
+
invoice = find_invoice_or_fail(id)
|
100
97
|
|
101
98
|
filename ? invoice.download_as(filename, data) : invoice.download(data)
|
102
99
|
end
|
103
100
|
|
104
101
|
# Get a collection of the entity's invoices.
|
105
102
|
def invoices(include_pending = false, parameters = {})
|
106
|
-
return [] unless
|
103
|
+
return [] unless stripe_id?
|
107
104
|
|
108
105
|
invoices = []
|
109
106
|
|
110
|
-
parameters = {:
|
107
|
+
parameters = { limit: 24 }.merge(parameters)
|
111
108
|
|
112
109
|
stripe_invoices = Stripe::Invoice.list(
|
113
|
-
{:
|
114
|
-
|
110
|
+
{ customer: stripe_id }.merge(parameters),
|
111
|
+
stripe_options
|
115
112
|
)
|
116
113
|
|
117
114
|
# Here we will loop through the Stripe invoices and create our own custom Invoice
|
118
115
|
# instances that have more helper methods and are generally more convenient to
|
119
116
|
# work with than the plain Stripe objects are. Then, we'll return the array.
|
120
|
-
|
121
|
-
|
122
|
-
if invoice.paid || include_pending
|
123
|
-
invoices << Invoice.new(self, invoice)
|
124
|
-
end
|
125
|
-
end
|
117
|
+
stripe_invoices&.data&.each do |invoice|
|
118
|
+
invoices << Invoice.new(self, invoice) if invoice.paid || include_pending
|
126
119
|
end
|
127
120
|
|
128
121
|
invoices
|
@@ -130,7 +123,7 @@ module Reji
|
|
130
123
|
|
131
124
|
# Get an array of the entity's invoices.
|
132
125
|
def invoices_include_pending(parameters = {})
|
133
|
-
|
126
|
+
invoices(true, parameters)
|
134
127
|
end
|
135
128
|
end
|
136
129
|
end
|
@@ -6,29 +6,29 @@ module Reji
|
|
6
6
|
|
7
7
|
# Create a new SetupIntent instance.
|
8
8
|
def create_setup_intent(options = {})
|
9
|
-
Stripe::SetupIntent.create(options,
|
9
|
+
Stripe::SetupIntent.create(options, stripe_options)
|
10
10
|
end
|
11
11
|
|
12
12
|
# Determines if the customer currently has a default payment method.
|
13
|
-
def
|
14
|
-
|
13
|
+
def default_payment_method?
|
14
|
+
card_brand.present?
|
15
15
|
end
|
16
16
|
|
17
17
|
# Determines if the customer currently has at least one payment method.
|
18
|
-
def
|
19
|
-
!
|
18
|
+
def payment_method?
|
19
|
+
!payment_methods.empty?
|
20
20
|
end
|
21
21
|
|
22
22
|
# Get a collection of the entity's payment methods.
|
23
23
|
def payment_methods(parameters = {})
|
24
|
-
return [] unless
|
24
|
+
return [] unless stripe_id?
|
25
25
|
|
26
|
-
parameters = {:
|
26
|
+
parameters = { limit: 24 }.merge(parameters)
|
27
27
|
|
28
28
|
# "type" is temporarily required by Stripe...
|
29
29
|
payment_methods = Stripe::PaymentMethod.list(
|
30
|
-
{customer:
|
31
|
-
|
30
|
+
{ customer: stripe_id, type: 'card' }.merge(parameters),
|
31
|
+
stripe_options
|
32
32
|
)
|
33
33
|
|
34
34
|
payment_methods.data.map { |payment_method| PaymentMethod.new(self, payment_method) }
|
@@ -36,13 +36,13 @@ module Reji
|
|
36
36
|
|
37
37
|
# Add a payment method to the customer.
|
38
38
|
def add_payment_method(payment_method)
|
39
|
-
|
39
|
+
assert_customer_exists
|
40
40
|
|
41
|
-
stripe_payment_method =
|
41
|
+
stripe_payment_method = resolve_stripe_payment_method(payment_method)
|
42
42
|
|
43
|
-
if stripe_payment_method.customer !=
|
43
|
+
if stripe_payment_method.customer != stripe_id
|
44
44
|
stripe_payment_method = stripe_payment_method.attach(
|
45
|
-
{:
|
45
|
+
{ customer: stripe_id }, stripe_options
|
46
46
|
)
|
47
47
|
end
|
48
48
|
|
@@ -51,74 +51,64 @@ module Reji
|
|
51
51
|
|
52
52
|
# Remove a payment method from the customer.
|
53
53
|
def remove_payment_method(payment_method)
|
54
|
-
|
54
|
+
assert_customer_exists
|
55
55
|
|
56
|
-
stripe_payment_method =
|
56
|
+
stripe_payment_method = resolve_stripe_payment_method(payment_method)
|
57
57
|
|
58
|
-
return if stripe_payment_method.customer !=
|
58
|
+
return if stripe_payment_method.customer != stripe_id
|
59
59
|
|
60
|
-
customer =
|
60
|
+
customer = as_stripe_customer
|
61
61
|
|
62
62
|
default_payment_method = customer.invoice_settings.default_payment_method
|
63
63
|
|
64
|
-
stripe_payment_method.detach({},
|
64
|
+
stripe_payment_method.detach({}, stripe_options)
|
65
65
|
|
66
66
|
# If the payment method was the default payment method, we'll remove it manually...
|
67
|
-
if stripe_payment_method.id == default_payment_method
|
68
|
-
self.update({
|
69
|
-
:card_brand => nil,
|
70
|
-
:card_last_four => nil,
|
71
|
-
})
|
72
|
-
end
|
67
|
+
update({ card_brand: nil, card_last_four: nil }) if stripe_payment_method.id == default_payment_method
|
73
68
|
end
|
74
69
|
|
75
70
|
# Get the default payment method for the entity.
|
76
71
|
def default_payment_method
|
77
|
-
return unless
|
72
|
+
return unless stripe_id?
|
78
73
|
|
79
74
|
customer = Stripe::Customer.retrieve({
|
80
|
-
:
|
81
|
-
:
|
75
|
+
id: stripe_id,
|
76
|
+
expand: [
|
82
77
|
'invoice_settings.default_payment_method',
|
83
78
|
'default_source',
|
84
|
-
]
|
85
|
-
},
|
86
|
-
|
87
|
-
if customer.invoice_settings.default_payment_method
|
88
|
-
return PaymentMethod.new(
|
89
|
-
self,
|
90
|
-
customer.invoice_settings.default_payment_method
|
91
|
-
)
|
92
|
-
end
|
79
|
+
],
|
80
|
+
}, stripe_options)
|
93
81
|
|
94
82
|
# If we can't find a payment method, try to return a legacy source...
|
95
|
-
customer.default_source
|
83
|
+
return customer.default_source unless customer.invoice_settings.default_payment_method
|
84
|
+
|
85
|
+
PaymentMethod.new(self, customer.invoice_settings.default_payment_method)
|
96
86
|
end
|
97
87
|
|
98
88
|
# Update customer's default payment method.
|
99
89
|
def update_default_payment_method(payment_method)
|
100
|
-
|
90
|
+
assert_customer_exists
|
101
91
|
|
102
|
-
customer =
|
92
|
+
customer = as_stripe_customer
|
103
93
|
|
104
|
-
stripe_payment_method =
|
94
|
+
stripe_payment_method = resolve_stripe_payment_method(payment_method)
|
105
95
|
|
106
96
|
# If the customer already has the payment method as their default, we can bail out
|
107
97
|
# of the call now. We don't need to keep adding the same payment method to this
|
108
98
|
# model's account every single time we go through this specific process call.
|
109
99
|
return if stripe_payment_method.id == customer.invoice_settings.default_payment_method
|
110
100
|
|
111
|
-
payment_method =
|
101
|
+
payment_method = add_payment_method(stripe_payment_method)
|
112
102
|
|
113
|
-
customer.invoice_settings = {:
|
103
|
+
customer.invoice_settings = { default_payment_method: payment_method.id }
|
114
104
|
|
115
105
|
customer.save
|
116
106
|
|
117
107
|
# Next we will get the default payment method for this user so we can update the
|
118
108
|
# payment method details on the record in the database. This will allow us to
|
119
109
|
# show that information on the front-end when updating the payment methods.
|
120
|
-
|
121
|
-
|
110
|
+
fill_payment_method_details(payment_method)
|
111
|
+
save
|
122
112
|
|
123
113
|
payment_method
|
124
114
|
end
|
@@ -129,17 +119,12 @@ module Reji
|
|
129
119
|
|
130
120
|
if default_payment_method
|
131
121
|
if default_payment_method.instance_of? PaymentMethod
|
132
|
-
|
133
|
-
default_payment_method.as_stripe_payment_method
|
134
|
-
).save
|
122
|
+
fill_payment_method_details(default_payment_method.as_stripe_payment_method).save
|
135
123
|
else
|
136
|
-
|
124
|
+
fill_source_details(default_payment_method).save
|
137
125
|
end
|
138
126
|
else
|
139
|
-
|
140
|
-
:card_brand => nil,
|
141
|
-
:card_last_four => nil,
|
142
|
-
})
|
127
|
+
update({ card_brand: nil, card_last_four: nil })
|
143
128
|
end
|
144
129
|
|
145
130
|
self
|
@@ -147,9 +132,9 @@ module Reji
|
|
147
132
|
|
148
133
|
# Deletes the entity's payment methods.
|
149
134
|
def delete_payment_methods
|
150
|
-
|
135
|
+
payment_methods.each(&:delete)
|
151
136
|
|
152
|
-
|
137
|
+
update_default_payment_method_from_stripe
|
153
138
|
end
|
154
139
|
|
155
140
|
# Find a PaymentMethod by ID.
|
@@ -157,18 +142,16 @@ module Reji
|
|
157
142
|
stripe_payment_method = nil
|
158
143
|
|
159
144
|
begin
|
160
|
-
stripe_payment_method =
|
161
|
-
rescue =>
|
145
|
+
stripe_payment_method = resolve_stripe_payment_method(payment_method)
|
146
|
+
rescue StandardError => _e
|
162
147
|
#
|
163
148
|
end
|
164
149
|
|
165
150
|
stripe_payment_method ? PaymentMethod.new(self, stripe_payment_method) : nil
|
166
151
|
end
|
167
152
|
|
168
|
-
protected
|
169
|
-
|
170
153
|
# Fills the model's properties with the payment method from Stripe.
|
171
|
-
def fill_payment_method_details(payment_method)
|
154
|
+
protected def fill_payment_method_details(payment_method)
|
172
155
|
if payment_method.type == 'card'
|
173
156
|
self.card_brand = payment_method.card.brand
|
174
157
|
self.card_last_four = payment_method.card.last4
|
@@ -178,7 +161,7 @@ module Reji
|
|
178
161
|
end
|
179
162
|
|
180
163
|
# Fills the model's properties with the source from Stripe.
|
181
|
-
def fill_source_details(source)
|
164
|
+
protected def fill_source_details(source)
|
182
165
|
if source.instance_of? Stripe::Card
|
183
166
|
self.card_brand = source.brand
|
184
167
|
self.card_last_four = source.last4
|
@@ -191,11 +174,11 @@ module Reji
|
|
191
174
|
end
|
192
175
|
|
193
176
|
# Resolve a PaymentMethod ID to a Stripe PaymentMethod object.
|
194
|
-
def resolve_stripe_payment_method(payment_method)
|
177
|
+
protected def resolve_stripe_payment_method(payment_method)
|
195
178
|
return payment_method if payment_method.instance_of? Stripe::PaymentMethod
|
196
179
|
|
197
180
|
Stripe::PaymentMethod.retrieve(
|
198
|
-
payment_method,
|
181
|
+
payment_method, stripe_options
|
199
182
|
)
|
200
183
|
end
|
201
184
|
end
|
@@ -15,54 +15,54 @@ module Reji
|
|
15
15
|
|
16
16
|
# Determine if the Stripe model is on trial.
|
17
17
|
def on_trial(name = 'default', plan = nil)
|
18
|
-
return true if name == 'default' && plan.nil? &&
|
18
|
+
return true if name == 'default' && plan.nil? && on_generic_trial
|
19
19
|
|
20
20
|
subscription = self.subscription(name)
|
21
21
|
|
22
|
-
return false unless subscription
|
22
|
+
return false unless subscription&.on_trial
|
23
23
|
|
24
|
-
plan ? subscription.
|
24
|
+
plan ? subscription.plan?(plan) : true
|
25
25
|
end
|
26
26
|
|
27
27
|
# Determine if the Stripe model is on a "generic" trial at the model level.
|
28
28
|
def on_generic_trial
|
29
|
-
!!
|
29
|
+
!!trial_ends_at && trial_ends_at.future?
|
30
30
|
end
|
31
31
|
|
32
32
|
# Determine if the Stripe model has a given subscription.
|
33
33
|
def subscribed(name = 'default', plan = nil)
|
34
34
|
subscription = self.subscription(name)
|
35
35
|
|
36
|
-
return false unless subscription
|
36
|
+
return false unless subscription&.valid?
|
37
37
|
|
38
|
-
plan ? subscription.
|
38
|
+
plan ? subscription.plan?(plan) : true
|
39
39
|
end
|
40
40
|
|
41
41
|
# Get a subscription instance by name.
|
42
42
|
def subscription(name = 'default')
|
43
|
-
|
43
|
+
subscriptions
|
44
44
|
.sort_by { |subscription| subscription.created_at.to_i }
|
45
45
|
.reverse
|
46
46
|
.find { |subscription| subscription.name == name }
|
47
47
|
end
|
48
48
|
|
49
49
|
# Determine if the customer's subscription has an incomplete payment.
|
50
|
-
def
|
50
|
+
def incomplete_payment?(name = 'default')
|
51
51
|
subscription = self.subscription(name)
|
52
52
|
|
53
|
-
subscription ? subscription.
|
53
|
+
subscription ? subscription.incomplete_payment? : false
|
54
54
|
end
|
55
55
|
|
56
56
|
# Determine if the Stripe model is actively subscribed to one of the given plans.
|
57
57
|
def subscribed_to_plan(plans, name = 'default')
|
58
58
|
subscription = self.subscription(name)
|
59
59
|
|
60
|
-
return false unless subscription
|
60
|
+
return false unless subscription&.valid?
|
61
61
|
|
62
62
|
plans = [plans] unless plans.instance_of? Array
|
63
63
|
|
64
64
|
plans.each do |plan|
|
65
|
-
return true if subscription.
|
65
|
+
return true if subscription.plan?(plan)
|
66
66
|
end
|
67
67
|
|
68
68
|
false
|
@@ -70,7 +70,7 @@ module Reji
|
|
70
70
|
|
71
71
|
# Determine if the entity has a valid subscription on the given plan.
|
72
72
|
def on_plan(plan)
|
73
|
-
|
73
|
+
subscriptions.any? { |subscription| subscription.valid && subscription.plan?(plan) }
|
74
74
|
end
|
75
75
|
|
76
76
|
# Get the tax percentage to apply to the subscription.
|
@@ -80,7 +80,7 @@ module Reji
|
|
80
80
|
|
81
81
|
# Get the tax rates to apply to the subscription.
|
82
82
|
def tax_rates
|
83
|
-
|
83
|
+
[]
|
84
84
|
end
|
85
85
|
|
86
86
|
# Get the tax rates to apply to individual subscription items.
|