reji 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -7,17 +7,17 @@ module Reji
|
|
7
7
|
# Make a "one off" charge on the customer for the given amount.
|
8
8
|
def charge(amount, payment_method, options = {})
|
9
9
|
options = {
|
10
|
-
:
|
11
|
-
:
|
12
|
-
:
|
10
|
+
confirmation_method: 'automatic',
|
11
|
+
confirm: true,
|
12
|
+
currency: preferred_currency,
|
13
13
|
}.merge(options)
|
14
14
|
|
15
15
|
options[:amount] = amount
|
16
16
|
options[:payment_method] = payment_method
|
17
|
-
options[:customer] =
|
17
|
+
options[:customer] = stripe_id if stripe_id?
|
18
18
|
|
19
19
|
payment = Payment.new(
|
20
|
-
Stripe::PaymentIntent.create(options,
|
20
|
+
Stripe::PaymentIntent.create(options, stripe_options)
|
21
21
|
)
|
22
22
|
|
23
23
|
payment.validate
|
@@ -28,8 +28,8 @@ module Reji
|
|
28
28
|
# Refund a customer for a charge.
|
29
29
|
def refund(payment_intent, options = {})
|
30
30
|
Stripe::Refund.create(
|
31
|
-
{:payment_intent
|
32
|
-
|
31
|
+
{ payment_intent: payment_intent }.merge(options),
|
32
|
+
stripe_options
|
33
33
|
)
|
34
34
|
end
|
35
35
|
end
|
data/lib/reji/configuration.rb
CHANGED
@@ -36,8 +36,8 @@ module Reji
|
|
36
36
|
@key = ENV['STRIPE_KEY']
|
37
37
|
@secret = ENV['STRIPE_SECRET']
|
38
38
|
@webhook = {
|
39
|
-
:
|
40
|
-
:
|
39
|
+
secret: ENV['STRIPE_WEBHOOK_SECRET'],
|
40
|
+
tolerance: ENV['STRIPE_WEBHOOK_TOLERANCE'] || 300,
|
41
41
|
}
|
42
42
|
@model = ENV['REJI_MODEL'] || 'User'
|
43
43
|
@model_id = ENV['REJI_MODEL_ID'] || 'user_id'
|
data/lib/reji/engine.rb
CHANGED
data/lib/reji/errors.rb
CHANGED
@@ -13,51 +13,51 @@ module Reji
|
|
13
13
|
|
14
14
|
class PaymentActionRequiredError < IncompletePaymentError
|
15
15
|
def self.incomplete(payment)
|
16
|
-
|
16
|
+
new(payment, 'The payment attempt failed because additional action is required before it can be completed.')
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
20
|
class PaymentFailureError < IncompletePaymentError
|
21
21
|
def self.invalid_payment_method(payment)
|
22
|
-
|
22
|
+
new(payment, 'The payment attempt failed because of an invalid payment method.')
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
26
|
class CustomerAlreadyCreatedError < StandardError
|
27
27
|
def self.exists(owner)
|
28
|
-
|
28
|
+
new("#{owner.class.name} is already a Stripe customer with ID #{owner.stripe_id}.")
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
32
|
class InvalidCustomerError < StandardError
|
33
33
|
def self.not_yet_created(owner)
|
34
|
-
|
34
|
+
new("#{owner.class.name} is not a Stripe customer yet. See the create_as_stripe_customer method.")
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
38
38
|
class InvalidPaymentMethodError < StandardError
|
39
39
|
def self.invalid_owner(payment_method, owner)
|
40
|
-
|
40
|
+
new("The payment method `#{payment_method.id}` does not belong to this customer `#{owner.stripe_id}`.")
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
44
|
class InvalidInvoiceError < StandardError
|
45
45
|
def self.invalid_owner(invoice, owner)
|
46
|
-
|
46
|
+
new("The invoice `#{invoice.id}` does not belong to this customer `#{owner.stripe_id}`.")
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
50
50
|
class SubscriptionUpdateFailureError < StandardError
|
51
51
|
def self.incomplete_subscription(subscription)
|
52
|
-
|
52
|
+
new("The subscription \"#{subscription.stripe_id}\" cannot be updated because its payment is incomplete.")
|
53
53
|
end
|
54
54
|
|
55
55
|
def self.duplicate_plan(subscription, plan)
|
56
|
-
|
56
|
+
new("The plan \"#{plan}\" is already attached to subscription \"#{subscription.stripe_id}\".")
|
57
57
|
end
|
58
58
|
|
59
59
|
def self.cannot_delete_last_plan(subscription)
|
60
|
-
|
60
|
+
new("The plan on subscription \"#{subscription.stripe_id}\" cannot be removed because it is the last one.")
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
data/lib/reji/invoice.rb
CHANGED
@@ -15,17 +15,17 @@ module Reji
|
|
15
15
|
|
16
16
|
# Get a date for the invoice.
|
17
17
|
def date
|
18
|
-
Time.at(@invoice.created
|
18
|
+
Time.zone.at(@invoice.created || @invoice.date)
|
19
19
|
end
|
20
20
|
|
21
21
|
# Get the total amount that was paid (or will be paid).
|
22
22
|
def total
|
23
|
-
Reji.format_amount(
|
23
|
+
Reji.format_amount(raw_total)
|
24
24
|
end
|
25
25
|
|
26
26
|
# Get the raw total amount that was paid (or will be paid).
|
27
27
|
def raw_total
|
28
|
-
@invoice.total +
|
28
|
+
@invoice.total + raw_starting_balance
|
29
29
|
end
|
30
30
|
|
31
31
|
# Get the total of the invoice (before discounts).
|
@@ -34,37 +34,37 @@ module Reji
|
|
34
34
|
end
|
35
35
|
|
36
36
|
# Determine if the account had a starting balance.
|
37
|
-
def
|
38
|
-
|
37
|
+
def starting_balance?
|
38
|
+
raw_starting_balance < 0
|
39
39
|
end
|
40
40
|
|
41
41
|
# Get the starting balance for the invoice.
|
42
42
|
def starting_balance
|
43
|
-
Reji.format_amount(
|
43
|
+
Reji.format_amount(raw_starting_balance)
|
44
44
|
end
|
45
45
|
|
46
46
|
# Get the raw starting balance for the invoice.
|
47
47
|
def raw_starting_balance
|
48
|
-
@invoice[:starting_balance]
|
48
|
+
@invoice[:starting_balance] || 0
|
49
49
|
end
|
50
50
|
|
51
51
|
# Determine if the invoice has a discount.
|
52
|
-
def
|
53
|
-
|
52
|
+
def discount?
|
53
|
+
raw_discount > 0
|
54
54
|
end
|
55
55
|
|
56
56
|
# Get the discount amount.
|
57
57
|
def discount
|
58
|
-
|
58
|
+
format_amount(raw_discount)
|
59
59
|
end
|
60
60
|
|
61
61
|
# Get the raw discount amount.
|
62
62
|
def raw_discount
|
63
63
|
return 0 unless @invoice.discount
|
64
64
|
|
65
|
-
return (@invoice.subtotal * (
|
65
|
+
return (@invoice.subtotal * (percent_off / 100)).round.to_i if discount_is_percentage
|
66
66
|
|
67
|
-
|
67
|
+
raw_amount_off
|
68
68
|
end
|
69
69
|
|
70
70
|
# Get the coupon code applied to the invoice.
|
@@ -76,43 +76,43 @@ module Reji
|
|
76
76
|
def discount_is_percentage
|
77
77
|
return false unless @invoice[:discount]
|
78
78
|
|
79
|
-
|
79
|
+
!!@invoice[:discount][:coupon][:percent_off]
|
80
80
|
end
|
81
81
|
|
82
82
|
# Get the discount percentage for the invoice.
|
83
83
|
def percent_off
|
84
|
-
|
84
|
+
coupon ? @invoice[:discount][:coupon][:percent_off] : 0
|
85
85
|
end
|
86
86
|
|
87
87
|
# Get the discount amount for the invoice.
|
88
88
|
def amount_off
|
89
|
-
|
89
|
+
format_amount(raw_amount_off)
|
90
90
|
end
|
91
91
|
|
92
92
|
# Get the raw discount amount for the invoice.
|
93
93
|
def raw_amount_off
|
94
94
|
amount_off = @invoice[:discount][:coupon][:amount_off]
|
95
95
|
|
96
|
-
amount_off
|
96
|
+
amount_off || 0
|
97
97
|
end
|
98
98
|
|
99
99
|
# Get the total tax amount.
|
100
100
|
def tax
|
101
|
-
|
101
|
+
format_amount(@invoice.tax)
|
102
102
|
end
|
103
103
|
|
104
104
|
# Determine if the invoice has tax applied.
|
105
|
-
def
|
106
|
-
line_items =
|
105
|
+
def tax?
|
106
|
+
line_items = invoice_items + subscriptions
|
107
107
|
|
108
|
-
line_items.any?
|
108
|
+
line_items.any?(&:tax_rates?)
|
109
109
|
end
|
110
110
|
|
111
111
|
# Get the taxes applied to the invoice.
|
112
112
|
def taxes
|
113
113
|
return @taxes unless @taxes.nil?
|
114
114
|
|
115
|
-
|
115
|
+
refresh_with_expanded_tax_rates
|
116
116
|
|
117
117
|
@taxes = @invoice.total_tax_amounts
|
118
118
|
.sort_by(&:inclusive)
|
@@ -123,12 +123,12 @@ module Reji
|
|
123
123
|
end
|
124
124
|
|
125
125
|
# Determine if the customer is not exempted from taxes.
|
126
|
-
def
|
126
|
+
def not_tax_exempt?
|
127
127
|
@invoice[:customer_tax_exempt] == 'none'
|
128
128
|
end
|
129
129
|
|
130
130
|
# Determine if the customer is exempted from taxes.
|
131
|
-
def
|
131
|
+
def tax_exempt?
|
132
132
|
@invoice[:customer_tax_exempt] == 'exempt'
|
133
133
|
end
|
134
134
|
|
@@ -139,18 +139,18 @@ module Reji
|
|
139
139
|
|
140
140
|
# Get all of the "invoice item" line items.
|
141
141
|
def invoice_items
|
142
|
-
|
142
|
+
invoice_line_items_by_type('invoiceitem')
|
143
143
|
end
|
144
144
|
|
145
145
|
# Get all of the "subscription" line items.
|
146
146
|
def subscriptions
|
147
|
-
|
147
|
+
invoice_line_items_by_type('subscription')
|
148
148
|
end
|
149
149
|
|
150
150
|
# Get all of the invoice items by a given type.
|
151
151
|
def invoice_line_items_by_type(type)
|
152
152
|
if @items.nil?
|
153
|
-
|
153
|
+
refresh_with_expanded_tax_rates
|
154
154
|
|
155
155
|
@items = @invoice.lines.auto_paging_each
|
156
156
|
end
|
@@ -166,27 +166,27 @@ module Reji
|
|
166
166
|
template: 'receipt',
|
167
167
|
locals: data.merge({
|
168
168
|
invoice: self,
|
169
|
-
owner:
|
170
|
-
user:
|
169
|
+
owner: owner,
|
170
|
+
user: owner,
|
171
171
|
})
|
172
172
|
)
|
173
173
|
end
|
174
174
|
|
175
175
|
# Capture the invoice as a PDF and return the raw bytes.
|
176
176
|
def pdf(data)
|
177
|
-
WickedPdf.new.pdf_from_string(
|
177
|
+
WickedPdf.new.pdf_from_string(view(data))
|
178
178
|
end
|
179
179
|
|
180
180
|
# Create an invoice download response.
|
181
181
|
def download(data)
|
182
|
-
filename = "#{data[:product]}_#{
|
182
|
+
filename = "#{data[:product]}_#{date.month}_#{date.year}"
|
183
183
|
|
184
|
-
|
184
|
+
download_as(filename, data)
|
185
185
|
end
|
186
186
|
|
187
187
|
# Create an invoice download response with a specific filename.
|
188
188
|
def download_as(filename, data)
|
189
|
-
{:
|
189
|
+
{ data: pdf(data), filename: filename }
|
190
190
|
end
|
191
191
|
|
192
192
|
# Void the Stripe invoice.
|
@@ -197,9 +197,7 @@ module Reji
|
|
197
197
|
end
|
198
198
|
|
199
199
|
# Get the Stripe model instance.
|
200
|
-
|
201
|
-
@owner
|
202
|
-
end
|
200
|
+
attr_reader :owner
|
203
201
|
|
204
202
|
# Get the Stripe invoice instance.
|
205
203
|
def as_stripe_invoice
|
@@ -211,32 +209,35 @@ module Reji
|
|
211
209
|
@invoice[key]
|
212
210
|
end
|
213
211
|
|
214
|
-
|
212
|
+
def respond_to_missing?(method_name, include_private = false)
|
213
|
+
super
|
214
|
+
end
|
215
215
|
|
216
216
|
# Refresh the invoice with expanded TaxRate objects.
|
217
|
-
def refresh_with_expanded_tax_rates
|
218
|
-
|
219
|
-
@invoice
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
217
|
+
protected def refresh_with_expanded_tax_rates
|
218
|
+
@invoice =
|
219
|
+
if @invoice.id
|
220
|
+
Stripe::Invoice.retrieve({
|
221
|
+
id: @invoice.id,
|
222
|
+
expand: [
|
223
|
+
'lines.data.tax_amounts.tax_rate',
|
224
|
+
'total_tax_amounts.tax_rate',
|
225
|
+
],
|
226
|
+
}, @owner.stripe_options)
|
227
|
+
else
|
228
|
+
# If no invoice ID is present then assume this is the customer's upcoming invoice...
|
229
|
+
Stripe::Invoice.upcoming({
|
230
|
+
customer: @owner.stripe_id,
|
231
|
+
expand: [
|
232
|
+
'lines.data.tax_amounts.tax_rate',
|
233
|
+
'total_tax_amounts.tax_rate',
|
234
|
+
],
|
235
|
+
}, @owner.stripe_options)
|
236
|
+
end
|
236
237
|
end
|
237
238
|
|
238
239
|
# Format the given amount into a displayable currency.
|
239
|
-
def format_amount(amount)
|
240
|
+
protected def format_amount(amount)
|
240
241
|
Reji.format_amount(amount, @invoice.currency)
|
241
242
|
end
|
242
243
|
end
|
@@ -9,54 +9,50 @@ module Reji
|
|
9
9
|
|
10
10
|
# Get the total for the invoice line item.
|
11
11
|
def total
|
12
|
-
|
12
|
+
format_amount(@item.amount)
|
13
13
|
end
|
14
14
|
|
15
15
|
# Determine if the line item has both inclusive and exclusive tax.
|
16
|
-
def
|
17
|
-
|
16
|
+
def both_inclusive_and_exclusive_tax?
|
17
|
+
inclusive_tax_percentage > 0 && exclusive_tax_percentage > 0
|
18
18
|
end
|
19
19
|
|
20
20
|
# Get the total percentage of the default inclusive tax for the invoice line item.
|
21
21
|
def inclusive_tax_percentage
|
22
|
-
@invoice.
|
23
|
-
|
24
|
-
|
22
|
+
return calculate_tax_percentage_by_tax_amount(true) if @invoice.not_tax_exempt?
|
23
|
+
|
24
|
+
calculate_tax_percentage_by_tax_rate(true)
|
25
25
|
end
|
26
26
|
|
27
27
|
# Get the total percentage of the default exclusive tax for the invoice line item.
|
28
28
|
def exclusive_tax_percentage
|
29
|
-
@invoice.
|
30
|
-
|
31
|
-
|
29
|
+
return calculate_tax_percentage_by_tax_amount(false) if @invoice.not_tax_exempt?
|
30
|
+
|
31
|
+
calculate_tax_percentage_by_tax_rate(false)
|
32
32
|
end
|
33
33
|
|
34
34
|
# Determine if the invoice line item has tax rates.
|
35
|
-
def
|
36
|
-
@invoice.
|
37
|
-
! @item.tax_amounts.empty? :
|
38
|
-
! @item.tax_rates.empty?
|
35
|
+
def tax_rates?
|
36
|
+
@invoice.not_tax_exempt? ? !@item.tax_amounts.empty? : !@item.tax_rates.empty?
|
39
37
|
end
|
40
38
|
|
41
39
|
# Get a human readable date for the start date.
|
42
40
|
def start_date
|
43
|
-
|
41
|
+
subscription? ? Time.zone.at(@item.period.start).strftime('%b %d, %Y') : nil
|
44
42
|
end
|
45
43
|
|
46
44
|
# Get a human readable date for the end date.
|
47
45
|
def end_date
|
48
|
-
|
46
|
+
subscription? ? Time.zone.at(@item.period.end).strftime('%b %d, %Y') : nil
|
49
47
|
end
|
50
48
|
|
51
49
|
# Determine if the invoice line item is for a subscription.
|
52
|
-
def
|
50
|
+
def subscription?
|
53
51
|
@item.type == 'subscription'
|
54
52
|
end
|
55
53
|
|
56
54
|
# Get the Stripe model instance.
|
57
|
-
|
58
|
-
@invoice
|
59
|
-
end
|
55
|
+
attr_reader :invoice
|
60
56
|
|
61
57
|
# Get the underlying Stripe invoice line item.
|
62
58
|
def as_stripe_invoice_line_item
|
@@ -68,10 +64,12 @@ module Reji
|
|
68
64
|
@item[key]
|
69
65
|
end
|
70
66
|
|
71
|
-
|
67
|
+
def respond_to_missing?(method_name, include_private = false)
|
68
|
+
super
|
69
|
+
end
|
72
70
|
|
73
71
|
# Calculate the total tax percentage for either the inclusive or exclusive tax by tax rate.
|
74
|
-
def calculate_tax_percentage_by_tax_rate(inclusive)
|
72
|
+
protected def calculate_tax_percentage_by_tax_rate(inclusive)
|
75
73
|
return 0 if @item[:tax_rates].empty?
|
76
74
|
|
77
75
|
@item.tax_rates
|
@@ -81,7 +79,7 @@ module Reji
|
|
81
79
|
end
|
82
80
|
|
83
81
|
# Calculate the total tax percentage for either the inclusive or exclusive tax by tax amount.
|
84
|
-
def calculate_tax_percentage_by_tax_amount(inclusive)
|
82
|
+
protected def calculate_tax_percentage_by_tax_amount(inclusive)
|
85
83
|
return 0 if @item[:tax_amounts].blank?
|
86
84
|
|
87
85
|
@item.tax_amounts
|
@@ -91,7 +89,7 @@ module Reji
|
|
91
89
|
end
|
92
90
|
|
93
91
|
# Format the given amount into a displayable currency.
|
94
|
-
def format_amount(amount)
|
92
|
+
protected def format_amount(amount)
|
95
93
|
Reji.format_amount(amount, @item.currency)
|
96
94
|
end
|
97
95
|
end
|