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