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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +73 -0
  4. data/.rubocop_todo.yml +31 -0
  5. data/Appraisals +2 -0
  6. data/Gemfile +1 -1
  7. data/README.md +41 -17
  8. data/Rakefile +8 -2
  9. data/app/controllers/reji/payment_controller.rb +4 -4
  10. data/app/controllers/reji/webhook_controller.rb +51 -62
  11. data/app/views/payment.html.erb +4 -4
  12. data/app/views/receipt.html.erb +16 -16
  13. data/config/routes.rb +2 -0
  14. data/gemfiles/rails_5.0.gemfile +9 -9
  15. data/gemfiles/rails_5.1.gemfile +7 -9
  16. data/gemfiles/rails_5.2.gemfile +7 -9
  17. data/gemfiles/rails_6.0.gemfile +7 -9
  18. data/lib/generators/reji/install/install_generator.rb +20 -24
  19. data/lib/generators/reji/install/templates/reji.rb +2 -2
  20. data/lib/reji.rb +12 -8
  21. data/lib/reji/concerns/manages_customer.rb +25 -29
  22. data/lib/reji/concerns/manages_invoices.rb +37 -44
  23. data/lib/reji/concerns/manages_payment_methods.rb +45 -62
  24. data/lib/reji/concerns/manages_subscriptions.rb +13 -13
  25. data/lib/reji/concerns/performs_charges.rb +7 -7
  26. data/lib/reji/concerns/prorates.rb +1 -1
  27. data/lib/reji/configuration.rb +2 -2
  28. data/lib/reji/engine.rb +2 -0
  29. data/lib/reji/errors.rb +9 -9
  30. data/lib/reji/invoice.rb +57 -56
  31. data/lib/reji/invoice_line_item.rb +21 -23
  32. data/lib/reji/payment.rb +9 -5
  33. data/lib/reji/payment_method.rb +8 -4
  34. data/lib/reji/subscription.rb +165 -183
  35. data/lib/reji/subscription_builder.rb +41 -49
  36. data/lib/reji/subscription_item.rb +26 -26
  37. data/lib/reji/tax.rb +8 -10
  38. data/lib/reji/version.rb +1 -1
  39. data/reji.gemspec +5 -4
  40. data/spec/dummy/app/models/user.rb +3 -7
  41. data/spec/dummy/application.rb +3 -7
  42. data/spec/dummy/db/schema.rb +3 -4
  43. data/spec/feature/charges_spec.rb +1 -1
  44. data/spec/feature/customer_spec.rb +1 -1
  45. data/spec/feature/invoices_spec.rb +6 -6
  46. data/spec/feature/multiplan_subscriptions_spec.rb +51 -53
  47. data/spec/feature/payment_methods_spec.rb +25 -25
  48. data/spec/feature/pending_updates_spec.rb +26 -26
  49. data/spec/feature/subscriptions_spec.rb +78 -78
  50. data/spec/feature/webhooks_spec.rb +72 -72
  51. data/spec/spec_helper.rb +2 -2
  52. data/spec/support/feature_helpers.rb +6 -12
  53. data/spec/unit/customer_spec.rb +13 -13
  54. data/spec/unit/invoice_line_item_spec.rb +12 -14
  55. data/spec/unit/invoice_spec.rb +7 -9
  56. data/spec/unit/payment_spec.rb +3 -3
  57. data/spec/unit/subscription_spec.rb +29 -30
  58. metadata +26 -11
  59. 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
- :confirmation_method => 'automatic',
11
- :confirm => true,
12
- :currency => self.preferred_currency,
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] = self.stripe_id if self.has_stripe_id
17
+ options[:customer] = stripe_id if stripe_id?
18
18
 
19
19
  payment = Payment.new(
20
- Stripe::PaymentIntent.create(options, self.stripe_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 => payment_intent}.merge(options),
32
- self.stripe_options
31
+ { payment_intent: payment_intent }.merge(options),
32
+ stripe_options
33
33
  )
34
34
  end
35
35
  end
@@ -31,7 +31,7 @@ module Reji
31
31
  end
32
32
 
33
33
  # Determine the prorating behavior when updating the subscription.
34
- def prorate_behavior
34
+ def proration_behavior
35
35
  @proration_behavior ||= 'create_prorations'
36
36
  end
37
37
  end
@@ -36,8 +36,8 @@ module Reji
36
36
  @key = ENV['STRIPE_KEY']
37
37
  @secret = ENV['STRIPE_SECRET']
