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
@@ -12,10 +12,14 @@ module ActsAsPurchasable
|
|
12
12
|
end
|
13
13
|
|
14
14
|
included do
|
15
|
-
has_many :orders, through: :order_items, class_name: 'Effective::Order'
|
16
|
-
has_many :order_items, as: :purchasable, class_name: 'Effective::OrderItem'
|
17
15
|
has_many :cart_items, as: :purchasable, dependent: :delete_all, class_name: 'Effective::CartItem'
|
18
16
|
|
17
|
+
has_many :order_items, as: :purchasable, class_name: 'Effective::OrderItem'
|
18
|
+
has_many :orders, -> { order(:id) }, through: :order_items, class_name: 'Effective::Order'
|
19
|
+
|
20
|
+
has_many :purchased_orders, -> { where(purchase_state: EffectiveOrders::PURCHASED).order(:purchased_at) },
|
21
|
+
through: :order_items, class_name: 'Effective::Order', source: :order
|
22
|
+
|
19
23
|
validates_with Effective::SoldOutValidator, on: :create
|
20
24
|
|
21
25
|
# Database max integer value is 2147483647. So let's round that down and use a max/min of $20 million (2000000000)
|
@@ -24,8 +28,8 @@ module ActsAsPurchasable
|
|
24
28
|
validates :tax_exempt, inclusion: { in: [true, false] }
|
25
29
|
|
26
30
|
# These are breaking on the check for quanitty_enabled?. More research is due
|
27
|
-
validates :quantity_purchased, numericality: {allow_nil: true}, if: proc { |purchasable| (purchasable.quantity_enabled? rescue false) }
|
28
|
-
validates :quantity_max, numericality: {allow_nil: true}, if: proc { |purchasable| (purchasable.quantity_enabled? rescue false) }
|
31
|
+
validates :quantity_purchased, numericality: { allow_nil: true }, if: proc { |purchasable| (purchasable.quantity_enabled? rescue false) }
|
32
|
+
validates :quantity_max, numericality: { allow_nil: true }, if: proc { |purchasable| (purchasable.quantity_enabled? rescue false) }
|
29
33
|
|
30
34
|
scope :purchased, -> { joins(order_items: :order).where(orders: {purchase_state: EffectiveOrders::PURCHASED}).distinct }
|
31
35
|
scope :purchased_by, lambda { |user| joins(order_items: :order).where(orders: {user_id: user.try(:id), purchase_state: EffectiveOrders::PURCHASED}).distinct }
|
@@ -37,6 +41,10 @@ module ActsAsPurchasable
|
|
37
41
|
end
|
38
42
|
|
39
43
|
module ClassMethods
|
44
|
+
def before_purchase(&block)
|
45
|
+
send :define_method, :before_purchase do |order, order_item| self.instance_exec(order, order_item, &block) end
|
46
|
+
end
|
47
|
+
|
40
48
|
def after_purchase(&block)
|
41
49
|
send :define_method, :after_purchase do |order, order_item| self.instance_exec(order, order_item, &block) end
|
42
50
|
end
|
@@ -54,16 +62,12 @@ module ActsAsPurchasable
|
|
54
62
|
|
55
63
|
# If I have a column type of Integer, and I'm passed a non-Integer, convert it here
|
56
64
|
def price=(value)
|
57
|
-
|
58
|
-
|
59
|
-
if integer_column == false
|
60
|
-
super
|
61
|
-
elsif value.kind_of?(Integer)
|
65
|
+
if value.kind_of?(Integer)
|
62
66
|
super
|
63
67
|
elsif value.kind_of?(String) && !value.include?('.') # Looks like an integer
|
64
68
|
super
|
65
|
-
else
|
66
|
-
|
69
|
+
else
|
70
|
+
raise 'expected price to be an Integer representing the number of cents.'
|
67
71
|
end
|
68
72
|
end
|
69
73
|
|
@@ -77,28 +81,24 @@ module ActsAsPurchasable
|
|
77
81
|
|
78
82
|
def seller
|
79
83
|
if EffectiveOrders.stripe_connect_enabled
|
80
|
-
raise 'acts_as_purchasable object requires the seller be defined to return the User selling this item.
|
84
|
+
raise 'acts_as_purchasable object requires the seller be defined to return the User selling this item. This is only a requirement when using StripeConnect.'
|
81
85
|
end
|
82
86
|
end
|
83
87
|
|
84
|
-
def
|
85
|
-
@
|
88
|
+
def purchased_order
|
89
|
+
@purchased_order ||= purchased_orders.first
|
86
90
|
end
|
87
91
|
|
88
|
-
def
|
89
|
-
@
|
92
|
+
def purchased?
|
93
|
+
@is_purchased ||= purchased_order.present?
|
90
94
|
end
|
91
95
|
|
92
96
|
def purchased_by?(user)
|
93
|
-
|
97
|
+
purchased_orders.any? { |order| order.user_id == user.id }
|
94
98
|
end
|
95
99
|
|
96
|
-
def
|
97
|
-
|
98
|
-
end
|
99
|
-
|
100
|
-
def purchased_order
|
101
|
-
purchased_orders.first
|
100
|
+
def purchased_at
|
101
|
+
purchased_order.try(:purchased_at)
|
102
102
|
end
|
103
103
|
|
104
104
|
def quantity_enabled?
|
@@ -113,16 +113,6 @@ module ActsAsPurchasable
|
|
113
113
|
quantity_enabled? ? (quantity_remaining == 0) : false
|
114
114
|
end
|
115
115
|
|
116
|
-
def purchased!(order = nil, order_item = nil)
|
117
|
-
after_purchase(order, order_item) if self.respond_to?(:after_purchase)
|
118
|
-
save!
|
119
|
-
end
|
120
|
-
|
121
|
-
def declined!(order = nil, order_item = nil)
|
122
|
-
after_decline(order, order_item) if self.respond_to?(:after_decline)
|
123
|
-
save!
|
124
|
-
end
|
125
|
-
|
126
116
|
# Override me if this is a digital purchase.
|
127
117
|
def purchased_download_url
|
128
118
|
false
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module ActsAsSubscribable
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
mattr_accessor :descendants
|
5
|
+
|
6
|
+
module ActiveRecord
|
7
|
+
def acts_as_subscribable(*options)
|
8
|
+
@acts_as_subscribable = options || []
|
9
|
+
include ::ActsAsSubscribable
|
10
|
+
(ActsAsSubscribable.descendants ||= []) << self
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
included do
|
15
|
+
has_one :subscription, as: :subscribable, class_name: 'Effective::Subscription'
|
16
|
+
has_one :customer, through: :subscription, class_name: 'Effective::Customer'
|
17
|
+
|
18
|
+
validates :subscripter, associated: true
|
19
|
+
|
20
|
+
scope :subscribed, -> { where(id: joins(:subscription)) } # All resources with a subscription
|
21
|
+
scope :trialing, -> { where.not(id: joins(:subscription)) } # All resources without a subscription
|
22
|
+
end
|
23
|
+
|
24
|
+
module ClassMethods
|
25
|
+
end
|
26
|
+
|
27
|
+
def subscripter
|
28
|
+
@_effective_subscripter ||= Effective::Subscripter.new(subscribable: self, user: buyer)
|
29
|
+
end
|
30
|
+
|
31
|
+
def subscripter=(atts)
|
32
|
+
subscripter.assign_attributes(atts)
|
33
|
+
end
|
34
|
+
|
35
|
+
def subscribed?(stripe_plan_id = nil)
|
36
|
+
case stripe_plan_id
|
37
|
+
when nil
|
38
|
+
subscription.present? # Subscribed to any subscription?
|
39
|
+
when (EffectiveOrders.stripe_plans['trial'] || {})[:id]
|
40
|
+
subscription.blank? || subscription.new_record? || subscription.stripe_plan_id == stripe_plan_id
|
41
|
+
else
|
42
|
+
subscription && subscription.persisted? && subscription.errors.blank? && subscription.stripe_plan_id == stripe_plan_id
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def subscription_active?
|
47
|
+
(trialing? && !trial_expired?) || (subscribed? && subscription.active?)
|
48
|
+
end
|
49
|
+
|
50
|
+
def trialing?
|
51
|
+
!subscribed?
|
52
|
+
end
|
53
|
+
|
54
|
+
def trial_expired?
|
55
|
+
trialing? && Time.zone.now > trial_expires_at
|
56
|
+
end
|
57
|
+
|
58
|
+
def trial_expires_at
|
59
|
+
# The rake task send_trial_expiring_emails depends on this beginning_of_day
|
60
|
+
((created_at || Time.zone.now) + EffectiveOrders.subscription[:trial_period]).beginning_of_day
|
61
|
+
end
|
62
|
+
|
63
|
+
def buyer
|
64
|
+
raise 'acts_as_subscribable object requires the buyer be defined to return the User buying this item.'
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ActsAsSubscribableBuyer
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
module ActiveRecord
|
5
|
+
def acts_as_subscribable_buyer(*options)
|
6
|
+
include ::ActsAsSubscribableBuyer
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
included do
|
11
|
+
has_one :customer, class_name: 'Effective::Customer'
|
12
|
+
|
13
|
+
before_save(if: -> { persisted? && email_changed? && customer.present? }) do
|
14
|
+
Rails.logger.info "STRIPE CUSTOMER EMAIL UPDATE: #{customer.stripe_customer_id}"
|
15
|
+
customer.stripe_customer.email = email
|
16
|
+
customer.stripe_customer.description = to_s
|
17
|
+
throw :abort unless (customer.stripe_customer.save rescue false)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
@@ -2,58 +2,87 @@ module Effective
|
|
2
2
|
class Cart < ActiveRecord::Base
|
3
3
|
self.table_name = EffectiveOrders.carts_table_name.to_s
|
4
4
|
|
5
|
-
belongs_to :user #
|
6
|
-
has_many :cart_items,
|
5
|
+
belongs_to :user # Optional. We want non-logged-in users to have carts too.
|
6
|
+
has_many :cart_items, -> { includes(:purchasable).order(:updated_at) }, dependent: :delete_all, class_name: 'Effective::CartItem'
|
7
7
|
|
8
|
-
|
9
|
-
# timestamps
|
10
|
-
# end
|
8
|
+
accepts_nested_attributes_for :cart_items
|
11
9
|
|
12
|
-
|
10
|
+
# Attributes
|
11
|
+
# cart_items_count :integer
|
12
|
+
# timestamps
|
13
13
|
|
14
|
-
|
14
|
+
scope :deep, -> { includes(cart_items: :purchasable) }
|
15
|
+
|
16
|
+
# cart.add(@product, unique: -> (a, b) { a.kind_of?(Product) && b.kind_of?(Product) && a.category == b.category })
|
17
|
+
# cart.add(@product, unique: :category)
|
18
|
+
# cart.add(@product, unique: false) # Add as many as you want
|
19
|
+
def add(item, quantity: 1, unique: true)
|
15
20
|
raise 'expecting an acts_as_purchasable object' unless item.kind_of?(ActsAsPurchasable)
|
16
21
|
|
17
|
-
|
22
|
+
existing = (
|
23
|
+
if unique.kind_of?(Proc)
|
24
|
+
cart_items.find { |cart_item| instance_exec(item, cart_item.purchasable, &unique) }
|
25
|
+
elsif unique.kind_of?(Symbol) || (unique.kind_of?(String) && unique != 'true')
|
26
|
+
raise "expected item to respond to unique #{unique}" unless item.respond_to?(unique)
|
27
|
+
cart_items.find { |cart_item| cart_item.purchasable.respond_to?(unique) && item.send(unique) == cart_item.purchasable.send(unique) }
|
28
|
+
elsif unique.present?
|
29
|
+
find(item)
|
30
|
+
end
|
31
|
+
)
|
18
32
|
|
19
|
-
if
|
20
|
-
|
21
|
-
|
33
|
+
if existing
|
34
|
+
if unique || (existing.unique.present?)
|
35
|
+
existing.assign_attributes(purchasable: item, quantity: quantity, unique: existing.unique)
|
36
|
+
else
|
37
|
+
existing.quantity = existing.quantity + quantity
|
38
|
+
end
|
22
39
|
end
|
23
40
|
|
24
|
-
if
|
25
|
-
|
26
|
-
else
|
27
|
-
cart_items.create(cart: self, purchasable_id: item.id, purchasable_type: item.class.name, quantity: quantity)
|
41
|
+
if item.quantity_enabled? && (existing ? existing.quantity : quantity) > item.quantity_remaining
|
42
|
+
raise EffectiveOrders::SoldOutException, "#{item.title} is sold out"
|
28
43
|
end
|
44
|
+
|
45
|
+
existing ||= cart_items.build(purchasable: item, quantity: quantity, unique: (unique.to_s if unique.kind_of?(Symbol) || unique.kind_of?(String) || unique == true))
|
46
|
+
save!
|
47
|
+
end
|
48
|
+
|
49
|
+
def clear!
|
50
|
+
cart_items.each { |cart_item| cart_item.mark_for_destruction }
|
51
|
+
save!
|
29
52
|
end
|
30
|
-
alias_method :add_to_cart, :add
|
31
53
|
|
32
|
-
def remove(
|
33
|
-
|
54
|
+
def remove(item)
|
55
|
+
find(item).try(:mark_for_destruction)
|
56
|
+
save!
|
34
57
|
end
|
35
|
-
alias_method :remove_from_cart, :remove
|
36
58
|
|
37
59
|
def includes?(item)
|
38
60
|
find(item).present?
|
39
61
|
end
|
40
62
|
|
41
63
|
def find(item)
|
42
|
-
cart_items.
|
64
|
+
cart_items.find { |cart_item| cart_item == item || cart_item.purchasable == item }
|
65
|
+
end
|
66
|
+
|
67
|
+
def purchasables
|
68
|
+
cart_items.map { |cart_item| cart_item.purchasable }
|
43
69
|
end
|
44
70
|
|
45
71
|
def size
|
46
|
-
cart_items.
|
72
|
+
cart_items_count || cart_items.length
|
73
|
+
end
|
74
|
+
|
75
|
+
def present?
|
76
|
+
size > 0
|
47
77
|
end
|
48
78
|
|
49
|
-
def
|
50
|
-
size
|
79
|
+
def blank?
|
80
|
+
size <= 0
|
51
81
|
end
|
52
82
|
|
53
83
|
def subtotal
|
54
84
|
cart_items.map { |ci| ci.subtotal }.sum
|
55
85
|
end
|
56
|
-
alias_method :total, :subtotal
|
57
86
|
|
58
87
|
end
|
59
88
|
end
|
@@ -2,26 +2,21 @@ module Effective
|
|
2
2
|
class CartItem < ActiveRecord::Base
|
3
3
|
self.table_name = EffectiveOrders.cart_items_table_name.to_s
|
4
4
|
|
5
|
-
belongs_to :cart
|
6
|
-
belongs_to :purchasable, :
|
5
|
+
belongs_to :cart, counter_cache: true, class_name: 'Effective::Cart'
|
6
|
+
belongs_to :purchasable, polymorphic: true
|
7
7
|
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
# timestamps
|
12
|
-
# end
|
8
|
+
# Attributes
|
9
|
+
# quantity :integer
|
10
|
+
# timestamps
|
13
11
|
|
14
12
|
validates :purchasable, presence: true
|
15
13
|
validates :quantity, presence: true
|
16
14
|
|
17
|
-
default_scope -> { order(:updated_at) }
|
18
|
-
|
19
15
|
def price
|
20
16
|
if (purchasable.price || 0).kind_of?(Integer)
|
21
17
|
purchasable.price || 0
|
22
18
|
else
|
23
|
-
|
24
|
-
(purchasable.price * 100.0).round(0).to_i rescue 0
|
19
|
+
raise 'expected price to be an Integer representing the number of cents.'
|
25
20
|
end
|
26
21
|
end
|
27
22
|
|
@@ -36,7 +31,6 @@ module Effective
|
|
36
31
|
def subtotal
|
37
32
|
price * quantity
|
38
33
|
end
|
39
|
-
alias_method :total, :subtotal
|
40
34
|
|
41
35
|
end
|
42
36
|
end
|
@@ -2,83 +2,80 @@ module Effective
|
|
2
2
|
class Customer < ActiveRecord::Base
|
3
3
|
self.table_name = EffectiveOrders.customers_table_name.to_s
|
4
4
|
|
5
|
-
attr_accessor :
|
5
|
+
attr_accessor :stripe_token # This is a convenience method so we have a place to store StripeConnect temporary access tokens
|
6
|
+
attr_accessor :stripe_customer, :stripe_subscription
|
6
7
|
|
7
8
|
belongs_to :user
|
8
|
-
has_many :subscriptions, :
|
9
|
+
has_many :subscriptions, class_name: 'Effective::Subscription', foreign_key: 'customer_id'
|
10
|
+
has_many :subscribables, through: :subscriptions, source: :subscribable
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
#
|
13
|
-
#
|
12
|
+
accepts_nested_attributes_for :subscriptions
|
13
|
+
|
14
|
+
# Attributes
|
15
|
+
# stripe_customer_id :string # cus_xja7acoa03
|
16
|
+
# active_card :string # **** **** **** 4242 Visa 05/12
|
17
|
+
|
18
|
+
# stripe_subscription_id :string # Each user gets one stripe subscription object, which can contain many items
|
19
|
+
# status :string
|
20
|
+
|
21
|
+
# stripe_connect_access_token :string # If using StripeConnect and this user is a connected Seller
|
14
22
|
#
|
15
|
-
#
|
16
|
-
|
23
|
+
# timestamps
|
24
|
+
|
25
|
+
scope :deep, -> { includes(subscriptions: :subscribable) }
|
26
|
+
|
27
|
+
before_validation do
|
28
|
+
subscriptions.each { |subscription| subscription.status = status }
|
29
|
+
end
|
17
30
|
|
18
31
|
validates :user, presence: true
|
19
|
-
validates :
|
32
|
+
validates :stripe_customer_id, presence: true
|
33
|
+
validates :status, if: -> { stripe_subscription_id.present? }, inclusion: { in: %w(active past_due) }
|
20
34
|
|
21
|
-
|
35
|
+
def self.for_user(user)
|
36
|
+
Effective::Customer.where(user: user).first_or_initialize
|
37
|
+
end
|
22
38
|
|
23
|
-
|
24
|
-
|
25
|
-
if user.present?
|
26
|
-
Effective::Customer.where(:user_id => (user.try(:id) rescue user.to_i)).first_or_create
|
27
|
-
end
|
28
|
-
end
|
39
|
+
def to_s
|
40
|
+
user.to_s.presence || 'New Customer'
|
29
41
|
end
|
30
42
|
|
31
43
|
def stripe_customer
|
32
44
|
@stripe_customer ||= if stripe_customer_id.present?
|
45
|
+
Rails.logger.info "STRIPE CUSTOMER RETRIEVE: #{stripe_customer_id}"
|
33
46
|
::Stripe::Customer.retrieve(stripe_customer_id)
|
34
|
-
else
|
35
|
-
::Stripe::Customer.create(:email => user.email, :description => user.id.to_s).tap do |stripe_customer|
|
36
|
-
self.update_attributes(:stripe_customer_id => stripe_customer.id)
|
37
|
-
end
|
38
47
|
end
|
39
48
|
end
|
40
49
|
|
41
|
-
def
|
42
|
-
if
|
43
|
-
|
44
|
-
|
45
|
-
elsif stripe_customer.respond_to?(:sources)
|
46
|
-
stripe_customer.source = token
|
47
|
-
end
|
48
|
-
|
49
|
-
if stripe_customer.save && default_card.present?
|
50
|
-
card = cards.retrieve(default_card)
|
51
|
-
|
52
|
-
self.stripe_active_card = "**** **** **** #{card.last4} #{card.brand} #{card.exp_month}/#{card.exp_year}"
|
53
|
-
self.save!
|
54
|
-
else
|
55
|
-
raise Exception.new('unable to update stripe customer with new card')
|
56
|
-
end
|
50
|
+
def stripe_subscription
|
51
|
+
@stripe_subscription ||= if stripe_subscription_id.present?
|
52
|
+
Rails.logger.info "STRIPE SUBSCRIPTION RETRIEVE: #{stripe_subscription_id}"
|
53
|
+
::Stripe::Subscription.retrieve(stripe_subscription_id)
|
57
54
|
end
|
58
55
|
end
|
59
56
|
|
60
|
-
def
|
61
|
-
|
57
|
+
def upcoming_invoice
|
58
|
+
@upcoming_invoice ||= if stripe_customer_id.present? && stripe_subscription_id.present?
|
59
|
+
Rails.logger.info "STRIPE UPCOMING INVOICE RETRIEVE: #{stripe_customer_id}"
|
60
|
+
::Stripe::Invoice.upcoming(customer: stripe_customer_id) rescue nil
|
61
|
+
end
|
62
62
|
end
|
63
63
|
|
64
|
-
|
65
|
-
|
66
|
-
def default_card
|
67
|
-
case
|
68
|
-
when stripe_customer.respond_to?(:default_card)
|
69
|
-
stripe_customer.default_card
|
70
|
-
when stripe_customer.respond_to?(:default_source)
|
71
|
-
stripe_customer.default_source
|
72
|
-
end
|
64
|
+
def token_required?
|
65
|
+
active_card.blank? || (active_card.present? && stripe_subscription_id.present? && status != 'active')
|
73
66
|
end
|
74
67
|
|
75
|
-
def
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
68
|
+
def payment_status
|
69
|
+
if status == 'past_due'
|
70
|
+
'We ran into an error processing your last payment. Please update or confirm your card details to continue.'
|
71
|
+
elsif active_card.present? && token_required?
|
72
|
+
'Please update or confirm your card details to continue.'
|
73
|
+
elsif active_card.present?
|
74
|
+
'Thanks for your support! The card we have on file is'
|
75
|
+
else
|
76
|
+
'No credit card on file. Please add a card.'
|
77
|
+
end.html_safe
|
82
78
|
end
|
79
|
+
|
83
80
|
end
|
84
81
|
end
|