effective_orders 4.0.6 → 4.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/README.md +1 -1
- data/app/assets/javascripts/effective_orders/subscriptions.js.coffee +25 -5
- data/app/controllers/effective/customers_controller.rb +3 -9
- data/app/controllers/effective/subscripter_controller.rb +1 -6
- data/app/controllers/effective/webhooks_controller.rb +12 -7
- data/app/helpers/effective_subscriptions_helper.rb +2 -37
- data/app/models/concerns/acts_as_purchasable.rb +4 -1
- data/app/models/concerns/acts_as_subscribable.rb +7 -2
- data/app/models/effective/customer.rb +13 -21
- data/app/models/effective/subscripter.rb +88 -127
- data/app/models/effective/subscription.rb +55 -5
- data/app/views/effective/customers/_customer.html.haml +8 -72
- data/app/views/effective/customers/update.js.erb +1 -1
- data/app/views/effective/subscripter/_form.html.haml +56 -3
- data/db/migrate/01_create_effective_orders.rb.erb +6 -3
- data/lib/effective_orders/version.rb +1 -1
- metadata +2 -4
- data/app/views/effective/subscriptions/_plan.html.haml +0 -20
- data/app/views/effective/subscriptions/_subscription.html.haml +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f15a457703f171efde10a8a750dcd7b8c7092e3f
|
4
|
+
data.tar.gz: 182e31f6db68aac1ff8f6a50fdf405f5e3307ae5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ed4b21b9c7d9bd6ad1ac9615874b1735eb69826bd5db89ab18df7f96889a8282edcd1ca37b363e6e4f0dcc0001c608a83df431b8e6ab7361d5633ca1f2b3326
|
7
|
+
data.tar.gz: d59aa7963b0436533fbf99fbe9e1d6ee79dbdbf8278b7d361fde6e16187023e935436492a7b04a1d4df80d682d7b4725e4e6ebb2fe4233c40ad993a9861cc691
|
data/README.md
CHANGED
@@ -20,7 +20,7 @@ Please check out [Effective Orders 3.x](https://github.com/code-and-effect/effec
|
|
20
20
|
|
21
21
|
## Getting Started
|
22
22
|
|
23
|
-
Please first install the [effective_addresses](https://github.com/code-and-effect/effective_addresses), [effective_datatables](https://github.com/code-and-effect/effective_datatables) and [
|
23
|
+
Please first install the [effective_addresses](https://github.com/code-and-effect/effective_addresses), [effective_datatables](https://github.com/code-and-effect/effective_datatables) and [effective_bootstrap](https://github.com/code-and-effect/effective_bootstrap) gems.
|
24
24
|
|
25
25
|
Add to your Gemfile:
|
26
26
|
|
@@ -13,6 +13,30 @@ stripeSubscriptionHandler = (key, form) ->
|
|
13
13
|
form.find("input[name$='[stripe_token]']").val('' + token['id'])
|
14
14
|
form.addClass('stripe-success').submit() # Submits the form. As this is a remote form, submits via JS
|
15
15
|
|
16
|
+
# This updates the form whenever a quantity change is made
|
17
|
+
$(document).on 'change keyup', '.effective-orders-subscripter-plan-quantity', (event) ->
|
18
|
+
$obj = $(event.currentTarget)
|
19
|
+
$plan = $obj.closest('.effective-orders-stripe-plan')
|
20
|
+
return unless $plan.length == 1
|
21
|
+
|
22
|
+
# Assign the quantity to each quantity field
|
23
|
+
$plan.closest('form').find(".effective-orders-stripe-plan:not([data-plan-id='#{$plan.data('id')}'])").find("input[name$='[quantity]']").val($obj.val())
|
24
|
+
|
25
|
+
quantity = $obj.val() || 0
|
26
|
+
|
27
|
+
# Assign all totals
|
28
|
+
$plan.closest('form').find(".effective-orders-stripe-plan").each ->
|
29
|
+
plan = $(this)
|
30
|
+
amount = parseInt(plan.data('amount'))
|
31
|
+
interval = plan.data('plan-interval')
|
32
|
+
|
33
|
+
total = (quantity * amount)
|
34
|
+
total = (total / 12) if interval == 'year'
|
35
|
+
|
36
|
+
total = '$' + (total / 100.0).toFixed(2)
|
37
|
+
|
38
|
+
plan.find('#effective_subscripter_total_amount').text(total)
|
39
|
+
|
16
40
|
# Hijack submit and get a stripe token
|
17
41
|
$(document).on 'click', ".effective-orders-stripe-token-required[type='submit'],[data-choose-stripe-plan-id]", (event) ->
|
18
42
|
$obj = $(event.currentTarget)
|
@@ -44,8 +68,4 @@ $(document).on 'click', ".effective-orders-stripe-token-required[type='submit'],
|
|
44
68
|
name: stripe.name
|
45
69
|
email: stripe.email
|
46
70
|
description: plan.name
|
47
|
-
|
48
|
-
panelLabel: "{{amount}}/#{plan.interval} Go!"
|
49
|
-
|
50
|
-
$(document).on 'change', "input[name='effective_subscripter[stripe_plan_id]']", (event) ->
|
51
|
-
$(event.currentTarget).closest('form').find(".effective-orders-stripe-token-required[type='submit']").click()
|
71
|
+
panelLabel: 'Update Plan'
|
@@ -4,18 +4,12 @@ module Effective
|
|
4
4
|
|
5
5
|
include Effective::CrudController
|
6
6
|
|
7
|
-
submit :save, 'Save',
|
7
|
+
submit :save, 'Save', success: -> { 'Successfully updated card.' }
|
8
8
|
page_title 'Customer Settings'
|
9
9
|
|
10
10
|
def resource
|
11
|
-
@customer
|
12
|
-
@subscripter ||= Effective::Subscripter.new(customer: @customer,
|
13
|
-
end
|
14
|
-
|
15
|
-
# I don't want save_resource to wrap my save in a transaction
|
16
|
-
def save_resource(resource, action, to_assign)
|
17
|
-
resource.assign_attributes(to_assign)
|
18
|
-
resource.save!
|
11
|
+
@customer = Effective::Customer.where(user: current_user).first!
|
12
|
+
@subscripter ||= Effective::Subscripter.new(customer: @customer, current_user: current_user)
|
19
13
|
end
|
20
14
|
|
21
15
|
# StrongParameters
|
@@ -7,12 +7,7 @@ module Effective
|
|
7
7
|
submit :save, 'Save', redirect: :back, success: -> { 'Successfully updated plan.' }
|
8
8
|
|
9
9
|
def resource
|
10
|
-
@subscripter ||= Effective::Subscripter.new(
|
11
|
-
end
|
12
|
-
|
13
|
-
# I don't want save_resource to wrap my save in a transaction
|
14
|
-
def save_resource(resource, action = :save, &block)
|
15
|
-
resource.save!
|
10
|
+
@subscripter ||= Effective::Subscripter.new(current_user: current_user)
|
16
11
|
end
|
17
12
|
|
18
13
|
# StrongParameters
|
@@ -42,17 +42,13 @@ module Effective
|
|
42
42
|
# when 'charge.failed' # Card declined. 4000 0000 0000 0341
|
43
43
|
|
44
44
|
when 'invoice.payment_succeeded'
|
45
|
-
|
46
|
-
|
45
|
+
subscriptions.each { |subscription| subscription.update!(status: EffectiveOrders::ACTIVE) }
|
47
46
|
send_email(:subscription_payment_succeeded, customer)
|
48
47
|
when 'invoice.payment_failed'
|
49
|
-
|
50
|
-
|
48
|
+
subscriptions.each { |subscription| subscription.update!(status: EffectiveOrders::PAST_DUE) }
|
51
49
|
send_email(:subscription_payment_failed, customer)
|
52
50
|
when 'customer.subscription.deleted'
|
53
|
-
|
54
|
-
customer.subscriptions.delete_all
|
55
|
-
|
51
|
+
subscriptions.each { |subscription| subscription.destroy! }
|
56
52
|
send_email(:subscription_canceled, customer)
|
57
53
|
when 'customer.subscription.created'
|
58
54
|
send_email(:subscription_created, customer)
|
@@ -79,6 +75,15 @@ module Effective
|
|
79
75
|
)
|
80
76
|
end
|
81
77
|
|
78
|
+
def subscriptions
|
79
|
+
customer.subscriptions.select { |subscription| stripe_plan_ids.include?(subscription.stripe_plan_id) }
|
80
|
+
end
|
81
|
+
|
82
|
+
def stripe_plan_ids
|
83
|
+
return unless @event.respond_to?(:data)
|
84
|
+
@stripe_plan_ids ||= @event.data.object.lines.map { |line| line.plan.id }
|
85
|
+
end
|
86
|
+
|
82
87
|
def send_email(email, *mailer_args)
|
83
88
|
Effective::OrdersMailer.public_send(email, *mailer_args).public_send(EffectiveOrders.mailer[:deliver_method])
|
84
89
|
Effective::OrdersMailer.public_send(:subscription_event_to_admin, email.to_s, *mailer_args).public_send(EffectiveOrders.mailer[:deliver_method])
|
@@ -1,45 +1,10 @@
|
|
1
1
|
module EffectiveSubscriptionsHelper
|
2
2
|
|
3
|
-
def
|
4
|
-
|
5
|
-
end
|
6
|
-
|
7
|
-
def render_plan_partial(plan)
|
8
|
-
if lookup_context.template_exists?("effective/subscriptions/#{plan[:id].downcase}", [], true)
|
9
|
-
"effective/subscriptions/#{plan[:id].downcase}" # Render the app's views/effective/subscriptions/_gold.html.haml
|
10
|
-
elsif lookup_context.template_exists?("effective/subscriptions/#{plan[:name].downcase}", [], true)
|
11
|
-
"effective/subscriptions/#{plan[:name].downcase}" # Render the app's views/effective/subscriptions/_gold.html.haml
|
12
|
-
else
|
13
|
-
'effective/subscriptions/plan' # Render effective_orders default plan panel
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
def stripe_plans_collection(form)
|
18
|
-
raise 'expected an Effective::FormBuilder object' unless form.class.name == 'Effective::FormBuilder'
|
19
|
-
|
20
|
-
subscripter = form.object
|
21
|
-
raise 'form object must be a subscripter object' unless subscripter.class.name == 'Effective::Subscripter'
|
22
|
-
|
23
|
-
plans = EffectiveOrders.stripe_plans.values.sort do |x, y|
|
3
|
+
def stripe_plans_collection(subscripter = nil)
|
4
|
+
EffectiveOrders.stripe_plans.values.sort do |x, y|
|
24
5
|
amount = (x[:amount] <=> y[:amount])
|
25
6
|
(amount != 0) ? amount : x[:name] <=> y[:name]
|
26
7
|
end
|
27
|
-
|
28
|
-
if (existing = subscripter.customer.stripe_subscription_interval).present?
|
29
|
-
plans.select! { |plan| plan[:interval] == existing }
|
30
|
-
end
|
31
|
-
|
32
|
-
plans.map do |plan|
|
33
|
-
content = render(partial: render_plan_partial(plan), locals: {
|
34
|
-
f: form,
|
35
|
-
plan: plan,
|
36
|
-
selected: Array(form.object.stripe_plan_id).include?(plan[:id]),
|
37
|
-
subscribable: form.object.subscribable,
|
38
|
-
subscribed: form.object.subscribable.subscribed?(plan[:id])
|
39
|
-
})
|
40
|
-
|
41
|
-
[content, plan[:id]]
|
42
|
-
end
|
43
8
|
end
|
44
9
|
|
45
10
|
def subscribable_form_with(subscribable)
|
@@ -29,7 +29,10 @@ module ActsAsPurchasable
|
|
29
29
|
through: :order_items, class_name: 'Effective::Order', source: :order
|
30
30
|
|
31
31
|
# Database max integer value is 2147483647. So let's round that down and use a max/min of $20 million (2000000000)
|
32
|
-
validates :price, presence: true
|
32
|
+
validates :price, presence: true
|
33
|
+
validates :price, numericality: { less_than_or_equal_to: 2000000000, message: 'maximum price is $20,000,000' }
|
34
|
+
validates :price, numericality: { greater_than_or_equal_to: -2000000000, message: 'minimum price is -$20,000,000' }
|
35
|
+
|
33
36
|
validates :tax_exempt, inclusion: { in: [true, false] }
|
34
37
|
|
35
38
|
with_options(if: -> { quantity_enabled? }) do
|
@@ -44,7 +44,9 @@ module ActsAsSubscribable
|
|
44
44
|
end
|
45
45
|
|
46
46
|
def subscripter
|
47
|
-
@_effective_subscripter ||=
|
47
|
+
@_effective_subscripter ||= begin
|
48
|
+
Effective::Subscripter.new(subscribable: self, user: subscribable_buyer, quantity: (subscription&.quantity || 0), stripe_plan_id: subscription&.stripe_plan_id)
|
49
|
+
end
|
48
50
|
end
|
49
51
|
|
50
52
|
def subscribed?(stripe_plan_id = nil)
|
@@ -76,5 +78,8 @@ module ActsAsSubscribable
|
|
76
78
|
raise 'acts_as_subscribable object requires the subscribable_buyer method be defined to return the User buying this item.'
|
77
79
|
end
|
78
80
|
|
79
|
-
|
81
|
+
def subscribable_quantity_used
|
82
|
+
raise 'acts_as_subscribable object requires the subscribable_quantity_used method be defined to determine how many are in use.'
|
83
|
+
end
|
80
84
|
|
85
|
+
end
|
@@ -2,7 +2,7 @@ module Effective
|
|
2
2
|
class Customer < ActiveRecord::Base
|
3
3
|
self.table_name = EffectiveOrders.customers_table_name.to_s
|
4
4
|
|
5
|
-
attr_accessor :stripe_customer
|
5
|
+
attr_accessor :stripe_customer
|
6
6
|
|
7
7
|
belongs_to :user
|
8
8
|
has_many :subscriptions, -> { includes(:subscribable) }, class_name: 'Effective::Subscription', foreign_key: 'customer_id'
|
@@ -12,21 +12,12 @@ module Effective
|
|
12
12
|
# stripe_customer_id :string # cus_xja7acoa03
|
13
13
|
# active_card :string # **** **** **** 4242 Visa 05/12
|
14
14
|
|
15
|
-
# stripe_subscription_id :string # Each user gets one stripe subscription object, which can contain many items
|
16
|
-
# stripe_subscription_interval :string
|
17
|
-
# status :string
|
18
|
-
|
19
15
|
# timestamps
|
20
16
|
|
21
17
|
scope :deep, -> { includes(subscriptions: :subscribable) }
|
22
18
|
|
23
|
-
after_commit do
|
24
|
-
subscriptions.each { |subscription| subscription.subscribable.update_column(:subscription_status, status) }
|
25
|
-
end
|
26
|
-
|
27
19
|
validates :user, presence: true
|
28
20
|
validates :stripe_customer_id, presence: true
|
29
|
-
validates :status, if: -> { stripe_subscription_id.present? }, inclusion: { in: EffectiveOrders::STATUSES.keys }
|
30
21
|
|
31
22
|
def self.for_user(user)
|
32
23
|
Effective::Customer.where(user: user).first_or_initialize
|
@@ -47,28 +38,29 @@ module Effective
|
|
47
38
|
end
|
48
39
|
end
|
49
40
|
|
50
|
-
def stripe_subscription
|
51
|
-
@stripe_subscription ||= if stripe_subscription_id.present?
|
52
|
-
Rails.logger.info "[STRIPE] get subscription: #{stripe_subscription_id}"
|
53
|
-
::Stripe::Subscription.retrieve(stripe_subscription_id)
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
41
|
def upcoming_invoice
|
58
|
-
@upcoming_invoice ||= if stripe_customer_id.present?
|
42
|
+
@upcoming_invoice ||= if stripe_customer_id.present?
|
59
43
|
Rails.logger.info "[STRIPE] get upcoming invoice: #{stripe_customer_id}"
|
60
44
|
::Stripe::Invoice.upcoming(customer: stripe_customer_id) rescue nil
|
61
45
|
end
|
62
46
|
end
|
63
47
|
|
64
48
|
def token_required?
|
65
|
-
active_card.blank? ||
|
49
|
+
active_card.blank? || past_due?
|
50
|
+
end
|
51
|
+
|
52
|
+
def past_due?
|
53
|
+
subscriptions.any? { |subscription| subscription.past_due? }
|
54
|
+
end
|
55
|
+
|
56
|
+
def active?
|
57
|
+
subscriptions.present? && subscriptions.all? { |subscription| subscription.active? }
|
66
58
|
end
|
67
59
|
|
68
60
|
def payment_status
|
69
|
-
if
|
61
|
+
if past_due?
|
70
62
|
'We ran into an error processing your last payment. Please update or confirm your card details to continue.'
|
71
|
-
elsif
|
63
|
+
elsif active?
|
72
64
|
"Your payment is in good standing. Thanks so much for your support!"
|
73
65
|
elsif active_card.blank?
|
74
66
|
'No credit card on file. Please add a card.'
|
@@ -4,8 +4,8 @@ module Effective
|
|
4
4
|
class Subscripter
|
5
5
|
include ActiveModel::Model
|
6
6
|
|
7
|
-
attr_accessor :user, :subscribable, :customer
|
8
|
-
attr_accessor :subscribable_global_id, :stripe_token, :stripe_plan_id, :
|
7
|
+
attr_accessor :current_user, :user, :subscribable, :customer
|
8
|
+
attr_accessor :subscribable_global_id, :stripe_token, :stripe_plan_id, :quantity
|
9
9
|
|
10
10
|
validates :user, presence: true
|
11
11
|
validates :subscribable, presence: true, if: -> { stripe_plan_id.present? }
|
@@ -17,10 +17,23 @@ module Effective
|
|
17
17
|
self.errors.add(:stripe_token, 'updated payment card required') if stripe_token.blank? && token_required?
|
18
18
|
end
|
19
19
|
|
20
|
+
validate(if: -> { stripe_plan_id && subscribable }) do
|
21
|
+
quantity_used = [subscribable.subscribable_quantity_used, 0].max
|
22
|
+
self.errors.add(:quantity, "must be #{quantity_used} or greater") unless quantity >= quantity_used
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
'Your Plan'
|
27
|
+
end
|
28
|
+
|
20
29
|
def customer
|
21
30
|
@customer ||= Effective::Customer.deep.where(user: user).first_or_initialize
|
22
31
|
end
|
23
32
|
|
33
|
+
def current_user=(user)
|
34
|
+
@user = user
|
35
|
+
end
|
36
|
+
|
24
37
|
def subscribable_global_id
|
25
38
|
subscribable&.to_global_id
|
26
39
|
end
|
@@ -29,20 +42,12 @@ module Effective
|
|
29
42
|
@subscribable = GlobalID::Locator.locate(global_id)
|
30
43
|
end
|
31
44
|
|
32
|
-
def user_id=(id)
|
33
|
-
@user = User.find(id)
|
34
|
-
end
|
35
|
-
|
36
|
-
def current_plan
|
37
|
-
subscribable&.subscription&.plan
|
38
|
-
end
|
39
|
-
|
40
45
|
def plan
|
41
46
|
EffectiveOrders.stripe_plans[stripe_plan_id]
|
42
47
|
end
|
43
48
|
|
44
|
-
def
|
45
|
-
@
|
49
|
+
def quantity
|
50
|
+
(@quantity.to_i if @quantity.present?)
|
46
51
|
end
|
47
52
|
|
48
53
|
def token_required?
|
@@ -50,142 +55,113 @@ module Effective
|
|
50
55
|
end
|
51
56
|
|
52
57
|
def save!
|
53
|
-
return true if (plan == current_plan) && stripe_token.blank? # No work to do
|
54
|
-
|
55
58
|
raise 'is invalid' unless valid?
|
56
59
|
|
57
60
|
create_customer!
|
58
61
|
create_stripe_token!
|
59
|
-
|
60
|
-
sync_subscription!
|
62
|
+
save_subscription!
|
61
63
|
true
|
62
64
|
end
|
63
65
|
|
64
66
|
def destroy!
|
65
67
|
return true unless plan
|
66
68
|
|
67
|
-
subscribable.subscription
|
68
|
-
|
69
|
-
|
69
|
+
subscription = subscribable.subscription
|
70
|
+
|
71
|
+
Rails.logger.info " -> [STRIPE] delete subscription"
|
72
|
+
subscription.stripe_subscription.delete
|
73
|
+
subscription.destroy!
|
74
|
+
|
70
75
|
true
|
71
76
|
end
|
72
77
|
|
73
78
|
protected
|
74
79
|
|
75
|
-
# This should work even if the rest of the form doesn't. Careful with our transactions...
|
76
80
|
def create_customer!
|
77
|
-
if customer.stripe_customer.
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
81
|
+
return if customer.stripe_customer.present?
|
82
|
+
|
83
|
+
Rails.logger.info "[STRIPE] create customer: #{user.email}"
|
84
|
+
customer.stripe_customer = Stripe::Customer.create(email: user.email, description: user.to_s, metadata: { user_id: user.id })
|
85
|
+
customer.stripe_customer_id = customer.stripe_customer.id
|
86
|
+
customer.save!
|
83
87
|
end
|
84
88
|
|
85
89
|
# Update stripe customer card
|
86
90
|
def create_stripe_token!
|
87
|
-
if stripe_token.
|
88
|
-
Rails.logger.info "[STRIPE] update source: #{stripe_token}"
|
89
|
-
customer.stripe_customer.source = stripe_token
|
90
|
-
customer.stripe_customer.save
|
91
|
-
|
92
|
-
if customer.stripe_customer.default_source.present?
|
93
|
-
card = customer.stripe_customer.sources.retrieve(customer.stripe_customer.default_source)
|
94
|
-
customer.active_card = "**** **** **** #{card.last4} #{card.brand} #{card.exp_month}/#{card.exp_year}"
|
95
|
-
customer.save!
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|
91
|
+
return if stripe_token.blank?
|
99
92
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
end
|
93
|
+
Rails.logger.info "[STRIPE] update source: #{stripe_token}"
|
94
|
+
customer.stripe_customer.source = stripe_token
|
95
|
+
customer.stripe_customer.save
|
104
96
|
|
105
|
-
|
106
|
-
return unless plan.present?
|
107
|
-
customer.stripe_subscription.blank? ? create_subscription! : update_subscription!
|
97
|
+
return if customer.stripe_customer.default_source.blank?
|
108
98
|
|
99
|
+
card = customer.stripe_customer.sources.retrieve(customer.stripe_customer.default_source)
|
100
|
+
customer.active_card = "**** **** **** #{card.last4} #{card.brand} #{card.exp_month}/#{card.exp_year}"
|
109
101
|
customer.save!
|
110
102
|
end
|
111
103
|
|
112
|
-
def
|
104
|
+
def save_subscription!
|
113
105
|
return unless plan.present?
|
114
|
-
return if customer.stripe_subscription.present?
|
115
106
|
|
116
|
-
|
117
|
-
customer.stripe_subscription = Stripe::Subscription.create(customer: customer.stripe_customer_id, items: items(metadata: false), metadata: metadata)
|
118
|
-
customer.stripe_subscription_id = customer.stripe_subscription.id
|
107
|
+
subscription.assign_attributes(stripe_plan_id: stripe_plan_id, quantity: quantity)
|
119
108
|
|
120
|
-
|
121
|
-
|
109
|
+
cancel_subscription!
|
110
|
+
create_subscription! || update_subscription!
|
111
|
+
true
|
122
112
|
end
|
123
113
|
|
124
|
-
def
|
125
|
-
return unless
|
126
|
-
return if customer.stripe_subscription.blank?
|
127
|
-
|
128
|
-
Rails.logger.info "[STRIPE] update subscription: #{customer.stripe_subscription_id} #{items}"
|
114
|
+
def cancel_subscription!
|
115
|
+
return false unless subscription.persisted? && subscription.stripe_plan_id_changed?
|
129
116
|
|
130
|
-
|
131
|
-
|
132
|
-
customer.stripe_subscription_id = nil
|
133
|
-
customer.status = EffectiveOrders::CANCELED
|
134
|
-
return
|
135
|
-
end
|
117
|
+
item = items.first
|
118
|
+
stripe_item = subscription.stripe_subscription.items.first
|
136
119
|
|
137
|
-
|
138
|
-
quantity_increased = false
|
120
|
+
return false unless stripe_item.present? && item[:plan] != stripe_item['plan']['id']
|
139
121
|
|
140
|
-
|
141
|
-
|
122
|
+
Rails.logger.info " -> [STRIPE] cancel plan: #{stripe_item['plan']['id']}"
|
123
|
+
subscription.stripe_subscription.delete
|
124
|
+
subscription.assign_attributes(stripe_subscription: nil, stripe_subscription_id: nil)
|
142
125
|
|
143
|
-
|
144
|
-
|
145
|
-
quantity_increased ||= (item[:quantity] > stripe_item['quantity']) # Any quantity increased
|
126
|
+
true
|
127
|
+
end
|
146
128
|
|
147
|
-
|
148
|
-
|
129
|
+
def create_subscription!
|
130
|
+
return false unless subscription.stripe_subscription.blank?
|
149
131
|
|
150
|
-
|
151
|
-
|
152
|
-
end
|
132
|
+
Rails.logger.info "[STRIPE] create subscription: #{items}"
|
133
|
+
stripe_subscription = Stripe::Subscription.create(customer: customer.stripe_customer_id, items: items, metadata: metadata)
|
153
134
|
|
154
|
-
|
155
|
-
|
156
|
-
|
135
|
+
subscription.update!(
|
136
|
+
stripe_subscription: stripe_subscription,
|
137
|
+
stripe_subscription_id: stripe_subscription.id,
|
138
|
+
status: stripe_subscription.status,
|
139
|
+
name: stripe_subscription.plan.nickname,
|
140
|
+
interval: stripe_subscription.plan.interval,
|
141
|
+
quantity: quantity
|
142
|
+
)
|
143
|
+
end
|
157
144
|
|
158
|
-
|
159
|
-
|
160
|
-
end
|
145
|
+
def update_subscription!
|
146
|
+
return false unless subscription.stripe_subscription.present?
|
161
147
|
|
162
|
-
|
163
|
-
|
164
|
-
next if items.find { |item| item[:plan] == stripe_item['plan']['id'] }
|
148
|
+
stripe_item = subscription.stripe_subscription.items.first
|
149
|
+
item = items.first
|
165
150
|
|
166
|
-
|
167
|
-
|
168
|
-
end
|
151
|
+
return false unless stripe_item.present? && item[:plan] == stripe_item['plan']['id']
|
152
|
+
return false unless item[:quantity] != subscription.stripe_subscription.quantity
|
169
153
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
customer.stripe_subscription.metadata = metadata
|
174
|
-
customer.stripe_subscription.save
|
175
|
-
end
|
154
|
+
Rails.logger.info " -> [STRIPE] update plan: #{item[:plan]}"
|
155
|
+
stripe_item.quantity = item[:quantity]
|
156
|
+
stripe_item.save
|
176
157
|
|
177
|
-
|
178
|
-
if quantity_increased
|
179
|
-
Rails.logger.info " -> [STRIPE] generate invoice"
|
180
|
-
Stripe::Invoice.create(customer: customer.stripe_customer_id).pay
|
181
|
-
end
|
158
|
+
subscription.update!(status: subscription.stripe_subscription.status)
|
182
159
|
|
183
|
-
|
184
|
-
|
160
|
+
# Invoice immediately
|
161
|
+
Rails.logger.info " -> [STRIPE] generate invoice"
|
162
|
+
Stripe::Invoice.create(customer: customer.stripe_customer_id).pay
|
185
163
|
|
186
|
-
|
187
|
-
self.stripe_plan_id = stripe_plan_id
|
188
|
-
save!
|
164
|
+
true
|
189
165
|
end
|
190
166
|
|
191
167
|
private
|
@@ -195,33 +171,18 @@ module Effective
|
|
195
171
|
customer.subscriptions.find { |sub| sub.subscribable == subscribable } || customer.subscriptions.build(subscribable: subscribable, customer: customer)
|
196
172
|
end
|
197
173
|
|
198
|
-
def items
|
199
|
-
|
200
|
-
if metadata
|
201
|
-
{ plan: plan, quantity: subscriptions.length, metadata: metadata(subscriptions: subscriptions) }
|
202
|
-
else
|
203
|
-
{ plan: plan, quantity: subscriptions.length }
|
204
|
-
end
|
205
|
-
end
|
174
|
+
def items
|
175
|
+
[{ plan: subscription.stripe_plan_id, quantity: subscription.quantity }]
|
206
176
|
end
|
207
177
|
|
208
178
|
# The stripe metadata limit is 500 characters
|
209
|
-
def metadata
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
retval[subscribable_type.downcase + '_id'] = subs.map { |sub| sub.subscribable.id }.join(',')
|
217
|
-
retval[subscribable_type.downcase] = subs.map { |sub| sub.subscribable.to_s }.join(',').truncate(500)
|
218
|
-
else
|
219
|
-
retval[subscribable_type.downcase + '_ids'] = subs.map { |sub| sub.subscribable.id }.join(',')
|
220
|
-
retval[subscribable_type.downcase.pluralize] = subs.map { |sub| sub.subscribable.to_s }.join(',').truncate(500)
|
221
|
-
end
|
222
|
-
end
|
223
|
-
|
224
|
-
retval
|
179
|
+
def metadata
|
180
|
+
{
|
181
|
+
:user_id => user.id.to_s,
|
182
|
+
:user => user.to_s.truncate(500),
|
183
|
+
(subscription.subscribable_type.downcase + '_id').to_sym => subscription.subscribable.id.to_s,
|
184
|
+
subscription.subscribable_type.downcase.to_sym => subscription.subscribable.to_s
|
185
|
+
}
|
225
186
|
end
|
226
187
|
|
227
188
|
end
|
@@ -1,27 +1,58 @@
|
|
1
|
-
# This links the acts_as_subscribable_buyer (customer) to the acts_as_subscribable (subscribable)
|
2
|
-
|
3
1
|
module Effective
|
4
2
|
class Subscription < ActiveRecord::Base
|
5
3
|
self.table_name = EffectiveOrders.subscriptions_table_name.to_s
|
6
4
|
|
5
|
+
attr_accessor :stripe_subscription
|
6
|
+
|
7
7
|
belongs_to :customer, class_name: 'Effective::Customer', counter_cache: true
|
8
8
|
belongs_to :subscribable, polymorphic: true
|
9
9
|
|
10
10
|
# Attributes
|
11
|
-
# stripe_plan_id
|
12
|
-
#
|
11
|
+
# stripe_plan_id :string
|
12
|
+
# stripe_subscription_id :string
|
13
|
+
# name :string
|
14
|
+
# description :string
|
15
|
+
# interval :string
|
16
|
+
# quantity :integer
|
17
|
+
#
|
18
|
+
# status :string
|
13
19
|
#
|
14
20
|
# timestamps
|
15
21
|
|
16
22
|
before_validation(if: -> { plan && (stripe_plan_id_changed? || new_record?) }) do
|
17
|
-
self.name =
|
23
|
+
self.name = plan[:name]
|
24
|
+
self.description = plan[:description]
|
25
|
+
end
|
26
|
+
|
27
|
+
after_save(on: [:create, :update]) do
|
28
|
+
subscribable.subscription_name = name if subscribable.respond_to?(:subscription_name=)
|
29
|
+
subscribable.subscription_description = description if subscribable.respond_to?(:subscription_description=)
|
30
|
+
subscribable.subscription_interval = interval if subscribable.respond_to?(:subscription_interval=)
|
31
|
+
subscribable.subscription_quantity = quantity if subscribable.respond_to?(:subscription_quantity=)
|
32
|
+
subscribable.subscription_status = status if subscribable.respond_to?(:subscription_status=)
|
33
|
+
subscribable.save!(validate: false)
|
34
|
+
end
|
35
|
+
|
36
|
+
after_destroy do
|
37
|
+
subscribable.subscription_name = nil if subscribable.respond_to?(:subscription_name=)
|
38
|
+
subscribable.subscription_description = nil if subscribable.respond_to?(:subscription_description=)
|
39
|
+
subscribable.subscription_interval = nil if subscribable.respond_to?(:subscription_interval=)
|
40
|
+
subscribable.subscription_quantity = nil if subscribable.respond_to?(:subscription_quantity=)
|
41
|
+
subscribable.subscription_status = nil if subscribable.respond_to?(:subscription_status=)
|
42
|
+
subscribable.save!(validate: false)
|
18
43
|
end
|
19
44
|
|
20
45
|
validates :customer, presence: true
|
21
46
|
validates :subscribable, presence: true
|
22
47
|
|
23
48
|
validates :stripe_plan_id, presence: true, inclusion: { in: EffectiveOrders.stripe_plans.keys }
|
49
|
+
validates :stripe_subscription_id, presence: true
|
50
|
+
|
24
51
|
validates :name, presence: true
|
52
|
+
validates :interval, presence: true
|
53
|
+
validates :quantity, presence: true, numericality: { greater_than: 0 }
|
54
|
+
|
55
|
+
validates :status, inclusion: { in: EffectiveOrders::STATUSES.keys }
|
25
56
|
|
26
57
|
def to_s
|
27
58
|
name || 'New Subscription'
|
@@ -31,9 +62,28 @@ module Effective
|
|
31
62
|
EffectiveOrders.stripe_plans[stripe_plan_id]
|
32
63
|
end
|
33
64
|
|
65
|
+
def stripe_subscription
|
66
|
+
@stripe_subscription ||= if stripe_subscription_id.present?
|
67
|
+
Rails.logger.info "[STRIPE] get subscription: #{stripe_subscription_id}"
|
68
|
+
::Stripe::Subscription.retrieve(stripe_subscription_id)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
34
72
|
def <=>(other)
|
35
73
|
name.to_s <=> other&.name.to_s
|
36
74
|
end
|
37
75
|
|
76
|
+
def active?
|
77
|
+
status == 'active'
|
78
|
+
end
|
79
|
+
|
80
|
+
def past_due?
|
81
|
+
status == 'past_due'
|
82
|
+
end
|
83
|
+
|
84
|
+
def canceled?
|
85
|
+
status == 'canceled'
|
86
|
+
end
|
87
|
+
|
38
88
|
end
|
39
89
|
end
|
@@ -1,85 +1,21 @@
|
|
1
|
-
- if customer.stripe_subscription.present?
|
2
|
-
.card.my-4
|
3
|
-
.card-header Subscription
|
4
|
-
.card-body
|
5
|
-
%table.table
|
6
|
-
%tbody
|
7
|
-
%tr
|
8
|
-
%th Status
|
9
|
-
%td= customer.stripe_subscription.status.presence || 'unknown'
|
10
|
-
|
11
|
-
%tr
|
12
|
-
%th Email
|
13
|
-
%td= customer.stripe_customer.email
|
14
|
-
|
15
|
-
%tr
|
16
|
-
%th Card
|
17
|
-
%td
|
18
|
-
- if customer.stripe_customer.default_source.present?
|
19
|
-
- card = customer.stripe_customer.sources.retrieve(customer.stripe_customer.default_source)
|
20
|
-
= "**** **** **** #{card.last4} #{card.brand} #{card.exp_month}/#{card.exp_year}"
|
21
|
-
- else
|
22
|
-
None
|
23
|
-
|
24
|
-
%tr
|
25
|
-
%th Currency
|
26
|
-
%td= customer.stripe_customer.currency.to_s.upcase
|
27
|
-
|
28
|
-
- if customer.stripe_subscription.discount.present?
|
29
|
-
%tr
|
30
|
-
%th Coupon
|
31
|
-
%td= stripe_coupon_description(customer.stripe_subscription.discount.coupon)
|
32
|
-
|
33
|
-
- if customer.stripe_subscription.start.present?
|
34
|
-
%tr
|
35
|
-
%th Started
|
36
|
-
%td= Time.zone.at(customer.stripe_subscription.start).strftime('%F')
|
37
|
-
|
38
|
-
- if customer.stripe_subscription.ended_at.present?
|
39
|
-
%tr
|
40
|
-
%th Ended
|
41
|
-
%td= Time.zone.at(customer.stripe_subscription.ended_at).strftime('%F')
|
42
|
-
|
43
|
-
- if customer.stripe_subscription.canceled_at.present?
|
44
|
-
%tr
|
45
|
-
%th Cancelled
|
46
|
-
%td= Time.zone.at(customer.stripe_subscription.canceled_at).strftime('%F')
|
47
|
-
|
48
|
-
- if customer.stripe_subscription.current_period_start.present?
|
49
|
-
%tr
|
50
|
-
%th Current Period Start
|
51
|
-
%td= Time.zone.at(customer.stripe_subscription.current_period_start).strftime('%F')
|
52
|
-
|
53
|
-
- if customer.stripe_subscription.current_period_end.present?
|
54
|
-
%tr
|
55
|
-
%th Current Period End
|
56
|
-
%td= Time.zone.at(customer.stripe_subscription.current_period_end).strftime('%F')
|
57
|
-
|
58
|
-
- if customer.stripe_subscription.metadata.present? && false
|
59
|
-
%tr
|
60
|
-
%th Metadata
|
61
|
-
%td= tableize_hash(customer.stripe_subscription.metadata.to_h, th: false)
|
62
|
-
|
63
|
-
- if customer.stripe_subscription.items.present?
|
64
|
-
%tr
|
65
|
-
%th Plans
|
66
|
-
%td= tableize_hash(customer.stripe_subscription.items.inject({}) { |h, item| h[item.plan.nickname] = item.quantity; h }, th: false)
|
67
|
-
|
68
1
|
- if customer.subscriptions.present?
|
69
2
|
.card.my-4
|
70
|
-
.card-header
|
3
|
+
.card-header Subscriptions
|
71
4
|
.card-body
|
72
5
|
%table.table
|
73
6
|
%thead
|
74
7
|
%tr
|
75
|
-
%th
|
8
|
+
%th Subscribed
|
76
9
|
%th Plan
|
77
|
-
%th
|
10
|
+
%th Quantity
|
11
|
+
%th Interval
|
78
12
|
%tbody
|
79
13
|
- customer.subscriptions.each do |sub|
|
80
14
|
%tr
|
81
15
|
%td= sub.subscribable
|
82
16
|
%td= sub
|
17
|
+
%td= sub.quantity
|
18
|
+
%td= sub.interval
|
83
19
|
|
84
20
|
- if customer.stripe_customer.invoices.present?
|
85
21
|
.card.my-4
|
@@ -94,10 +30,10 @@
|
|
94
30
|
%tbody
|
95
31
|
- customer.stripe_customer.invoices.each do |invoice|
|
96
32
|
%tr
|
97
|
-
%td= Time.zone.at(invoice.date).strftime('%F')
|
33
|
+
%td= link_to Time.zone.at(invoice.date).strftime('%F'), invoice.invoice_pdf
|
98
34
|
%td
|
99
35
|
%p
|
100
|
-
= invoice.
|
36
|
+
= invoice.number
|
101
37
|
%br
|
102
38
|
= Time.zone.at(invoice.lines.first.period.start).strftime('%F')
|
103
39
|
to
|
@@ -1,5 +1,5 @@
|
|
1
1
|
<% resource = (@_effective_resource || Effective::Resource.new(controller_path)) %>
|
2
2
|
<% @resource = instance_variable_get('@' + resource.name) if resource.name %>
|
3
3
|
|
4
|
-
EffectiveForm.remote_form_payload =
|
4
|
+
EffectiveForm.remote_form_payload = "<%= j render('effective/customers/form', subscripter: @subscripter) %>";
|
5
5
|
EffectiveForm.remote_form_flash = <%= raw flash.to_json %>;
|
@@ -1,11 +1,64 @@
|
|
1
1
|
= javascript_include_tag 'https://checkout.stripe.com/checkout.js'
|
2
2
|
|
3
3
|
= effective_form_with(model: subscripter, url: effective_orders.subscripter_path, remote: true, data: { stripe: subscripter_stripe_data(subscripter) }) do |f|
|
4
|
-
= f.hidden_field :user_id, value: current_user.id
|
5
4
|
= f.hidden_field :subscribable_global_id
|
6
5
|
= f.hidden_field :stripe_token, value: nil
|
7
6
|
= f.error :stripe_token
|
8
7
|
|
9
|
-
= f.
|
8
|
+
= f.hidden_field :stripe_plan_id
|
10
9
|
|
11
|
-
|
10
|
+
- stripe_plans = stripe_plans_collection(f.object)
|
11
|
+
- stripe_plans = stripe_plans.select { |plan| plan[:name].include?('per User') }
|
12
|
+
|
13
|
+
- f.object.stripe_plan_id ||= stripe_plans.first[:id]
|
14
|
+
|
15
|
+
- if f.object.subscribable.subscribed?
|
16
|
+
- quantity_used = f.object.subscribable.subscribable_quantity_used.to_i
|
17
|
+
- quantity_purchased = f.object.subscribable.subscription.quantity
|
18
|
+
|
19
|
+
.text-center
|
20
|
+
%p
|
21
|
+
You currently have <strong>#{pluralize(quantity_used, 'member')}</strong> in your team
|
22
|
+
and have space for <strong>#{pluralize(quantity_purchased, 'member')}</strong>.
|
23
|
+
|
24
|
+
- if quantity_purchased > quantity_used
|
25
|
+
%p You can add #{pluralize(quantity_purchased - quantity_used, 'more member')} without updating your plan.
|
26
|
+
- else
|
27
|
+
%p To add more members you will need to update your plan.
|
28
|
+
|
29
|
+
%p To pay for less than #{pluralize(quantity_used, 'member')} you'll have to remove some first.
|
30
|
+
|
31
|
+
.card.mb-4
|
32
|
+
.card-body.text-center
|
33
|
+
%h5.card-title Billing Cycle
|
34
|
+
|
35
|
+
= f.radios :stripe_plan_id, stripe_plans.map { |plan| [plan[:name], plan[:id]] }, label: false, buttons: true
|
36
|
+
|
37
|
+
%hr
|
38
|
+
|
39
|
+
- stripe_plans.select { |plan| plan[:interval] == 'month' }.each do |plan|
|
40
|
+
= f.show_if(:stripe_plan_id, plan[:id]) do
|
41
|
+
.effective-orders-stripe-plan{'data-id': plan[:id], 'data-amount': plan[:amount], 'data-interval': plan[:interval]}
|
42
|
+
.d-flex.justify-content-around.align-items-center
|
43
|
+
= f.number_field :quantity, class: 'effective-orders-subscripter-plan-quantity form-control-lg', autocomplete: 'off', required: true
|
44
|
+
%div= 'x'
|
45
|
+
= f.static_field :price_per_person do
|
46
|
+
= price_to_currency(plan[:amount])
|
47
|
+
%div= '='
|
48
|
+
= f.static_field :total_amount do
|
49
|
+
= price_to_currency(plan[:amount] * f.object.quantity.to_i)
|
50
|
+
|
51
|
+
- stripe_plans.select { |plan| plan[:interval] == 'year' }.each do |plan|
|
52
|
+
= f.show_if(:stripe_plan_id, plan[:id]) do
|
53
|
+
.effective-orders-stripe-plan{'data-id': plan[:id], 'data-amount': plan[:amount], 'data-interval': plan[:interval]}
|
54
|
+
.d-flex.justify-content-around.align-items-center
|
55
|
+
= f.number_field :quantity, class: 'effective-orders-subscripter-plan-quantity form-control-lg', autocomplete: 'off', required: true
|
56
|
+
%div= 'x'
|
57
|
+
= f.static_field :price_per_person do
|
58
|
+
= price_to_currency(plan[:amount] / 12)
|
59
|
+
%div= '='
|
60
|
+
= f.static_field :total_amount do
|
61
|
+
= price_to_currency(plan[:amount] * f.object.quantity.to_i)
|
62
|
+
|
63
|
+
= f.submit(border: false, center: true) do
|
64
|
+
= f.save('Update Plan', class: ('effective-orders-stripe-token-required' if f.object.token_required?))
|
@@ -73,9 +73,6 @@ class CreateEffectiveOrders < ActiveRecord::Migration[4.2]
|
|
73
73
|
|
74
74
|
t.string :stripe_customer_id
|
75
75
|
t.string :active_card
|
76
|
-
|
77
|
-
t.string :stripe_subscription_id
|
78
|
-
t.string :stripe_subscription_interval
|
79
76
|
t.string :status
|
80
77
|
|
81
78
|
t.integer :subscriptions_count, :default => 0
|
@@ -92,7 +89,13 @@ class CreateEffectiveOrders < ActiveRecord::Migration[4.2]
|
|
92
89
|
t.string :subscribable_type
|
93
90
|
|
94
91
|
t.string :stripe_plan_id
|
92
|
+
t.string :stripe_subscription_id
|
93
|
+
|
95
94
|
t.string :name
|
95
|
+
t.string :description
|
96
|
+
t.string :interval
|
97
|
+
t.integer :quantity
|
98
|
+
t.string :status
|
96
99
|
|
97
100
|
t.timestamps
|
98
101
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: effective_orders
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.0
|
4
|
+
version: 4.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Code and Effect
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-04-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -231,8 +231,6 @@ files:
|
|
231
231
|
- app/views/effective/orders_mailer/subscription_trialing.html.haml
|
232
232
|
- app/views/effective/orders_mailer/subscription_updated.html.haml
|
233
233
|
- app/views/effective/subscripter/_form.html.haml
|
234
|
-
- app/views/effective/subscriptions/_plan.html.haml
|
235
|
-
- app/views/effective/subscriptions/_subscription.html.haml
|
236
234
|
- app/views/layouts/effective_orders_mailer_layout.html.haml
|
237
235
|
- config/effective_orders.rb
|
238
236
|
- config/routes.rb
|
@@ -1,20 +0,0 @@
|
|
1
|
-
.card-header.text-center
|
2
|
-
%h3= plan[:name]
|
3
|
-
%p= plan[:description]
|
4
|
-
|
5
|
-
.card-body
|
6
|
-
%p Includes features
|
7
|
-
|
8
|
-
- if plan[:amount] == 0
|
9
|
-
%p Free plan
|
10
|
-
|
11
|
-
.visible-when-unselected.text-center
|
12
|
-
= link_to 'Select', '#', class: 'btn btn-secondary', 'data-toggle': 'card'
|
13
|
-
|
14
|
-
.visible-when-selected.text-center
|
15
|
-
- if subscribed
|
16
|
-
%p
|
17
|
-
%strong Your current plan
|
18
|
-
- else
|
19
|
-
%p This is #{['an amazing', 'a great', 'an excellent', 'an exceptional', 'a marvelous', 'a wonderful'].sample} plan
|
20
|
-
|