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
@@ -9,11 +9,11 @@ module Reji
|
|
9
9
|
def initialize(owner, name, plans = [])
|
10
10
|
@owner = owner
|
11
11
|
@name = name
|
12
|
-
@trial_expires # The date and time the trial will expire.
|
12
|
+
@trial_expires = nil # The date and time the trial will expire.
|
13
13
|
@skip_trial = false # Indicates that the trial should end immediately.
|
14
14
|
@billing_cycle_anchor = nil # The date on which the billing cycle should be anchored.
|
15
|
-
@coupon # The coupon code being applied to the customer.
|
16
|
-
@metadata # The metadata to apply to the subscription.
|
15
|
+
@coupon = nil # The coupon code being applied to the customer.
|
16
|
+
@metadata = nil # The metadata to apply to the subscription.
|
17
17
|
@items = {}
|
18
18
|
|
19
19
|
plans = [plans] unless plans.instance_of? Array
|
@@ -24,11 +24,11 @@ module Reji
|
|
24
24
|
# Set a plan on the subscription builder.
|
25
25
|
def plan(plan, quantity = 1)
|
26
26
|
options = {
|
27
|
-
:
|
28
|
-
:
|
27
|
+
plan: plan,
|
28
|
+
quantity: quantity,
|
29
29
|
}
|
30
30
|
|
31
|
-
tax_rates =
|
31
|
+
tax_rates = get_plan_tax_rates_for_payload(plan)
|
32
32
|
|
33
33
|
options[:tax_rates] = tax_rates if tax_rates
|
34
34
|
|
@@ -40,7 +40,7 @@ module Reji
|
|
40
40
|
# Specify the quantity of a subscription item.
|
41
41
|
def quantity(quantity, plan = nil)
|
42
42
|
if plan.nil?
|
43
|
-
raise ArgumentError
|
43
|
+
raise ArgumentError, 'Plan is required when creating multi-plan subscriptions.' if @items.length > 1
|
44
44
|
|
45
45
|
plan = @items.values[0][:plan]
|
46
46
|
end
|
@@ -50,7 +50,7 @@ module Reji
|
|
50
50
|
|
51
51
|
# Specify the number of days of the trial.
|
52
52
|
def trial_days(trial_days)
|
53
|
-
@trial_expires = Time.
|
53
|
+
@trial_expires = Time.current + trial_days.days
|
54
54
|
|
55
55
|
self
|
56
56
|
end
|
@@ -92,16 +92,14 @@ module Reji
|
|
92
92
|
|
93
93
|
# Add a new Stripe subscription to the Stripe model.
|
94
94
|
def add(customer_options = {}, subscription_options = {})
|
95
|
-
|
95
|
+
create(nil, customer_options, subscription_options)
|
96
96
|
end
|
97
97
|
|
98
98
|
# Create a new Stripe subscription.
|
99
99
|
def create(payment_method = nil, customer_options = {}, subscription_options = {})
|
100
|
-
customer =
|
100
|
+
customer = get_stripe_customer(payment_method, customer_options)
|
101
101
|
|
102
|
-
payload = {:
|
103
|
-
.merge(self.build_payload)
|
104
|
-
.merge(subscription_options)
|
102
|
+
payload = { customer: customer.id }.merge(build_payload).merge(subscription_options)
|
105
103
|
|
106
104
|
stripe_subscription = Stripe::Subscription.create(
|
107
105
|
payload,
|
@@ -109,34 +107,30 @@ module Reji
|
|
109
107
|
)
|
110
108
|
|
111
109
|
subscription = @owner.subscriptions.create({
|
112
|
-
:
|
113
|
-
:
|
114
|
-
:
|
115
|
-
:
|
116
|
-
:
|
117
|
-
:
|
118
|
-
:
|
110
|
+
name: @name,
|
111
|
+
stripe_id: stripe_subscription.id,
|
112
|
+
stripe_status: stripe_subscription.status,
|
113
|
+
stripe_plan: stripe_subscription.plan ? stripe_subscription.plan.id : nil,
|
114
|
+
quantity: stripe_subscription.quantity,
|
115
|
+
trial_ends_at: @skip_trial ? nil : @trial_expires,
|
116
|
+
ends_at: nil,
|
119
117
|
})
|
120
118
|
|
121
119
|
stripe_subscription.items.each do |item|
|
122
120
|
subscription.items.create({
|
123
|
-
:
|
124
|
-
:
|
125
|
-
:
|
121
|
+
stripe_id: item.id,
|
122
|
+
stripe_plan: item.plan.id,
|
123
|
+
quantity: item.quantity,
|
126
124
|
})
|
127
125
|
end
|
128
126
|
|
129
|
-
if subscription.
|
130
|
-
Payment.new(stripe_subscription.latest_invoice.payment_intent).validate
|
131
|
-
end
|
127
|
+
Payment.new(stripe_subscription.latest_invoice.payment_intent).validate if subscription.incomplete_payment?
|
132
128
|
|
133
129
|
subscription
|
134
130
|
end
|
135
131
|
|
136
|
-
protected
|
137
|
-
|
138
132
|
# Get the Stripe customer instance for the current user and payment method.
|
139
|
-
def get_stripe_customer(payment_method = nil, options = {})
|
133
|
+
protected def get_stripe_customer(payment_method = nil, options = {})
|
140
134
|
customer = @owner.create_or_get_stripe_customer(options)
|
141
135
|
|
142
136
|
@owner.update_default_payment_method(payment_method) if payment_method
|
@@ -145,20 +139,20 @@ module Reji
|
|
145
139
|
end
|
146
140
|
|
147
141
|
# Build the payload for subscription creation.
|
148
|
-
def build_payload
|
142
|
+
protected def build_payload
|
149
143
|
payload = {
|
150
|
-
:
|
151
|
-
:
|
152
|
-
:
|
153
|
-
:
|
154
|
-
:
|
155
|
-
:
|
156
|
-
:proration_behavior
|
157
|
-
:
|
158
|
-
:
|
144
|
+
billing_cycle_anchor: @billing_cycle_anchor,
|
145
|
+
coupon: @coupon,
|
146
|
+
expand: ['latest_invoice.payment_intent'],
|
147
|
+
metadata: @metadata,
|
148
|
+
items: @items.values,
|
149
|
+
payment_behavior: payment_behavior,
|
150
|
+
proration_behavior: proration_behavior,
|
151
|
+
trial_end: get_trial_end_for_payload,
|
152
|
+
off_session: true,
|
159
153
|
}
|
160
154
|
|
161
|
-
tax_rates =
|
155
|
+
tax_rates = get_tax_rates_for_payload
|
162
156
|
|
163
157
|
if tax_rates
|
164
158
|
payload[:default_tax_rates] = tax_rates
|
@@ -166,7 +160,7 @@ module Reji
|
|
166
160
|
return payload
|
167
161
|
end
|
168
162
|
|
169
|
-
tax_percentage =
|
163
|
+
tax_percentage = get_tax_percentage_for_payload
|
170
164
|
|
171
165
|
payload[:tax_percent] = tax_percentage if tax_percentage
|
172
166
|
|
@@ -174,33 +168,31 @@ module Reji
|
|
174
168
|
end
|
175
169
|
|
176
170
|
# Get the trial ending date for the Stripe payload.
|
177
|
-
def get_trial_end_for_payload
|
171
|
+
protected def get_trial_end_for_payload
|
178
172
|
return 'now' if @skip_trial
|
179
173
|
|
180
|
-
@trial_expires
|
174
|
+
@trial_expires&.to_i
|
181
175
|
end
|
182
176
|
|
183
177
|
# Get the tax percentage for the Stripe payload.
|
184
|
-
def get_tax_percentage_for_payload
|
178
|
+
protected def get_tax_percentage_for_payload
|
185
179
|
tax_percentage = @owner.tax_percentage
|
186
180
|
|
187
181
|
tax_percentage if tax_percentage > 0
|
188
182
|
end
|
189
183
|
|
190
184
|
# Get the tax rates for the Stripe payload.
|
191
|
-
def get_tax_rates_for_payload
|
185
|
+
protected def get_tax_rates_for_payload
|
192
186
|
tax_rates = @owner.tax_rates
|
193
187
|
|
194
188
|
tax_rates unless tax_rates.empty?
|
195
189
|
end
|
196
190
|
|
197
191
|
# Get the plan tax rates for the Stripe payload.
|
198
|
-
def get_plan_tax_rates_for_payload(plan)
|
192
|
+
protected def get_plan_tax_rates_for_payload(plan)
|
199
193
|
tax_rates = @owner.plan_tax_rates
|
200
194
|
|
201
|
-
unless tax_rates.empty?
|
202
|
-
tax_rates[plan] || nil
|
203
|
-
end
|
195
|
+
tax_rates[plan] || nil unless tax_rates.empty?
|
204
196
|
end
|
205
197
|
end
|
206
198
|
end
|
@@ -9,88 +9,88 @@ module Reji
|
|
9
9
|
|
10
10
|
# Increment the quantity of the subscription item.
|
11
11
|
def increment_quantity(count = 1)
|
12
|
-
|
12
|
+
update_quantity(quantity + count)
|
13
13
|
|
14
14
|
self
|
15
15
|
end
|
16
16
|
|
17
17
|
# Increment the quantity of the subscription item, and invoice immediately.
|
18
18
|
def increment_and_invoice(count = 1)
|
19
|
-
|
19
|
+
always_invoice
|
20
20
|
|
21
|
-
|
21
|
+
increment_quantity(count)
|
22
22
|
|
23
23
|
self
|
24
24
|
end
|
25
25
|
|
26
26
|
# Decrement the quantity of the subscription item.
|
27
27
|
def decrement_quantity(count = 1)
|
28
|
-
|
28
|
+
update_quantity([1, quantity - count].max)
|
29
29
|
|
30
30
|
self
|
31
31
|
end
|
32
32
|
|
33
33
|
# Update the quantity of the subscription item.
|
34
34
|
def update_quantity(quantity)
|
35
|
-
|
35
|
+
subscription.guard_against_incomplete
|
36
36
|
|
37
|
-
stripe_subscription_item =
|
37
|
+
stripe_subscription_item = as_stripe_subscription_item
|
38
38
|
stripe_subscription_item.quantity = quantity
|
39
|
-
stripe_subscription_item.payment_behavior =
|
40
|
-
stripe_subscription_item.proration_behavior =
|
39
|
+
stripe_subscription_item.payment_behavior = payment_behavior
|
40
|
+
stripe_subscription_item.proration_behavior = proration_behavior
|
41
41
|
stripe_subscription_item.save
|
42
42
|
|
43
|
-
|
43
|
+
update(quantity: quantity)
|
44
44
|
|
45
|
-
|
45
|
+
subscription.update(quantity: quantity) if subscription.single_plan?
|
46
46
|
|
47
47
|
self
|
48
48
|
end
|
49
49
|
|
50
50
|
# Swap the subscription item to a new Stripe plan.
|
51
51
|
def swap(plan, options = {})
|
52
|
-
|
52
|
+
subscription.guard_against_incomplete
|
53
53
|
|
54
54
|
options = {
|
55
|
-
:
|
56
|
-
:
|
57
|
-
:
|
58
|
-
:proration_behavior
|
59
|
-
:
|
55
|
+
plan: plan,
|
56
|
+
quantity: quantity,
|
57
|
+
payment_behavior: payment_behavior,
|
58
|
+
proration_behavior: proration_behavior,
|
59
|
+
tax_rates: subscription.get_plan_tax_rates_for_payload(plan),
|
60
60
|
}.merge(options)
|
61
61
|
|
62
|
-
item = Stripe::SubscriptionItem
|
63
|
-
|
62
|
+
item = Stripe::SubscriptionItem.update(
|
63
|
+
stripe_id,
|
64
64
|
options,
|
65
|
-
|
65
|
+
subscription.owner.stripe_options
|
66
66
|
)
|
67
67
|
|
68
|
-
|
68
|
+
update(stripe_plan: plan, quantity: item.quantity)
|
69
69
|
|
70
|
-
|
70
|
+
subscription.update(stripe_plan: plan, quantity: item.quantity) if subscription.single_plan?
|
71
71
|
|
72
72
|
self
|
73
73
|
end
|
74
74
|
|
75
75
|
# Swap the subscription item to a new Stripe plan, and invoice immediately.
|
76
76
|
def swap_and_invoice(plan, options = {})
|
77
|
-
|
77
|
+
always_invoice
|
78
78
|
|
79
|
-
|
79
|
+
swap(plan, options)
|
80
80
|
end
|
81
81
|
|
82
82
|
# Update the underlying Stripe subscription item information for the model.
|
83
83
|
def update_stripe_subscription_item(options = {})
|
84
84
|
Stripe::SubscriptionItem.update(
|
85
|
-
|
85
|
+
stripe_id, options, subscription.owner.stripe_options
|
86
86
|
)
|
87
87
|
end
|
88
88
|
|
89
89
|
# Get the subscription as a Stripe subscription item object.
|
90
90
|
def as_stripe_subscription_item(expand = {})
|
91
91
|
Stripe::SubscriptionItem.retrieve(
|
92
|
-
{:
|
93
|
-
|
92
|
+
{ id: stripe_id, expand: expand },
|
93
|
+
subscription.owner.stripe_options
|
94
94
|
)
|
95
95
|
end
|
96
96
|
end
|
data/lib/reji/tax.rb
CHANGED
@@ -9,13 +9,11 @@ module Reji
|
|
9
9
|
end
|
10
10
|
|
11
11
|
# Get the applied currency.
|
12
|
-
|
13
|
-
@currency
|
14
|
-
end
|
12
|
+
attr_reader :currency
|
15
13
|
|
16
14
|
# Get the total tax that was paid (or will be paid).
|
17
15
|
def amount
|
18
|
-
|
16
|
+
format_amount(@amount)
|
19
17
|
end
|
20
18
|
|
21
19
|
# Get the raw total tax that was paid (or will be paid).
|
@@ -24,24 +22,24 @@ module Reji
|
|
24
22
|
end
|
25
23
|
|
26
24
|
# Determine if the tax is inclusive or not.
|
27
|
-
def
|
25
|
+
def inclusive?
|
28
26
|
@tax_rate.inclusive
|
29
27
|
end
|
30
28
|
|
31
29
|
# Stripe::TaxRate
|
32
|
-
|
33
|
-
@tax_rate
|
34
|
-
end
|
30
|
+
attr_reader :tax_rate
|
35
31
|
|
36
32
|
# Dynamically get values from the Stripe TaxRate.
|
37
33
|
def method_missing(key)
|
38
34
|
@tax_rate[key]
|
39
35
|
end
|
40
36
|
|
41
|
-
|
37
|
+
def respond_to_missing?(method_name, include_private = false)
|
38
|
+
super
|
39
|
+
end
|
42
40
|
|
43
41
|
# Format the given amount into a displayable currency.
|
44
|
-
def format_amount(amount)
|
42
|
+
protected def format_amount(amount)
|
45
43
|
Reji.format_amount(amount, @currency)
|
46
44
|
end
|
47
45
|
end
|
data/lib/reji/version.rb
CHANGED
data/reji.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
$LOAD_PATH.push File.expand_path('lib', __dir__)
|
4
4
|
|
5
5
|
require 'reji/version'
|
6
6
|
|
@@ -20,13 +20,14 @@ Gem::Specification.new do |s|
|
|
20
20
|
s.require_paths = ['lib']
|
21
21
|
s.test_files = `git ls-files -- {spec}/*`.split("\n")
|
22
22
|
|
23
|
-
s.add_dependency '
|
23
|
+
s.add_dependency 'actionmailer', '>= 5.0'
|
24
|
+
s.add_dependency 'activerecord', '>= 5.0'
|
24
25
|
s.add_dependency 'money', '>= 6.0'
|
25
26
|
s.add_dependency 'railties', '>= 5.0'
|
26
|
-
s.add_dependency '
|
27
|
-
s.add_dependency 'actionmailer', '>= 5.0'
|
27
|
+
s.add_dependency 'stripe', '>= 5.0'
|
28
28
|
s.add_dependency 'wicked_pdf'
|
29
29
|
s.add_dependency 'wkhtmltopdf-binary'
|
30
|
+
s.add_development_dependency 'appraisal'
|
30
31
|
s.add_development_dependency 'rspec-rails', '~> 4.0.1'
|
31
32
|
s.add_development_dependency 'sqlite3', '~> 1.4.2'
|
32
33
|
end
|
@@ -4,18 +4,14 @@ class User < ActiveRecord::Base
|
|
4
4
|
include Reji::Billable
|
5
5
|
|
6
6
|
def tax_rates
|
7
|
-
@tax_rates ||
|
7
|
+
@tax_rates || []
|
8
8
|
end
|
9
9
|
|
10
10
|
def plan_tax_rates
|
11
11
|
@plan_tax_rates || {}
|
12
12
|
end
|
13
13
|
|
14
|
-
|
15
|
-
@plan_tax_rates = value
|
16
|
-
end
|
14
|
+
attr_writer :plan_tax_rates
|
17
15
|
|
18
|
-
|
19
|
-
@tax_rates = value
|
20
|
-
end
|
16
|
+
attr_writer :tax_rates
|
21
17
|
end
|
data/spec/dummy/application.rb
CHANGED
@@ -4,7 +4,7 @@ require 'rails/all'
|
|
4
4
|
require 'reji'
|
5
5
|
|
6
6
|
module Dummy
|
7
|
-
APP_ROOT = File.expand_path(
|
7
|
+
APP_ROOT = File.expand_path(__dir__).freeze
|
8
8
|
|
9
9
|
I18n.enforce_available_locales = true
|
10
10
|
|
@@ -30,14 +30,10 @@ module Dummy
|
|
30
30
|
config.secret_key_base = 'SECRET_KEY_BASE'
|
31
31
|
|
32
32
|
if config.active_record.sqlite3.respond_to?(:represent_boolean_as_integer)
|
33
|
-
if Rails::VERSION::MAJOR < 6
|
34
|
-
config.active_record.sqlite3.represent_boolean_as_integer = true
|
35
|
-
end
|
33
|
+
config.active_record.sqlite3.represent_boolean_as_integer = true if Rails::VERSION::MAJOR < 6
|
36
34
|
end
|
37
35
|
|
38
|
-
if Rails::VERSION::MAJOR >= 6
|
39
|
-
config.action_mailer.delivery_job = 'ActionMailer::MailDeliveryJob'
|
40
|
-
end
|
36
|
+
config.action_mailer.delivery_job = 'ActionMailer::MailDeliveryJob' if Rails::VERSION::MAJOR >= 6
|
41
37
|
|
42
38
|
config.active_job.queue_adapter = :inline
|
43
39
|
|
data/spec/dummy/db/schema.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
ActiveRecord::Schema.define
|
3
|
+
ActiveRecord::Schema.define do
|
4
4
|
create_table 'subscription_items', force: true do |t|
|
5
5
|
t.bigint 'subscription_id', null: false
|
6
6
|
t.string 'stripe_id', null: false
|
@@ -9,7 +9,7 @@ ActiveRecord::Schema.define(version: 2020_01_01_00_00_00) do
|
|
9
9
|
t.datetime 'created_at', precision: 6, null: false
|
10
10
|
t.datetime 'updated_at', precision: 6, null: false
|
11
11
|
t.index ['stripe_id'], name: 'index_subscription_items_on_stripe_id'
|
12
|
-
t.index [
|
12
|
+
t.index %w[subscription_id stripe_plan], name: 'index_subscription_items_on_subscription_id_and_stripe_plan', unique: true
|
13
13
|
end
|
14
14
|
|
15
15
|
create_table 'subscriptions', force: true do |t|
|
@@ -23,7 +23,7 @@ ActiveRecord::Schema.define(version: 2020_01_01_00_00_00) do
|
|
23
23
|
t.timestamp 'ends_at'
|
24
24
|
t.datetime 'created_at', precision: 6, null: false
|
25
25
|
t.datetime 'updated_at', precision: 6, null: false
|
26
|
-
t.index [
|
26
|
+
t.index %w[user_id stripe_status], name: 'index_subscriptions_on_user_id_and_stripe_status'
|
27
27
|
end
|
28
28
|
|
29
29
|
create_table 'users', force: true do |t|
|
@@ -37,4 +37,3 @@ ActiveRecord::Schema.define(version: 2020_01_01_00_00_00) do
|
|
37
37
|
t.index ['email'], name: 'index_users_on_email', unique: true
|
38
38
|
end
|
39
39
|
end
|
40
|
-
|