38
38
  @webhook = {
39
- :secret => ENV['STRIPE_WEBHOOK_SECRET'],
40
- :tolerance => ENV['STRIPE_WEBHOOK_TOLERANCE'] || 300,
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'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Reji
2
4
  class Engine < ::Rails::Engine
3
5
  end
@@ -13,51 +13,51 @@ module Reji
13
13
 
14
14
  class PaymentActionRequiredError < IncompletePaymentError
15
15
  def self.incomplete(payment)
16
- self.new(payment, 'The payment attempt failed because additional action is required before it can be completed.')
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
- self.new(payment, 'The payment attempt failed because of an invalid payment method.')
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
- self.new("#{owner.class.name} is already a Stripe customer with ID #{owner.stripe_id}.")
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
- self.new("#{owner.class.name} is not a Stripe customer yet. See the create_as_stripe_customer method.")
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
- self.new("The payment method `#{payment_method.id}` does not belong to this customer `#{owner.stripe_id}`.")
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
- self.new("The invoice `#{invoice.id}` does not belong to this customer `#{owner.stripe_id}`.")
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
- self.new("The subscription \"#{subscription.stripe_id}\" cannot be updated because its payment is incomplete.")
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
- self.new("The plan \"#{plan}\" is already attached to subscription \"#{subscription.stripe_id}\".")
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
- self.new("The plan on subscription \"#{subscription.stripe_id}\" cannot be removed because it is the last one.")
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
 
@@ -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 ? @invoice.created : @invoice.date)
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(self.raw_total)
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 + self.raw_starting_balance
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 has_starting_balance
38
- self.raw_starting_balance < 0
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(self.raw_starting_balance)
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] ? @invoice[:starting_balance] : 0
48
+ @invoice[:starting_balance] || 0
49
49
  end
50
50
 
51
51
  # Determine if the invoice has a discount.
52
- def has_discount
53
- self.raw_discount > 0
52
+ def discount?
53
+ raw_discount > 0
54
54
  end
55
55
 
56
56
  # Get the discount amount.
57
57
  def discount
58
- self.format_amount(self.raw_discount)
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 * (self.percent_off / 100)).round.to_i if self.discount_is_percentage
65
+ return (@invoice.subtotal * (percent_off / 100)).round.to_i if discount_is_percentage
66
66
 
67
- self.raw_amount_off
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
- !! @invoice[:discount][:coupon][:percent_off]
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
- self.coupon ? @invoice[:discount][:coupon][:percent_off] : 0
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
- self.format_amount(self.raw_amount_off)
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 ? amount_off : 0
96
+ amount_off || 0
97
97
  end
98
98
 
99
99
  # Get the total tax amount.
100
100
  def tax
101
- self.format_amount(@invoice.tax)
101
+ format_amount(@invoice.tax)
102
102
  end
103
103
 
104
104
  # Determine if the invoice has tax applied.
105
- def has_tax
106
- line_items = self.invoice_items + self.subscriptions
105
+ def tax?
106
+ line_items = invoice_items + subscriptions
107
107
 
108
- line_items.any? { |item| item.has_tax_rates }
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
- self.refresh_with_expanded_tax_rates
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 is_not_tax_exempt
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 is_tax_exempt
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
- self.invoice_line_items_by_type('invoiceitem')
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
- self.invoice_line_items_by_type('subscription')
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
- self.refresh_with_expanded_tax_rates
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: self.owner,
170
- user: self.owner,
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(self.view(data))
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]}_#{self.date.month}_#{self.date.year}"
182
+ filename = "#{data[:product]}_#{date.month}_#{date.year}"
183
183
 
184
- self.download_as(filename, data)
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
- {:data => self.pdf(data), :filename => filename}
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
- def owner
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
- protected
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
- if @invoice.id
219
- @invoice = Stripe::Invoice.retrieve({
220
- :id => @invoice.id,
221
- :expand => [
222
- 'lines.data.tax_amounts.tax_rate',
223
- 'total_tax_amounts.tax_rate',
224
- ],
225
- }, @owner.stripe_options)
226
- else
227
- # If no invoice ID is present then assume this is the customer's upcoming invoice...
228
- @invoice = Stripe::Invoice.upcoming({
229
- :customer => @owner.stripe_id,
230
- :expand => [
231
- 'lines.data.tax_amounts.tax_rate',
232
- 'total_tax_amounts.tax_rate',
233
- ],
234
- }, @owner.stripe_options)
235
- end
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
- self.format_amount(@item.amount)
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 has_both_inclusive_and_exclusive_tax
17
- self.inclusive_tax_percentage > 0 && self.exclusive_tax_percentage > 0
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.is_not_tax_exempt ?
23
- self.calculate_tax_percentage_by_tax_amount(true) :
24
- self.calculate_tax_percentage_by_tax_rate(true)
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.is_not_tax_exempt ?
30
- self.calculate_tax_percentage_by_tax_amount(false) :
31
- self.calculate_tax_percentage_by_tax_rate(false)
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 has_tax_rates
36
- @invoice.is_not_tax_exempt ?
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
- self.is_subscription ? Time.at(@item.period.start).strftime('%b %d, %Y') : nil
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
- self.is_subscription ? Time.at(@item.period.end).strftime('%b %d, %Y') : nil
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 is_subscription
50
+ def subscription?
53
51
  @item.type == 'subscription'
54
52
  end
55
53
 
56
54
  # Get the Stripe model instance.
57
- def invoice
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
- protected
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