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
@@ -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
|