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