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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3dc3ed0ca01bf36b49326b92714290faff406bb8
4
- data.tar.gz: eb4eec1d4d0d58c97e3554a96357147b5b65ab90
3
+ metadata.gz: f15a457703f171efde10a8a750dcd7b8c7092e3f
4
+ data.tar.gz: 182e31f6db68aac1ff8f6a50fdf405f5e3307ae5
5
5
  SHA512:
6
- metadata.gz: c29cc4865ee5d538a801640dfca5986c8cff2924cc06767fece44a959cfbe2d17cdb82a55df6535e2af386317bd909b154646cee9cc1e8809a22937a24216775
7
- data.tar.gz: 2eb4b69106c3080f17e46badefc133749ddd1e33359b926ff7798d8b83af219db256faf7586682da4aeb9e0066a1c6a8e1ca890eda82c026614b73bc1c9714b3
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 [effective_form_inputs](https://github.com/code-and-effect/effective_form_inputs) gems.
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
- amount: plan.amount
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', redirect: :back, success: -> { 'Successfully updated card.' }
7
+ submit :save, 'Save', success: -> { 'Successfully updated card.' }
8
8
  page_title 'Customer Settings'
9
9
 
10
10
  def resource
11
- @customer ||= Effective::Customer.where(user: current_user).first!
12
- @subscripter ||= Effective::Subscripter.new(customer: @customer, user: @customer.user)
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(user: current_user)
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
- customer.update_attributes!(status: EffectiveOrders::ACTIVE)
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
- customer.update_attributes!(status: EffectiveOrders::PAST_DUE)
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
- customer.update_attributes!(stripe_subscription_id: nil, status: nil, active_card: nil)
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 render_plan(plan, subscribed: true)
4
- render(render_plan_partial(plan), plan: plan, subscribed: subscribed)
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, numericality: { less_than_or_equal_to: 2000000000, message: 'maximum price is $20,000,000' }
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 ||= Effective::Subscripter.new(subscribable: self, user: subscribable_buyer)
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
- end
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, :stripe_subscription
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? && stripe_subscription_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? || (active_card.present? && stripe_subscription_id.present? && status != 'active')
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 status == 'past_due'
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 status == 'active'
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, :include_trial
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 stripe_plan_id
45
- @stripe_plan_id || (current_plan[:id] if current_plan)
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
- build_subscription!
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.destroy!
68
- subscribable.update_column(:subscription_status, nil)
69
- sync_subscription!
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.blank?
78
- Rails.logger.info "[STRIPE] create customer: #{user.email}"
79
- customer.stripe_customer = Stripe::Customer.create(email: user.email, description: user.to_s, metadata: { user_id: user.id })
80
- customer.stripe_customer_id = customer.stripe_customer.id
81
- customer.save!
82
- end
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.present?
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
- def build_subscription!
101
- return unless plan.present?
102
- subscription.stripe_plan_id = plan[:id]
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
- def sync_subscription!
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 create_subscription!
104
+ def save_subscription!
113
105
  return unless plan.present?
114
- return if customer.stripe_subscription.present?
115
106
 
116
- Rails.logger.info "[STRIPE] create subscription: #{items(metadata: false)}"
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
- customer.status = customer.stripe_subscription.status
121
- customer.stripe_subscription_interval = customer.stripe_subscription.plan.interval
109
+ cancel_subscription!
110
+ create_subscription! || update_subscription!
111
+ true
122
112
  end
123
113
 
124
- def update_subscription!
125
- return unless plan.present?
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
- if items.length == 0
131
- customer.stripe_subscription.delete
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
- # Update stripe subscription items. Keep track if quantity changed here or not
138
- quantity_increased = false
120
+ return false unless stripe_item.present? && item[:plan] != stripe_item['plan']['id']
139
121
 
140
- customer.stripe_subscription.items.each do |stripe_item|
141
- item = items.find { |item| item[:plan] == stripe_item['plan']['id'] }
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
- next if item.blank? || item[:quantity] == stripe_item['quantity']
144
-
145
- quantity_increased ||= (item[:quantity] > stripe_item['quantity']) # Any quantity increased
126
+ true
127
+ end
146
128
 
147
- stripe_item.quantity = item[:quantity]
148
- stripe_item.metadata = item[:metadata]
129
+ def create_subscription!
130
+ return false unless subscription.stripe_subscription.blank?
149
131
 
150
- Rails.logger.info " -> [STRIPE] update plan: #{item[:plan]}"
151
- stripe_item.save
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
- # Create stripe subscription items
155
- items.each do |item|
156
- next if customer.stripe_subscription.items.find { |stripe_item| item[:plan] == stripe_item['plan']['id'] }
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
- Rails.logger.info " -> [STRIPE] create plan: #{item[:plan]}"
159
- customer.stripe_subscription.items.create(plan: item[:plan], quantity: item[:quantity], metadata: item[:metadata])
160
- end
145
+ def update_subscription!
146
+ return false unless subscription.stripe_subscription.present?
161
147
 
162
- # Delete stripe subscription items
163
- customer.stripe_subscription.items.each do |stripe_item|
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
- Rails.logger.info " -> [STRIPE] delete plan: #{stripe_item['plan']['id']}"
167
- stripe_item.delete
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
- # Update metadata
171
- if customer.stripe_subscription.metadata.to_h != metadata
172
- Rails.logger.info " -> [STRIPE] update metadata: #{metadata}"
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
- # When upgrading a plan, invoice immediately.
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
- customer.status = customer.stripe_subscription.status
184
- end
160
+ # Invoice immediately
161
+ Rails.logger.info " -> [STRIPE] generate invoice"
162
+ Stripe::Invoice.create(customer: customer.stripe_customer_id).pay
185
163
 
186
- def subscribe!(stripe_plan_id)
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(metadata: true)
199
- customer.subscriptions.group_by { |sub| sub.stripe_plan_id }.map do |plan, subscriptions|
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(subscriptions: nil)
210
- retval = { user_id: user.id.to_s, user: user.to_s.truncate(500) }
211
-
212
- (subscriptions || customer.subscriptions).group_by { |sub| sub.subscribable_type }.each do |subscribable_type, subs|
213
- subs = subs.sort
214
-
215
- if subs.length == 1
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 :string
12
- # name :string
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 = "#{plan[:name]} #{plan[:description]}"
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 Subscribed
3
+ .card-header Subscriptions
71
4
  .card-body
72
5
  %table.table
73
6
  %thead
74
7
  %tr
75
- %th Name
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.id
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 = $("<div><%= j render_resource_form(resource, subscripter: @subscripter) %></div>");
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.radios :stripe_plan_id, stripe_plans_collection(f), inline: false, cards: false, label: false, required: true
8
+ = f.hidden_field :stripe_plan_id
10
9
 
11
- = f.submit 'Choose Plan', center: true, border: false, class: ('effective-orders-stripe-token-required' if f.object.token_required?)
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
@@ -1,3 +1,3 @@
1
1
  module EffectiveOrders
2
- VERSION = '4.0.6'.freeze
2
+ VERSION = '4.1.0'.freeze
3
3
  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.6
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: 2018-10-12 00:00:00.000000000 Z
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
-
@@ -1,3 +0,0 @@
1
- .effective-radios.card-deck
2
- .card.active
3
- = render_plan(subscription.plan)