reji 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +14 -0
- data/.gitattributes +4 -0
- data/.gitignore +15 -0
- data/.travis.yml +28 -0
- data/Appraisals +17 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +133 -0
- data/LICENSE +20 -0
- data/README.md +1285 -0
- data/Rakefile +21 -0
- data/app/controllers/reji/payment_controller.rb +31 -0
- data/app/controllers/reji/webhook_controller.rb +170 -0
- data/app/views/payment.html.erb +228 -0
- data/app/views/receipt.html.erb +250 -0
- data/bin/setup +12 -0
- data/config/routes.rb +6 -0
- data/gemfiles/rails_5.0.gemfile +13 -0
- data/gemfiles/rails_5.1.gemfile +13 -0
- data/gemfiles/rails_5.2.gemfile +13 -0
- data/gemfiles/rails_6.0.gemfile +13 -0
- data/lib/generators/reji/install/install_generator.rb +69 -0
- data/lib/generators/reji/install/templates/db/migrate/add_reji_to_users.rb.erb +16 -0
- data/lib/generators/reji/install/templates/db/migrate/create_subscription_items.rb.erb +19 -0
- data/lib/generators/reji/install/templates/db/migrate/create_subscriptions.rb.erb +22 -0
- data/lib/generators/reji/install/templates/reji.rb +36 -0
- data/lib/reji.rb +75 -0
- data/lib/reji/billable.rb +13 -0
- data/lib/reji/concerns/interacts_with_payment_behavior.rb +33 -0
- data/lib/reji/concerns/manages_customer.rb +113 -0
- data/lib/reji/concerns/manages_invoices.rb +136 -0
- data/lib/reji/concerns/manages_payment_methods.rb +202 -0
- data/lib/reji/concerns/manages_subscriptions.rb +91 -0
- data/lib/reji/concerns/performs_charges.rb +36 -0
- data/lib/reji/concerns/prorates.rb +38 -0
- data/lib/reji/configuration.rb +59 -0
- data/lib/reji/engine.rb +4 -0
- data/lib/reji/errors.rb +66 -0
- data/lib/reji/invoice.rb +243 -0
- data/lib/reji/invoice_line_item.rb +98 -0
- data/lib/reji/payment.rb +61 -0
- data/lib/reji/payment_method.rb +32 -0
- data/lib/reji/subscription.rb +567 -0
- data/lib/reji/subscription_builder.rb +206 -0
- data/lib/reji/subscription_item.rb +97 -0
- data/lib/reji/tax.rb +48 -0
- data/lib/reji/version.rb +5 -0
- data/reji.gemspec +32 -0
- data/spec/dummy/app/models/user.rb +21 -0
- data/spec/dummy/application.rb +53 -0
- data/spec/dummy/config/database.yml +11 -0
- data/spec/dummy/db/schema.rb +40 -0
- data/spec/feature/charges_spec.rb +67 -0
- data/spec/feature/customer_spec.rb +23 -0
- data/spec/feature/invoices_spec.rb +73 -0
- data/spec/feature/multiplan_subscriptions_spec.rb +319 -0
- data/spec/feature/payment_methods_spec.rb +149 -0
- data/spec/feature/pending_updates_spec.rb +77 -0
- data/spec/feature/subscriptions_spec.rb +650 -0
- data/spec/feature/webhooks_spec.rb +162 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/support/feature_helpers.rb +39 -0
- data/spec/unit/customer_spec.rb +54 -0
- data/spec/unit/invoice_line_item_spec.rb +72 -0
- data/spec/unit/invoice_spec.rb +192 -0
- data/spec/unit/payment_spec.rb +33 -0
- data/spec/unit/subscription_spec.rb +103 -0
- metadata +237 -0
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reji
|
4
|
+
module ManagesSubscriptions
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
has_many :subscriptions, -> { order(created_at: :desc) }, class_name: 'Reji::Subscription'
|
9
|
+
end
|
10
|
+
|
11
|
+
# Begin creating a new subscription.
|
12
|
+
def new_subscription(name, plans)
|
13
|
+
SubscriptionBuilder.new(self, name, plans)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Determine if the Stripe model is on trial.
|
17
|
+
def on_trial(name = 'default', plan = nil)
|
18
|
+
return true if name == 'default' && plan.nil? && self.on_generic_trial
|
19
|
+
|
20
|
+
subscription = self.subscription(name)
|
21
|
+
|
22
|
+
return false unless subscription && subscription.on_trial
|
23
|
+
|
24
|
+
plan ? subscription.has_plan(plan) : true
|
25
|
+
end
|
26
|
+
|
27
|
+
# Determine if the Stripe model is on a "generic" trial at the model level.
|
28
|
+
def on_generic_trial
|
29
|
+
!! self.trial_ends_at && self.trial_ends_at.future?
|
30
|
+
end
|
31
|
+
|
32
|
+
# Determine if the Stripe model has a given subscription.
|
33
|
+
def subscribed(name = 'default', plan = nil)
|
34
|
+
subscription = self.subscription(name)
|
35
|
+
|
36
|
+
return false unless subscription && subscription.valid?
|
37
|
+
|
38
|
+
plan ? subscription.has_plan(plan) : true
|
39
|
+
end
|
40
|
+
|
41
|
+
# Get a subscription instance by name.
|
42
|
+
def subscription(name = 'default')
|
43
|
+
self.subscriptions
|
44
|
+
.sort_by { |subscription| subscription.created_at.to_i }
|
45
|
+
.reverse
|
46
|
+
.find { |subscription| subscription.name == name }
|
47
|
+
end
|
48
|
+
|
49
|
+
# Determine if the customer's subscription has an incomplete payment.
|
50
|
+
def has_incomplete_payment(name = 'default')
|
51
|
+
subscription = self.subscription(name)
|
52
|
+
|
53
|
+
subscription ? subscription.has_incomplete_payment : false
|
54
|
+
end
|
55
|
+
|
56
|
+
# Determine if the Stripe model is actively subscribed to one of the given plans.
|
57
|
+
def subscribed_to_plan(plans, name = 'default')
|
58
|
+
subscription = self.subscription(name)
|
59
|
+
|
60
|
+
return false unless subscription && subscription.valid?
|
61
|
+
|
62
|
+
plans = [plans] unless plans.instance_of? Array
|
63
|
+
|
64
|
+
plans.each do |plan|
|
65
|
+
return true if subscription.has_plan(plan)
|
66
|
+
end
|
67
|
+
|
68
|
+
false
|
69
|
+
end
|
70
|
+
|
71
|
+
# Determine if the entity has a valid subscription on the given plan.
|
72
|
+
def on_plan(plan)
|
73
|
+
self.subscriptions.any? { |subscription| subscription.valid && subscription.has_plan(plan) }
|
74
|
+
end
|
75
|
+
|
76
|
+
# Get the tax percentage to apply to the subscription.
|
77
|
+
def tax_percentage
|
78
|
+
0
|
79
|
+
end
|
80
|
+
|
81
|
+
# Get the tax rates to apply to the subscription.
|
82
|
+
def tax_rates
|
83
|
+
{}
|
84
|
+
end
|
85
|
+
|
86
|
+
# Get the tax rates to apply to individual subscription items.
|
87
|
+
def plan_tax_rates
|
88
|
+
{}
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reji
|
4
|
+
module PerformsCharges
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# Make a "one off" charge on the customer for the given amount.
|
8
|
+
def charge(amount, payment_method, options = {})
|
9
|
+
options = {
|
10
|
+
:confirmation_method => 'automatic',
|
11
|
+
:confirm => true,
|
12
|
+
:currency => self.preferred_currency,
|
13
|
+
}.merge(options)
|
14
|
+
|
15
|
+
options[:amount] = amount
|
16
|
+
options[:payment_method] = payment_method
|
17
|
+
options[:customer] = self.stripe_id if self.has_stripe_id
|
18
|
+
|
19
|
+
payment = Payment.new(
|
20
|
+
Stripe::PaymentIntent.create(options, self.stripe_options)
|
21
|
+
)
|
22
|
+
|
23
|
+
payment.validate
|
24
|
+
|
25
|
+
payment
|
26
|
+
end
|
27
|
+
|
28
|
+
# Refund a customer for a charge.
|
29
|
+
def refund(payment_intent, options = {})
|
30
|
+
Stripe::Refund.create(
|
31
|
+
{:payment_intent => payment_intent}.merge(options),
|
32
|
+
self.stripe_options
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reji
|
4
|
+
module Prorates
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# Indicate that the plan change should not be prorated.
|
8
|
+
def no_prorate
|
9
|
+
@proration_behavior = 'none'
|
10
|
+
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
# Indicate that the plan change should be prorated.
|
15
|
+
def prorate
|
16
|
+
@proration_behavior = 'create_prorations'
|
17
|
+
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
# Indicate that the plan change should always be invoiced.
|
22
|
+
def always_invoice
|
23
|
+
@proration_behavior = 'always_invoice'
|
24
|
+
end
|
25
|
+
|
26
|
+
# Set the prorating behavior.
|
27
|
+
def set_proration_behavior(value)
|
28
|
+
@proration_behavior = value
|
29
|
+
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
# Determine the prorating behavior when updating the subscription.
|
34
|
+
def prorate_behavior
|
35
|
+
@proration_behavior ||= 'create_prorations'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reji
|
4
|
+
class Configuration
|
5
|
+
# Stripe Keys
|
6
|
+
#
|
7
|
+
# The Stripe publishable key and secret key give you access to Stripe's
|
8
|
+
# API. The "publishable" key is typically used when interacting with
|
9
|
+
# Stripe.js while the "secret" key accesses private API endpoints.
|
10
|
+
attr_accessor :key
|
11
|
+
attr_accessor :secret
|
12
|
+
|
13
|
+
# Stripe Webhooks
|
14
|
+
#
|
15
|
+
# Your Stripe webhook secret is used to prevent unauthorized requests to
|
16
|
+
# your Stripe webhook handling controllers. The tolerance setting will
|
17
|
+
# check the drift between the current time and the signed request's.
|
18
|
+
attr_accessor :webhook
|
19
|
+
|
20
|
+
# Reji Model
|
21
|
+
#
|
22
|
+
# This is the model in your application that includes the Billable concern
|
23
|
+
# provided by Reji. It will serve as the primary model you use while
|
24
|
+
# interacting with Reji related methods, subscriptions, and so on.
|
25
|
+
attr_accessor :model
|
26
|
+
attr_accessor :model_id
|
27
|
+
|
28
|
+
# Currency
|
29
|
+
#
|
30
|
+
# This is the default currency that will be used when generating charges
|
31
|
+
# from your application. Of course, you are welcome to use any of the
|
32
|
+
# various world currencies that are currently supported via Stripe.
|
33
|
+
attr_accessor :currency
|
34
|
+
|
35
|
+
def initialize
|
36
|
+
@key = ENV['STRIPE_KEY']
|
37
|
+
@secret = ENV['STRIPE_SECRET']
|
38
|
+
@webhook = {
|
39
|
+
:secret => ENV['STRIPE_WEBHOOK_SECRET'],
|
40
|
+
:tolerance => ENV['STRIPE_WEBHOOK_TOLERANCE'] || 300,
|
41
|
+
}
|
42
|
+
@model = ENV['REJI_MODEL'] || 'User'
|
43
|
+
@model_id = ENV['REJI_MODEL_ID'] || 'user_id'
|
44
|
+
@currency = ENV['REJI_CURRENCY'] || 'usd'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.configuration
|
49
|
+
@configuration ||= Configuration.new
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.configuration=(config)
|
53
|
+
@configuration = config
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.configure
|
57
|
+
yield(configuration)
|
58
|
+
end
|
59
|
+
end
|
data/lib/reji/engine.rb
ADDED
data/lib/reji/errors.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reji
|
4
|
+
class IncompletePaymentError < StandardError
|
5
|
+
attr_accessor :payment
|
6
|
+
|
7
|
+
def initialize(payment, message = '')
|
8
|
+
super(message)
|
9
|
+
|
10
|
+
@payment = payment
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class PaymentActionRequiredError < IncompletePaymentError
|
15
|
+
def self.incomplete(payment)
|
16
|
+
self.new(payment, 'The payment attempt failed because additional action is required before it can be completed.')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class PaymentFailureError < IncompletePaymentError
|
21
|
+
def self.invalid_payment_method(payment)
|
22
|
+
self.new(payment, 'The payment attempt failed because of an invalid payment method.')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class CustomerAlreadyCreatedError < StandardError
|
27
|
+
def self.exists(owner)
|
28
|
+
self.new("#{owner.class.name} is already a Stripe customer with ID #{owner.stripe_id}.")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class InvalidCustomerError < StandardError
|
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.")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class InvalidPaymentMethodError < StandardError
|
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}`.")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class InvalidInvoiceError < StandardError
|
45
|
+
def self.invalid_owner(invoice, owner)
|
46
|
+
self.new("The invoice `#{invoice.id}` does not belong to this customer `#{owner.stripe_id}`.")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class SubscriptionUpdateFailureError < StandardError
|
51
|
+
def self.incomplete_subscription(subscription)
|
52
|
+
self.new("The subscription \"#{subscription.stripe_id}\" cannot be updated because its payment is incomplete.")
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.duplicate_plan(subscription, plan)
|
56
|
+
self.new("The plan \"#{plan}\" is already attached to subscription \"#{subscription.stripe_id}\".")
|
57
|
+
end
|
58
|
+
|
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.")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class AccessDeniedHttpError < StandardError
|
65
|
+
end
|
66
|
+
end
|
data/lib/reji/invoice.rb
ADDED
@@ -0,0 +1,243 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'wicked_pdf'
|
4
|
+
|
5
|
+
module Reji
|
6
|
+
class Invoice
|
7
|
+
def initialize(owner, invoice)
|
8
|
+
raise Reji::InvalidInvoiceError.invalid_owner(invoice, owner) if owner.stripe_id != invoice.customer
|
9
|
+
|
10
|
+
@owner = owner
|
11
|
+
@invoice = invoice
|
12
|
+
@items = nil
|
13
|
+
@taxes = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
# Get a date for the invoice.
|
17
|
+
def date
|
18
|
+
Time.at(@invoice.created ? @invoice.created : @invoice.date)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Get the total amount that was paid (or will be paid).
|
22
|
+
def total
|
23
|
+
Reji.format_amount(self.raw_total)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Get the raw total amount that was paid (or will be paid).
|
27
|
+
def raw_total
|
28
|
+
@invoice.total + self.raw_starting_balance
|
29
|
+
end
|
30
|
+
|
31
|
+
# Get the total of the invoice (before discounts).
|
32
|
+
def subtotal
|
33
|
+
Reji.format_amount(@invoice[:subtotal])
|
34
|
+
end
|
35
|
+
|
36
|
+
# Determine if the account had a starting balance.
|
37
|
+
def has_starting_balance
|
38
|
+
self.raw_starting_balance < 0
|
39
|
+
end
|
40
|
+
|
41
|
+
# Get the starting balance for the invoice.
|
42
|
+
def starting_balance
|
43
|
+
Reji.format_amount(self.raw_starting_balance)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Get the raw starting balance for the invoice.
|
47
|
+
def raw_starting_balance
|
48
|
+
@invoice[:starting_balance] ? @invoice[:starting_balance] : 0
|
49
|
+
end
|
50
|
+
|
51
|
+
# Determine if the invoice has a discount.
|
52
|
+
def has_discount
|
53
|
+
self.raw_discount > 0
|
54
|
+
end
|
55
|
+
|
56
|
+
# Get the discount amount.
|
57
|
+
def discount
|
58
|
+
self.format_amount(self.raw_discount)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Get the raw discount amount.
|
62
|
+
def raw_discount
|
63
|
+
return 0 unless @invoice.discount
|
64
|
+
|
65
|
+
return (@invoice.subtotal * (self.percent_off / 100)).round.to_i if self.discount_is_percentage
|
66
|
+
|
67
|
+
self.raw_amount_off
|
68
|
+
end
|
69
|
+
|
70
|
+
# Get the coupon code applied to the invoice.
|
71
|
+
def coupon
|
72
|
+
return @invoice[:discount][:coupon][:id] if @invoice[:discount]
|
73
|
+
end
|
74
|
+
|
75
|
+
# Determine if the discount is a percentage.
|
76
|
+
def discount_is_percentage
|
77
|
+
return false unless @invoice[:discount]
|
78
|
+
|
79
|
+
!! @invoice[:discount][:coupon][:percent_off]
|
80
|
+
end
|
81
|
+
|
82
|
+
# Get the discount percentage for the invoice.
|
83
|
+
def percent_off
|
84
|
+
self.coupon ? @invoice[:discount][:coupon][:percent_off] : 0
|
85
|
+
end
|
86
|
+
|
87
|
+
# Get the discount amount for the invoice.
|
88
|
+
def amount_off
|
89
|
+
self.format_amount(self.raw_amount_off)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Get the raw discount amount for the invoice.
|
93
|
+
def raw_amount_off
|
94
|
+
amount_off = @invoice[:discount][:coupon][:amount_off]
|
95
|
+
|
96
|
+
amount_off ? amount_off : 0
|
97
|
+
end
|
98
|
+
|
99
|
+
# Get the total tax amount.
|
100
|
+
def tax
|
101
|
+
self.format_amount(@invoice.tax)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Determine if the invoice has tax applied.
|
105
|
+
def has_tax
|
106
|
+
line_items = self.invoice_items + self.subscriptions
|
107
|
+
|
108
|
+
line_items.any? { |item| item.has_tax_rates }
|
109
|
+
end
|
110
|
+
|
111
|
+
# Get the taxes applied to the invoice.
|
112
|
+
def taxes
|
113
|
+
return @taxes unless @taxes.nil?
|
114
|
+
|
115
|
+
self.refresh_with_expanded_tax_rates
|
116
|
+
|
117
|
+
@taxes = @invoice.total_tax_amounts
|
118
|
+
.sort_by(&:inclusive)
|
119
|
+
.reverse
|
120
|
+
.map { |tax_amount| Tax.new(tax_amount.amount, @invoice.currency, tax_amount.tax_rate) }
|
121
|
+
|
122
|
+
@taxes
|
123
|
+
end
|
124
|
+
|
125
|
+
# Determine if the customer is not exempted from taxes.
|
126
|
+
def is_not_tax_exempt
|
127
|
+
@invoice[:customer_tax_exempt] == 'none'
|
128
|
+
end
|
129
|
+
|
130
|
+
# Determine if the customer is exempted from taxes.
|
131
|
+
def is_tax_exempt
|
132
|
+
@invoice[:customer_tax_exempt] == 'exempt'
|
133
|
+
end
|
134
|
+
|
135
|
+
# Determine if reverse charge applies to the customer.
|
136
|
+
def reverse_charge_applies
|
137
|
+
@invoice[:customer_tax_exempt] == 'reverse'
|
138
|
+
end
|
139
|
+
|
140
|
+
# Get all of the "invoice item" line items.
|
141
|
+
def invoice_items
|
142
|
+
self.invoice_line_items_by_type('invoiceitem')
|
143
|
+
end
|
144
|
+
|
145
|
+
# Get all of the "subscription" line items.
|
146
|
+
def subscriptions
|
147
|
+
self.invoice_line_items_by_type('subscription')
|
148
|
+
end
|
149
|
+
|
150
|
+
# Get all of the invoice items by a given type.
|
151
|
+
def invoice_line_items_by_type(type)
|
152
|
+
if @items.nil?
|
153
|
+
self.refresh_with_expanded_tax_rates
|
154
|
+
|
155
|
+
@items = @invoice.lines.auto_paging_each
|
156
|
+
end
|
157
|
+
|
158
|
+
@items
|
159
|
+
.select { |item| item.type == type }
|
160
|
+
.map { |item| InvoiceLineItem.new(self, item) }
|
161
|
+
end
|
162
|
+
|
163
|
+
# Get the View instance for the invoice.
|
164
|
+
def view(data)
|
165
|
+
ActionController::Base.new.render_to_string(
|
166
|
+
template: 'receipt',
|
167
|
+
locals: data.merge({
|
168
|
+
invoice: self,
|
169
|
+
owner: self.owner,
|
170
|
+
user: self.owner,
|
171
|
+
})
|
172
|
+
)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Capture the invoice as a PDF and return the raw bytes.
|
176
|
+
def pdf(data)
|
177
|
+
WickedPdf.new.pdf_from_string(self.view(data))
|
178
|
+
end
|
179
|
+
|
180
|
+
# Create an invoice download response.
|
181
|
+
def download(data)
|
182
|
+
filename = "#{data[:product]}_#{self.date.month}_#{self.date.year}"
|
183
|
+
|
184
|
+
self.download_as(filename, data)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Create an invoice download response with a specific filename.
|
188
|
+
def download_as(filename, data)
|
189
|
+
{:data => self.pdf(data), :filename => filename}
|
190
|
+
end
|
191
|
+
|
192
|
+
# Void the Stripe invoice.
|
193
|
+
def void(options = {})
|
194
|
+
@invoice = @invoice.void_invoice(options, @owner.stripe_options)
|
195
|
+
|
196
|
+
self
|
197
|
+
end
|
198
|
+
|
199
|
+
# Get the Stripe model instance.
|
200
|
+
def owner
|
201
|
+
@owner
|
202
|
+
end
|
203
|
+
|
204
|
+
# Get the Stripe invoice instance.
|
205
|
+
def as_stripe_invoice
|
206
|
+
@invoice
|
207
|
+
end
|
208
|
+
|
209
|
+
# Dynamically get values from the Stripe invoice.
|
210
|
+
def method_missing(key)
|
211
|
+
@invoice[key]
|
212
|
+
end
|
213
|
+
|
214
|
+
protected
|
215
|
+
|
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
|
236
|
+
end
|
237
|
+
|
238
|
+
# Format the given amount into a displayable currency.
|
239
|
+
def format_amount(amount)
|
240
|
+
Reji.format_amount(amount, @invoice.currency)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|