reji 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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