effective_orders 2.2.4 → 3.0.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/MIT-LICENSE +1 -1
- data/README.md +124 -84
- data/app/assets/javascripts/effective_orders/customers.js.coffee +39 -0
- data/app/assets/javascripts/effective_orders/providers/{stripe_charges.js.coffee → stripe.js.coffee} +15 -13
- data/app/assets/javascripts/effective_orders/subscriptions.js.coffee +73 -0
- data/app/assets/stylesheets/effective_orders.scss +2 -1
- data/app/assets/stylesheets/effective_orders/_order.scss +16 -8
- data/app/assets/stylesheets/effective_orders/_subscriptions.scss +14 -0
- data/app/controllers/admin/customers_controller.rb +11 -8
- data/app/controllers/admin/order_items_controller.rb +4 -8
- data/app/controllers/admin/orders_controller.rb +133 -87
- data/app/controllers/effective/carts_controller.rb +18 -8
- data/app/controllers/effective/concerns/purchase.rb +39 -0
- data/app/controllers/effective/customers_controller.rb +43 -0
- data/app/controllers/effective/orders_controller.rb +73 -119
- data/app/controllers/effective/providers/app_checkout.rb +3 -1
- data/app/controllers/effective/providers/ccbill.rb +4 -6
- data/app/controllers/effective/providers/cheque.rb +20 -11
- data/app/controllers/effective/providers/free.rb +33 -0
- data/app/controllers/effective/providers/mark_as_paid.rb +33 -0
- data/app/controllers/effective/providers/moneris.rb +9 -17
- data/app/controllers/effective/providers/paypal.rb +4 -6
- data/app/controllers/effective/providers/pretend.rb +4 -4
- data/app/controllers/effective/providers/refund.rb +39 -0
- data/app/controllers/effective/providers/stripe.rb +19 -40
- data/app/controllers/effective/providers/stripe_connect.rb +2 -6
- data/app/controllers/effective/webhooks_controller.rb +44 -95
- data/app/datatables/effective_customers_datatable.rb +21 -29
- data/app/datatables/effective_order_items_datatable.rb +77 -79
- data/app/datatables/effective_orders_datatable.rb +67 -57
- data/app/helpers/effective_carts_helper.rb +17 -14
- data/app/helpers/effective_orders_helper.rb +40 -56
- data/app/helpers/effective_paypal_helper.rb +3 -3
- data/app/helpers/effective_stripe_helper.rb +47 -18
- data/app/helpers/effective_subscriptions_helper.rb +79 -0
- data/app/mailers/effective/orders_mailer.rb +125 -2
- data/app/models/concerns/acts_as_purchasable.rb +23 -33
- data/app/models/concerns/acts_as_subscribable.rb +68 -0
- data/app/models/concerns/acts_as_subscribable_buyer.rb +22 -0
- data/app/models/effective/cart.rb +53 -24
- data/app/models/effective/cart_item.rb +6 -12
- data/app/models/effective/customer.rb +51 -54
- data/app/models/effective/order.rb +160 -147
- data/app/models/effective/order_item.rb +18 -21
- data/app/models/effective/product.rb +7 -7
- data/app/models/effective/providers/ccbill_postback.rb +1 -1
- data/app/models/effective/providers/stripe_charge.rb +8 -19
- data/app/models/effective/subscripter.rb +230 -0
- data/app/models/effective/subscription.rb +27 -76
- data/app/models/effective/tax_rate_calculator.rb +10 -7
- data/app/views/admin/customers/_actions.html.haml +1 -2
- data/app/views/admin/customers/index.html.haml +1 -1
- data/app/views/admin/customers/show.html.haml +6 -0
- data/app/views/admin/orders/_actions.html.haml +9 -7
- data/app/views/admin/orders/_form.html.haml +11 -7
- data/app/views/admin/orders/_order_actions.html.haml +2 -1
- data/app/views/admin/orders/_order_item_fields.html.haml +1 -1
- data/app/views/admin/orders/edit.html.haml +4 -0
- data/app/views/admin/orders/index.html.haml +1 -4
- data/app/views/admin/orders/new.html.haml +1 -1
- data/app/views/admin/orders/show.html.haml +5 -6
- data/app/views/effective/carts/_cart.html.haml +2 -2
- data/app/views/effective/carts/show.html.haml +2 -2
- data/app/views/effective/customers/_customer.html.haml +152 -0
- data/app/views/effective/customers/_fields.html.haml +12 -0
- data/app/views/effective/customers/_form.html.haml +13 -0
- data/app/views/effective/customers/edit.html.haml +3 -0
- data/app/views/effective/orders/_checkout_step1.html.haml +8 -15
- data/app/views/effective/orders/_checkout_step2.html.haml +34 -21
- data/app/views/effective/orders/_order.html.haml +8 -9
- data/app/views/effective/orders/_order_actions.html.haml +7 -8
- data/app/views/effective/orders/_order_header.html.haml +1 -1
- data/app/views/effective/orders/_order_items.html.haml +11 -5
- data/app/views/effective/orders/_order_note.html.haml +4 -7
- data/app/views/effective/orders/_orders_table.html.haml +26 -26
- data/app/views/effective/orders/app_checkout/_form.html.haml +2 -2
- data/app/views/effective/orders/ccbill/_form.html.haml +1 -1
- data/app/views/effective/orders/cheque/_form.html.haml +3 -1
- data/app/views/effective/orders/declined.html.haml +1 -1
- data/app/views/effective/orders/{checkout_step1.html.haml → edit.html.haml} +0 -0
- data/app/views/effective/orders/free/_form.html.haml +4 -0
- data/app/views/effective/orders/index.html.haml +2 -4
- data/app/views/effective/orders/mark_as_paid/_form.html.haml +32 -0
- data/app/views/effective/orders/moneris/_form.html.haml +6 -6
- data/app/views/effective/orders/{checkout_step2.html.haml → new.html.haml} +1 -1
- data/app/views/effective/orders/paypal/_form.html.haml +2 -2
- data/app/views/effective/orders/pretend/_form.html.haml +2 -2
- data/app/views/effective/orders/purchased.html.haml +3 -0
- data/app/views/effective/orders/refund/_form.html.haml +32 -0
- data/app/views/effective/orders/show.html.haml +4 -1
- data/app/views/effective/orders/stripe/_form.html.haml +5 -5
- data/app/views/effective/orders_mailer/subscription_canceled.html.haml +9 -0
- data/app/views/effective/orders_mailer/subscription_payment_failed.html.haml +9 -0
- data/app/views/effective/orders_mailer/subscription_payment_succeeded.html.haml +9 -0
- data/app/views/effective/orders_mailer/subscription_trial_expired.html.haml +5 -0
- data/app/views/effective/orders_mailer/subscription_trial_expiring.html.haml +7 -0
- data/app/views/effective/subscriptions/_fields.html.haml +16 -0
- data/app/views/effective/subscriptions/_plan.html.haml +21 -0
- data/app/views/layouts/effective_orders_mailer_layout.html.haml +6 -8
- data/config/effective_orders.rb +41 -20
- data/config/routes.rb +48 -48
- data/db/migrate/01_create_effective_orders.rb.erb +19 -5
- data/lib/effective_orders.rb +78 -42
- data/lib/effective_orders/engine.rb +36 -82
- data/lib/effective_orders/version.rb +1 -1
- data/lib/generators/effective_orders/install_generator.rb +2 -2
- data/lib/generators/templates/effective_orders_mailer_preview.rb +39 -4
- data/lib/tasks/effective_orders_tasks.rake +42 -0
- data/spec/controllers/carts_controller_spec.rb +1 -1
- data/spec/controllers/moneris_orders_controller_spec.rb +4 -4
- data/spec/controllers/orders_controller_spec.rb +4 -4
- data/spec/controllers/stripe_orders_controller_spec.rb +2 -2
- data/spec/controllers/webhooks_controller_spec.rb +1 -1
- data/spec/dummy/config/initializers/effective_orders.rb +1 -7
- data/spec/dummy/db/schema.rb +1 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/test.log +3 -0
- data/spec/models/acts_as_purchasable_spec.rb +0 -56
- data/spec/models/customer_spec.rb +3 -3
- data/spec/models/order_spec.rb +2 -2
- data/spec/spec_helper.rb +1 -1
- data/spec/support/factories.rb +2 -1
- metadata +37 -49
- data/active_admin/effective_carts.rb +0 -14
- data/active_admin/effective_orders.rb +0 -112
- data/app/assets/javascripts/effective_orders/providers/stripe_subscriptions.js.coffee +0 -28
- data/app/controllers/concerns/acts_as_active_admin_controller.rb +0 -69
- data/app/controllers/effective/subscriptions_controller.rb +0 -126
- data/app/models/effective/datatables/customers.rb +0 -40
- data/app/models/effective/datatables/order_items.rb +0 -101
- data/app/models/effective/datatables/orders.rb +0 -91
- data/app/models/inputs/price_field.rb +0 -63
- data/app/models/inputs/price_form_input.rb +0 -7
- data/app/models/inputs/price_formtastic_input.rb +0 -9
- data/app/models/inputs/price_input.rb +0 -19
- data/app/models/inputs/price_simple_form_input.rb +0 -8
- data/app/views/admin/orders/_form_mark_as_paid.html.haml +0 -33
- data/app/views/admin/orders/_order_payment_details.html.haml +0 -5
- data/app/views/admin/orders/mark_as_paid.html.haml +0 -7
- data/app/views/effective/orders/stripe/_subscription_fields.html.haml +0 -7
- data/app/views/effective/subscriptions/index.html.haml +0 -22
- data/app/views/effective/subscriptions/new.html.haml +0 -9
- data/app/views/effective/subscriptions/show.html.haml +0 -49
- data/db/upgrade/02_upgrade_effective_orders_from03x.rb.erb +0 -29
- data/db/upgrade/03_upgrade_effective_orders_from1x.rb.erb +0 -98
- data/db/upgrade/upgrade_price_column_on_table.rb.erb +0 -17
- data/lib/generators/effective_orders/upgrade_from03x_generator.rb +0 -31
- data/lib/generators/effective_orders/upgrade_from1x_generator.rb +0 -27
- data/lib/generators/effective_orders/upgrade_price_column_generator.rb +0 -30
@@ -2,34 +2,32 @@ module Effective
|
|
2
2
|
class OrderItem < ActiveRecord::Base
|
3
3
|
self.table_name = EffectiveOrders.order_items_table_name.to_s
|
4
4
|
|
5
|
-
belongs_to :order
|
6
|
-
belongs_to :purchasable, :
|
7
|
-
belongs_to :seller, :
|
5
|
+
belongs_to :order, class_name: 'Effective::Order'
|
6
|
+
belongs_to :purchasable, polymorphic: true
|
7
|
+
belongs_to :seller, class_name: 'User'
|
8
8
|
|
9
|
-
delegate :purchased_download_url, :
|
10
|
-
delegate :purchased?, :declined?, :
|
9
|
+
delegate :purchased_download_url, to: :purchasable
|
10
|
+
delegate :purchased?, :declined?, to: :order
|
11
11
|
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
# timestamps
|
19
|
-
# end
|
12
|
+
# Attributes
|
13
|
+
# title :string
|
14
|
+
# quantity :integer
|
15
|
+
# price :integer, default: 0
|
16
|
+
# tax_exempt :boolean
|
17
|
+
# timestamps
|
20
18
|
|
21
19
|
validates :purchasable, associated: true, presence: true
|
22
20
|
accepts_nested_attributes_for :purchasable, allow_destroy: false, reject_if: :all_blank, update_only: true
|
23
21
|
|
24
22
|
validates :title, presence: true
|
25
23
|
validates :quantity, presence: true, numericality: { greater_than: 0 }
|
26
|
-
validates :price,
|
24
|
+
validates :price, presence: true
|
27
25
|
validates :tax_exempt, inclusion: { in: [true, false] }
|
28
26
|
|
29
|
-
validates :seller_id, presence: true, if:
|
27
|
+
validates :seller_id, presence: true, if: -> { EffectiveOrders.stripe_connect_enabled }
|
30
28
|
|
31
|
-
scope :sold, -> { joins(:order).where(:
|
32
|
-
scope :sold_by, lambda { |user| sold().where(:
|
29
|
+
scope :sold, -> { joins(:order).where(orders: { purchase_state: EffectiveOrders::PURCHASED }) }
|
30
|
+
scope :sold_by, lambda { |user| sold().where(seller_id: user.id) }
|
33
31
|
|
34
32
|
def to_s
|
35
33
|
(quantity || 0) > 1 ? "#{quantity}x #{title}" : title
|
@@ -57,8 +55,7 @@ module Effective
|
|
57
55
|
elsif value.kind_of?(String) && !value.include?('.') # Looks like an integer
|
58
56
|
super
|
59
57
|
else # Could be Float, BigDecimal, or String like 9.99
|
60
|
-
|
61
|
-
super((value.to_f * 100.0).to_i)
|
58
|
+
raise 'expected price to be an Integer representing the number of cents.'
|
62
59
|
end
|
63
60
|
end
|
64
61
|
|
@@ -66,13 +63,13 @@ module Effective
|
|
66
63
|
# And is the Customer representing who is selling the product
|
67
64
|
# This is really only used for StripeConnect
|
68
65
|
def seller
|
69
|
-
@seller ||= Effective::Customer.
|
66
|
+
@seller ||= Effective::Customer.for_buyer(purchasable.seller)
|
70
67
|
end
|
71
68
|
|
72
69
|
def stripe_connect_application_fee
|
73
70
|
@stripe_connect_application_fee ||= (
|
74
71
|
self.instance_exec(self, &EffectiveOrders.stripe_connect_application_fee_method).to_i.tap do |fee|
|
75
|
-
raise
|
72
|
+
raise "expected EffectiveOrders.stripe_connect_application_fee_method to return a value between 0 and the order_item total (#{self.total}). Received #{fee}." if (fee > total || fee < 0)
|
76
73
|
end
|
77
74
|
)
|
78
75
|
end
|
@@ -4,16 +4,16 @@ module Effective
|
|
4
4
|
|
5
5
|
acts_as_purchasable
|
6
6
|
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
7
|
+
# Attributes
|
8
|
+
# title :string
|
9
|
+
# price :integer, default: 0
|
10
|
+
# tax_exempt :boolean, default: false
|
11
11
|
#
|
12
|
-
#
|
13
|
-
# end
|
12
|
+
# timestamps
|
14
13
|
|
15
14
|
validates :title, presence: true
|
16
|
-
validates :price,
|
15
|
+
validates :price, presence: true
|
16
|
+
validates :tax_exempt, inclusion: { in: [true, false] }
|
17
17
|
|
18
18
|
def to_s
|
19
19
|
self[:title] || 'New Product'
|
@@ -20,7 +20,7 @@ class Effective::Providers::CcbillPostback
|
|
20
20
|
|
21
21
|
def denial?
|
22
22
|
# denialId is for subscriptions only
|
23
|
-
[:reasonForDeclineCode, :reasonForDecline, :denialId].any? {|key| params[key].present?}
|
23
|
+
[:reasonForDeclineCode, :reasonForDecline, :denialId].any? { |key| params[key].present? }
|
24
24
|
end
|
25
25
|
|
26
26
|
def order_details
|
@@ -1,34 +1,23 @@
|
|
1
1
|
# This is an object for the stripe charge form
|
2
2
|
module Effective::Providers
|
3
3
|
class StripeCharge
|
4
|
-
|
5
|
-
include ActiveModel::Conversion
|
6
|
-
include ActiveModel::Validations
|
7
|
-
include ActiveRecord::Reflection
|
4
|
+
include ActiveModel::Model
|
8
5
|
|
9
|
-
attr_accessor :effective_order_id, :order, :
|
6
|
+
attr_accessor :effective_order_id, :order, :stripe_token # For our form
|
10
7
|
|
11
|
-
|
12
|
-
|
13
|
-
def initialize(params = {})
|
14
|
-
if params.kind_of?(Effective::Order)
|
15
|
-
@order = params
|
16
|
-
@effective_order_id = params.to_param
|
17
|
-
else
|
18
|
-
params.each { |k, v| self.send("#{k}=", v) if self.respond_to?("#{k}=") }
|
19
|
-
end
|
20
|
-
end
|
8
|
+
validates :effective_order_id, presence: true
|
9
|
+
validates :stripe_token, presence: true
|
21
10
|
|
22
11
|
def persisted?
|
23
12
|
false
|
24
13
|
end
|
25
14
|
|
26
|
-
def
|
27
|
-
order.
|
15
|
+
def effective_order_id
|
16
|
+
@effective_order_id || (order.to_param if order)
|
28
17
|
end
|
29
18
|
|
30
|
-
def
|
31
|
-
order.order_items
|
19
|
+
def order_items
|
20
|
+
order.order_items if order
|
32
21
|
end
|
33
22
|
|
34
23
|
end
|
@@ -0,0 +1,230 @@
|
|
1
|
+
# Form object to select a plan, and build the correct subscription and customer
|
2
|
+
|
3
|
+
module Effective
|
4
|
+
class Subscripter
|
5
|
+
include ActiveModel::Model
|
6
|
+
|
7
|
+
attr_accessor :user, :subscribable, :customer
|
8
|
+
attr_accessor :stripe_plan_id, :stripe_token
|
9
|
+
|
10
|
+
validates :user, presence: true
|
11
|
+
validates :subscribable, presence: true, if: -> { stripe_plan_id }
|
12
|
+
validates :stripe_plan_id, inclusion: { allow_blank: true, in: EffectiveOrders.stripe_plans.keys, message: 'unknown plan' }
|
13
|
+
|
14
|
+
validate(if: -> { stripe_plan_id && plan && subscribable }) do
|
15
|
+
if plan[:amount] > 0 && stripe_token.blank? && token_required?
|
16
|
+
self.errors.add(:stripe_token, 'updated payment card required')
|
17
|
+
customer.errors.add(:stripe_token, 'updated payment card required')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
validate(if: -> { subscribable }) do
|
22
|
+
subscribable.errors.add(:subscripter, errors.messages.values.flatten.to_sentence) if self.errors.present?
|
23
|
+
end
|
24
|
+
|
25
|
+
def customer
|
26
|
+
@customer ||= Effective::Customer.deep.where(user: user).first_or_initialize
|
27
|
+
end
|
28
|
+
|
29
|
+
def current_plan
|
30
|
+
return nil unless subscribable
|
31
|
+
subscribable.subscription.blank? ? EffectiveOrders.stripe_plans['trial'] : subscribable.subscription.plan
|
32
|
+
end
|
33
|
+
|
34
|
+
def plan
|
35
|
+
EffectiveOrders.stripe_plans[stripe_plan_id]
|
36
|
+
end
|
37
|
+
|
38
|
+
def token_required?
|
39
|
+
customer.token_required?
|
40
|
+
end
|
41
|
+
|
42
|
+
def save!
|
43
|
+
return true if (plan == current_plan) && stripe_token.blank? # No work to do
|
44
|
+
|
45
|
+
raise 'is invalid' unless valid?
|
46
|
+
|
47
|
+
begin
|
48
|
+
build! && sync! && customer.save!
|
49
|
+
rescue => e
|
50
|
+
reload!
|
51
|
+
|
52
|
+
self.errors.add(:base, e.message)
|
53
|
+
raise e
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def subscribe!(stripe_plan_id)
|
58
|
+
self.stripe_plan_id = stripe_plan_id
|
59
|
+
save!
|
60
|
+
end
|
61
|
+
|
62
|
+
def destroy!
|
63
|
+
return true unless subscription && subscription.persisted? && customer.stripe_subscription.present?
|
64
|
+
|
65
|
+
raise 'is invalid' unless valid?
|
66
|
+
|
67
|
+
subscription.destroy!
|
68
|
+
customer.subscriptions.reload
|
69
|
+
|
70
|
+
sync! && customer.save!
|
71
|
+
end
|
72
|
+
|
73
|
+
def reload!
|
74
|
+
@stripe_token = nil
|
75
|
+
@stripe_plan_id = nil
|
76
|
+
@customer = nil
|
77
|
+
@subscription = nil
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def subscription
|
83
|
+
return nil unless subscribable
|
84
|
+
@subscription ||= (
|
85
|
+
customer.subscriptions.find { |sub| sub.subscribable == subscribable } ||
|
86
|
+
customer.subscriptions.build(subscribable: subscribable, customer: customer)
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
90
|
+
def build!
|
91
|
+
# Check for an existing customer created within the last hour with this email
|
92
|
+
# This catches an edge case in which a stripe customer is created twice when a first time customer is using a declined card
|
93
|
+
if customer.stripe_customer.blank?
|
94
|
+
Rails.logger.info "STRIPE CUSTOMER CHECK FOR EXISTING: #{user.email}"
|
95
|
+
|
96
|
+
customers = Stripe::Customer.list(created: { gt: (user.created_at - 1.hour).to_i } ).data
|
97
|
+
|
98
|
+
if (existing = customers.find { |cus| cus.email == user.email && (cus.metadata || {})[:user_id] == user.id.to_s })
|
99
|
+
customer.stripe_customer = existing
|
100
|
+
customer.stripe_customer_id = existing.id
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Create customer
|
105
|
+
if customer.stripe_customer.blank?
|
106
|
+
Rails.logger.info "STRIPE CUSTOMER CREATE: #{user.email}"
|
107
|
+
customer.stripe_customer = Stripe::Customer.create(email: user.email, description: user.to_s, metadata: { user_id: user.id })
|
108
|
+
customer.stripe_customer_id = customer.stripe_customer.id
|
109
|
+
end
|
110
|
+
|
111
|
+
# Update stripe customer card
|
112
|
+
if stripe_token.present?
|
113
|
+
Rails.logger.info "STRIPE CUSTOMER SOURCE UPDATE #{stripe_token}"
|
114
|
+
customer.stripe_customer.source = stripe_token
|
115
|
+
customer.stripe_customer.save
|
116
|
+
|
117
|
+
if customer.stripe_customer.default_source.present?
|
118
|
+
card = customer.stripe_customer.sources.retrieve(customer.stripe_customer.default_source)
|
119
|
+
customer.active_card = "**** **** **** #{card.last4} #{card.brand} #{card.exp_month}/#{card.exp_year}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Assign stripe plan
|
124
|
+
if plan
|
125
|
+
subscription.stripe_plan_id = plan[:id]
|
126
|
+
|
127
|
+
# Ensure stripe subscription exists
|
128
|
+
if customer.stripe_subscription.blank?
|
129
|
+
Rails.logger.info "STRIPE SUBSCRIPTION CREATE: #{items(metadata: false)}"
|
130
|
+
customer.stripe_subscription = Stripe::Subscription.create(customer: customer.stripe_customer_id, items: items(metadata: false), metadata: metadata)
|
131
|
+
customer.stripe_subscription_id = customer.stripe_subscription.id
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
true
|
136
|
+
end
|
137
|
+
|
138
|
+
def sync!
|
139
|
+
return true unless plan && customer.stripe_subscription
|
140
|
+
|
141
|
+
Rails.logger.info "STRIPE SUBSCRIPTION SYNC: #{customer.stripe_subscription_id} #{items}"
|
142
|
+
|
143
|
+
if items.length == 0
|
144
|
+
customer.stripe_subscription.delete
|
145
|
+
customer.stripe_subscription_id = nil
|
146
|
+
customer.status = nil
|
147
|
+
return true
|
148
|
+
end
|
149
|
+
|
150
|
+
# Update stripe subscription items
|
151
|
+
customer.stripe_subscription.items.each do |stripe_item|
|
152
|
+
item = items.find { |item| item[:plan] == stripe_item['plan']['id'] }
|
153
|
+
|
154
|
+
next if item.blank? || item[:quantity] == stripe_item['quantity']
|
155
|
+
|
156
|
+
stripe_item.quantity = item[:quantity]
|
157
|
+
stripe_item.metadata = item[:metadata]
|
158
|
+
|
159
|
+
Rails.logger.info " -> UPDATE: #{item[:plan]}"
|
160
|
+
stripe_item.save
|
161
|
+
end
|
162
|
+
|
163
|
+
# Create stripe subscription items
|
164
|
+
items.each do |item|
|
165
|
+
next if customer.stripe_subscription.items.find { |stripe_item| item[:plan] == stripe_item['plan']['id'] }
|
166
|
+
|
167
|
+
Rails.logger.info " -> CREATE: #{item[:plan]}"
|
168
|
+
customer.stripe_subscription.items.create(plan: item[:plan], quantity: item[:quantity], metadata: item[:metadata])
|
169
|
+
end
|
170
|
+
|
171
|
+
# Delete stripe subscription items
|
172
|
+
customer.stripe_subscription.items.each do |stripe_item|
|
173
|
+
next if items.find { |item| item[:plan] == stripe_item['plan']['id'] }
|
174
|
+
|
175
|
+
Rails.logger.info " -> DELETE: #{stripe_item['plan']['id']}"
|
176
|
+
stripe_item.delete
|
177
|
+
end
|
178
|
+
|
179
|
+
# Update metadata
|
180
|
+
if customer.stripe_subscription.metadata.to_h != metadata
|
181
|
+
Rails.logger.info " -> METATADA: #{metadata}"
|
182
|
+
customer.stripe_subscription.metadata = metadata
|
183
|
+
customer.stripe_subscription.save
|
184
|
+
end
|
185
|
+
|
186
|
+
# When upgrading a plan, invoice immediately.
|
187
|
+
if current_plan && current_plan[:id] != 'trial' && plan[:amount] > current_plan[:amount]
|
188
|
+
Rails.logger.info " -> INVOICE GENERATED"
|
189
|
+
Stripe::Invoice.create(customer: customer.stripe_customer_id).pay rescue false
|
190
|
+
end
|
191
|
+
|
192
|
+
# Sync status
|
193
|
+
customer.status = customer.stripe_subscription.status
|
194
|
+
|
195
|
+
true
|
196
|
+
end
|
197
|
+
|
198
|
+
private
|
199
|
+
|
200
|
+
def items(metadata: true)
|
201
|
+
customer.subscriptions.group_by { |sub| sub.stripe_plan_id }.map do |plan, subscriptions|
|
202
|
+
if metadata
|
203
|
+
{ plan: plan, quantity: subscriptions.length, metadata: metadata(subscriptions: subscriptions) }
|
204
|
+
else
|
205
|
+
{ plan: plan, quantity: subscriptions.length }
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# The stripe metadata limit is 500 characters
|
211
|
+
def metadata(subscriptions: nil)
|
212
|
+
retval = { user_id: user.id.to_s, user: user.to_s.truncate(500) }
|
213
|
+
|
214
|
+
(subscriptions || customer.subscriptions).group_by { |sub| sub.subscribable_type }.each do |subscribable_type, subs|
|
215
|
+
subs = subs.sort
|
216
|
+
|
217
|
+
if subs.length == 1
|
218
|
+
retval[subscribable_type.downcase + '_id'] = subs.map { |sub| sub.subscribable.id }.join(',')
|
219
|
+
retval[subscribable_type.downcase] = subs.map { |sub| sub.subscribable.to_s }.join(',').truncate(500)
|
220
|
+
else
|
221
|
+
retval[subscribable_type.downcase + '_ids'] = subs.map { |sub| sub.subscribable.id }.join(',')
|
222
|
+
retval[subscribable_type.downcase.pluralize] = subs.map { |sub| sub.subscribable.to_s }.join(',').truncate(500)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
retval
|
227
|
+
end
|
228
|
+
|
229
|
+
end
|
230
|
+
end
|
@@ -1,98 +1,49 @@
|
|
1
|
+
# This links the acts_as_subscribable_buyer (customer) to the acts_as_subscribable (subscribable)
|
2
|
+
|
1
3
|
module Effective
|
2
4
|
class Subscription < ActiveRecord::Base
|
3
|
-
include EffectiveStripeHelper
|
4
|
-
|
5
5
|
self.table_name = EffectiveOrders.subscriptions_table_name.to_s
|
6
6
|
|
7
|
-
|
7
|
+
belongs_to :customer, class_name: 'Effective::Customer', counter_cache: true
|
8
|
+
belongs_to :subscribable, polymorphic: true
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
#
|
12
|
-
# stripe_plan_id :string # This will be 'Weekly' or something like that
|
13
|
-
# stripe_subscription_id :string
|
14
|
-
# stripe_coupon_id :string
|
10
|
+
# Attributes
|
11
|
+
# stripe_plan_id :string # This will be 'bronze' or something like that
|
12
|
+
# status :string
|
15
13
|
#
|
16
|
-
#
|
17
|
-
#
|
14
|
+
# name :string
|
15
|
+
# price :integer, default: 0
|
18
16
|
#
|
19
|
-
#
|
20
|
-
# end
|
21
|
-
|
22
|
-
delegate :user, :user_id, :to => :customer
|
23
|
-
|
24
|
-
validates :stripe_plan_id, presence: true
|
25
|
-
validates :title, presence: true
|
26
|
-
validates :price, numericality: { greater_than: 0 }
|
27
|
-
|
28
|
-
validates :customer, presence: true
|
29
|
-
validates :customer_id, uniqueness: { scope: [:stripe_plan_id] } # Can only be on each plan once.
|
30
|
-
|
31
|
-
before_validation do
|
32
|
-
self.errors.add(:stripe_plan_id, "is an invalid Plan") if stripe_plan_id.present? && stripe_plan.blank?
|
33
|
-
self.errors.add(:stripe_coupon_id, "is an invalid Coupon") if stripe_coupon_id.present? && stripe_coupon.blank?
|
34
|
-
end
|
35
|
-
|
36
|
-
def tax_exempt
|
37
|
-
true
|
38
|
-
end
|
39
|
-
|
40
|
-
def stripe_plan_id=(plan_id)
|
41
|
-
unless self[:stripe_plan_id] == plan_id
|
42
|
-
self[:stripe_plan_id] = plan_id
|
43
|
-
@stripe_plan = nil # Remove any memoization
|
17
|
+
# timestamps
|
44
18
|
|
45
|
-
|
46
|
-
|
19
|
+
before_validation(if: -> { plan && (stripe_plan_id_changed? || new_record?) }) do
|
20
|
+
self.name = "#{plan[:name]} #{plan[:description]}"
|
21
|
+
self.price = plan[:amount]
|
47
22
|
end
|
48
23
|
|
49
|
-
|
50
|
-
|
51
|
-
self[:stripe_coupon_id] = coupon_id
|
52
|
-
@stripe_coupon = nil # Remove any memoization
|
24
|
+
validates :customer, presence: true
|
25
|
+
validates :subscribable, presence: true
|
53
26
|
|
54
|
-
|
55
|
-
|
56
|
-
end
|
27
|
+
validates :stripe_plan_id, presence: true, inclusion: { in: EffectiveOrders.stripe_plans.except('trial').keys }
|
28
|
+
validates :status, presence: true, inclusion: { in: %w(active past_due) }
|
57
29
|
|
58
|
-
|
59
|
-
|
60
|
-
@stripe_plan ||= (Stripe::Plan.retrieve(stripe_plan_id) rescue nil)
|
61
|
-
end
|
62
|
-
end
|
30
|
+
validates :name, presence: true
|
31
|
+
validates :price, numericality: { greater_than_or_equal_to: 0, only_integer: true }
|
63
32
|
|
64
|
-
def
|
65
|
-
|
66
|
-
@stripe_coupon ||= (Stripe::Coupon.retrieve(stripe_coupon_id) rescue nil)
|
67
|
-
end
|
33
|
+
def to_s
|
34
|
+
name.presence || 'New Subscription'
|
68
35
|
end
|
69
36
|
|
70
|
-
def
|
71
|
-
|
72
|
-
@stripe_subscription ||= (customer.stripe_customer.subscriptions.retrieve(stripe_subscription_id) rescue nil)
|
73
|
-
end
|
37
|
+
def plan
|
38
|
+
EffectiveOrders.stripe_plans[stripe_plan_id]
|
74
39
|
end
|
75
40
|
|
76
|
-
|
77
|
-
|
78
|
-
def assign_price_and_title
|
79
|
-
if stripe_plan
|
80
|
-
if stripe_coupon
|
81
|
-
self.price = price_with_coupon(stripe_plan.amount, stripe_coupon)
|
82
|
-
self.title = stripe_plan_description(stripe_plan) + '<br>Coupon Code: ' + stripe_coupon_description(stripe_coupon)
|
83
|
-
else
|
84
|
-
self.title = stripe_plan_description(stripe_plan)
|
85
|
-
self.price = stripe_plan.amount
|
86
|
-
end
|
87
|
-
end
|
41
|
+
def active?
|
42
|
+
status == 'active'
|
88
43
|
end
|
89
44
|
|
90
|
-
def
|
91
|
-
|
92
|
-
(amount * (coupon.percent_off.to_i / 100.0)).round(0).to_i
|
93
|
-
else
|
94
|
-
[0, amount - coupon.amount_off].max
|
95
|
-
end
|
45
|
+
def <=>(other)
|
46
|
+
(name || '') <=> (other.try(:name) || '')
|
96
47
|
end
|
97
48
|
|
98
49
|
end
|