effective_orders 4.5.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +1004 -0
- data/app/assets/images/effective_orders/stripe.png +0 -0
- data/app/assets/javascripts/effective_orders.js +6 -0
- data/app/assets/javascripts/effective_orders/customers.js.coffee +32 -0
- data/app/assets/javascripts/effective_orders/providers/stripe.js.coffee +77 -0
- data/app/assets/javascripts/effective_orders/subscriptions.js.coffee +81 -0
- data/app/assets/stylesheets/effective_orders.scss +2 -0
- data/app/assets/stylesheets/effective_orders/_cart.scss +4 -0
- data/app/assets/stylesheets/effective_orders/_order.scss +58 -0
- data/app/controllers/admin/customers_controller.rb +24 -0
- data/app/controllers/admin/order_items_controller.rb +16 -0
- data/app/controllers/admin/orders_controller.rb +223 -0
- data/app/controllers/effective/carts_controller.rb +85 -0
- data/app/controllers/effective/concerns/purchase.rb +62 -0
- data/app/controllers/effective/customers_controller.rb +20 -0
- data/app/controllers/effective/orders_controller.rb +162 -0
- data/app/controllers/effective/providers/cheque.rb +22 -0
- 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 +60 -0
- data/app/controllers/effective/providers/paypal.rb +33 -0
- data/app/controllers/effective/providers/phone.rb +22 -0
- data/app/controllers/effective/providers/pretend.rb +26 -0
- data/app/controllers/effective/providers/refund.rb +33 -0
- data/app/controllers/effective/providers/stripe.rb +72 -0
- data/app/controllers/effective/subscripter_controller.rb +18 -0
- data/app/controllers/effective/webhooks_controller.rb +109 -0
- data/app/datatables/admin/effective_customers_datatable.rb +22 -0
- data/app/datatables/admin/effective_orders_datatable.rb +100 -0
- data/app/datatables/effective_orders_datatable.rb +79 -0
- data/app/helpers/effective_carts_helper.rb +113 -0
- data/app/helpers/effective_orders_helper.rb +143 -0
- data/app/helpers/effective_paypal_helper.rb +49 -0
- data/app/helpers/effective_stripe_helper.rb +85 -0
- data/app/helpers/effective_subscriptions_helper.rb +34 -0
- data/app/mailers/effective/orders_mailer.rb +196 -0
- data/app/models/concerns/acts_as_purchasable.rb +118 -0
- data/app/models/concerns/acts_as_subscribable.rb +90 -0
- data/app/models/concerns/acts_as_subscribable_buyer.rb +49 -0
- data/app/models/effective/access_denied.rb +17 -0
- data/app/models/effective/cart.rb +88 -0
- data/app/models/effective/cart_item.rb +40 -0
- data/app/models/effective/customer.rb +92 -0
- data/app/models/effective/order.rb +541 -0
- data/app/models/effective/order_item.rb +63 -0
- data/app/models/effective/product.rb +23 -0
- data/app/models/effective/sold_out_validator.rb +7 -0
- data/app/models/effective/subscripter.rb +185 -0
- data/app/models/effective/subscription.rb +95 -0
- data/app/models/effective/tax_rate_calculator.rb +48 -0
- data/app/views/admin/customers/_actions.html.haml +2 -0
- data/app/views/admin/customers/index.html.haml +6 -0
- data/app/views/admin/customers/show.html.haml +6 -0
- data/app/views/admin/order_items/index.html.haml +3 -0
- data/app/views/admin/orders/_datatable_actions.html.haml +18 -0
- data/app/views/admin/orders/_form.html.haml +35 -0
- data/app/views/admin/orders/_form_note_internal.html.haml +7 -0
- data/app/views/admin/orders/_order_actions.html.haml +9 -0
- data/app/views/admin/orders/_order_item_fields.html.haml +14 -0
- data/app/views/admin/orders/checkout.html.haml +3 -0
- data/app/views/admin/orders/edit.html.haml +6 -0
- data/app/views/admin/orders/index.html.haml +6 -0
- data/app/views/admin/orders/new.html.haml +4 -0
- data/app/views/admin/orders/show.html.haml +4 -0
- data/app/views/effective/carts/_cart.html.haml +28 -0
- data/app/views/effective/carts/_cart_actions.html.haml +3 -0
- data/app/views/effective/carts/show.html.haml +17 -0
- data/app/views/effective/customers/_customer.html.haml +72 -0
- data/app/views/effective/customers/_form.html.haml +21 -0
- data/app/views/effective/customers/edit.html.haml +4 -0
- data/app/views/effective/customers/update.js.erb +5 -0
- data/app/views/effective/orders/_checkout_actions.html.haml +3 -0
- data/app/views/effective/orders/_checkout_step1.html.haml +4 -0
- data/app/views/effective/orders/_checkout_step2.html.haml +37 -0
- data/app/views/effective/orders/_datatable_actions.html.haml +2 -0
- data/app/views/effective/orders/_fields.html.haml +31 -0
- data/app/views/effective/orders/_fields_note.html.haml +7 -0
- data/app/views/effective/orders/_fields_terms.html.haml +8 -0
- data/app/views/effective/orders/_order.html.haml +11 -0
- data/app/views/effective/orders/_order_actions.html.haml +18 -0
- data/app/views/effective/orders/_order_deferred.html.haml +9 -0
- data/app/views/effective/orders/_order_footer.html.haml +1 -0
- data/app/views/effective/orders/_order_header.html.haml +23 -0
- data/app/views/effective/orders/_order_items.html.haml +72 -0
- data/app/views/effective/orders/_order_notes.html.haml +17 -0
- data/app/views/effective/orders/_order_payment.html.haml +24 -0
- data/app/views/effective/orders/_order_shipping.html.haml +30 -0
- data/app/views/effective/orders/_orders_table.html.haml +23 -0
- data/app/views/effective/orders/cheque/_form.html.haml +4 -0
- data/app/views/effective/orders/declined.html.haml +12 -0
- data/app/views/effective/orders/deferred.html.haml +13 -0
- data/app/views/effective/orders/deferred/_form.html.haml +16 -0
- data/app/views/effective/orders/edit.html.haml +3 -0
- data/app/views/effective/orders/free/_form.html.haml +5 -0
- data/app/views/effective/orders/index.html.haml +3 -0
- data/app/views/effective/orders/mark_as_paid/_form.html.haml +23 -0
- data/app/views/effective/orders/moneris/_form.html.haml +47 -0
- data/app/views/effective/orders/new.html.haml +3 -0
- data/app/views/effective/orders/paypal/_form.html.haml +5 -0
- data/app/views/effective/orders/phone/_form.html.haml +4 -0
- data/app/views/effective/orders/pretend/_form.html.haml +8 -0
- data/app/views/effective/orders/purchased.html.haml +11 -0
- data/app/views/effective/orders/refund/_form.html.haml +5 -0
- data/app/views/effective/orders/show.html.haml +6 -0
- data/app/views/effective/orders/stripe/_element.html.haml +8 -0
- data/app/views/effective/orders/stripe/_form.html.haml +31 -0
- data/app/views/effective/orders_mailer/order_error.html.haml +11 -0
- data/app/views/effective/orders_mailer/order_receipt_to_admin.html.haml +2 -0
- data/app/views/effective/orders_mailer/order_receipt_to_buyer.html.haml +2 -0
- data/app/views/effective/orders_mailer/payment_request_to_buyer.html.haml +13 -0
- data/app/views/effective/orders_mailer/pending_order_invoice_to_buyer.html.haml +13 -0
- data/app/views/effective/orders_mailer/refund_notification_to_admin.html.haml +15 -0
- data/app/views/effective/orders_mailer/subscription_canceled.html.haml +9 -0
- data/app/views/effective/orders_mailer/subscription_created.html.haml +13 -0
- data/app/views/effective/orders_mailer/subscription_event_to_admin.html.haml +13 -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_trialing.html.haml +7 -0
- data/app/views/effective/orders_mailer/subscription_updated.html.haml +13 -0
- data/app/views/effective/subscripter/_form.html.haml +60 -0
- data/app/views/effective/subscripter/_plan.html.haml +23 -0
- data/app/views/layouts/effective_orders_mailer_layout.html.haml +25 -0
- data/config/effective_orders.rb +279 -0
- data/config/routes.rb +70 -0
- data/db/migrate/01_create_effective_orders.rb.erb +137 -0
- data/lib/effective_orders.rb +243 -0
- data/lib/effective_orders/engine.rb +60 -0
- data/lib/effective_orders/version.rb +3 -0
- data/lib/generators/effective_orders/install_generator.rb +63 -0
- data/lib/generators/templates/effective_orders_mailer_preview.rb +120 -0
- data/lib/tasks/effective_orders_tasks.rake +69 -0
- metadata +276 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
module ActsAsPurchasable
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
module Base
|
|
5
|
+
def acts_as_purchasable(*options)
|
|
6
|
+
@acts_as_purchasable = options || []
|
|
7
|
+
|
|
8
|
+
# if table_exists?
|
|
9
|
+
# instance = new()
|
|
10
|
+
# raise 'must respond_to price' unless instance.respond_to?(:price)
|
|
11
|
+
# raise 'must respond_to purchased_order_id' unless instance.respond_to?(:purchased_order_id)
|
|
12
|
+
|
|
13
|
+
# if defined?(EffectiveQbSync)
|
|
14
|
+
# raise 'must respond to qb_item_name' unless instance.respond_to?(:qb_item_name)
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
|
|
18
|
+
include ::ActsAsPurchasable
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
included do
|
|
23
|
+
belongs_to :purchased_order, class_name: 'Effective::Order', optional: true # Set when purchased
|
|
24
|
+
|
|
25
|
+
has_many :cart_items, as: :purchasable, dependent: :delete_all, class_name: 'Effective::CartItem'
|
|
26
|
+
|
|
27
|
+
has_many :order_items, as: :purchasable, class_name: 'Effective::OrderItem'
|
|
28
|
+
has_many :orders, -> { order(:id) }, through: :order_items, class_name: 'Effective::Order'
|
|
29
|
+
|
|
30
|
+
has_many :purchased_orders, -> { where(state: EffectiveOrders::PURCHASED).order(:purchased_at) },
|
|
31
|
+
through: :order_items, class_name: 'Effective::Order', source: :order
|
|
32
|
+
|
|
33
|
+
# Database max integer value is 2147483647. So let's round that down and use a max/min of $20 million (2000000000)
|
|
34
|
+
validates :price, presence: true
|
|
35
|
+
validates :price, numericality: { less_than_or_equal_to: 2000000000, message: 'maximum price is $20,000,000' }
|
|
36
|
+
validates :price, numericality: { greater_than_or_equal_to: -2000000000, message: 'minimum price is -$20,000,000' }
|
|
37
|
+
|
|
38
|
+
validates :tax_exempt, inclusion: { in: [true, false] }
|
|
39
|
+
|
|
40
|
+
with_options(if: -> { quantity_enabled? }) do
|
|
41
|
+
validates :quantity_purchased, numericality: { allow_nil: true }
|
|
42
|
+
validates :quantity_max, numericality: { allow_nil: true }
|
|
43
|
+
validates_with Effective::SoldOutValidator, on: :create
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
scope :purchased, -> { where.not(purchased_order_id: nil) }
|
|
47
|
+
scope :not_purchased, -> { where(purchased_order_id: nil) }
|
|
48
|
+
|
|
49
|
+
# scope :purchased, -> { joins(order_items: :order).where(orders: {state: EffectiveOrders::PURCHASED}).distinct }
|
|
50
|
+
# scope :not_purchased, -> { where('id NOT IN (?)', purchased.pluck(:id).presence || [0]) }
|
|
51
|
+
scope :purchased_by, lambda { |user| joins(order_items: :order).where(orders: { user_id: user.try(:id), state: EffectiveOrders::PURCHASED }).distinct }
|
|
52
|
+
scope :not_purchased_by, lambda { |user| where('id NOT IN (?)', purchased_by(user).pluck(:id).presence || [0]) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
module ClassMethods
|
|
56
|
+
def before_purchase(&block)
|
|
57
|
+
send :define_method, :before_purchase do |order, order_item| self.instance_exec(order, order_item, &block) end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def after_purchase(&block)
|
|
61
|
+
send :define_method, :after_purchase do |order, order_item| self.instance_exec(order, order_item, &block) end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def after_decline(&block)
|
|
65
|
+
send :define_method, :after_decline do |order, order_item| self.instance_exec(order, order_item, &block) end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Regular instance methods
|
|
70
|
+
|
|
71
|
+
# If I have a column type of Integer, and I'm passed a non-Integer, convert it here
|
|
72
|
+
def price=(value)
|
|
73
|
+
if value.kind_of?(Integer)
|
|
74
|
+
super
|
|
75
|
+
elsif value.kind_of?(String) && !value.include?('.') # Looks like an integer
|
|
76
|
+
super
|
|
77
|
+
else
|
|
78
|
+
raise 'expected price to be an Integer representing the number of cents.'
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def purchasable_name
|
|
83
|
+
to_s
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def tax_exempt
|
|
87
|
+
self[:tax_exempt] || false
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def purchased?
|
|
91
|
+
purchased_order_id.present?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def purchased_at
|
|
95
|
+
purchased_order.try(:purchased_at)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def purchased_by?(user)
|
|
99
|
+
purchased_orders.any? { |order| order.user_id == user.id }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def purchased_download_url # Override me if this is a digital purchase.
|
|
103
|
+
false
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def quantity_enabled?
|
|
107
|
+
false
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def quantity_remaining
|
|
111
|
+
quantity_max - quantity_purchased if quantity_enabled?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def sold_out?
|
|
115
|
+
quantity_enabled? ? (quantity_remaining <= 0) : false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module ActsAsSubscribable
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
mattr_accessor :descendants
|
|
5
|
+
|
|
6
|
+
module Base
|
|
7
|
+
def acts_as_subscribable(*options)
|
|
8
|
+
@acts_as_subscribable = options || []
|
|
9
|
+
|
|
10
|
+
# if table_exists?
|
|
11
|
+
# instance = new()
|
|
12
|
+
# raise 'must respond to trialing_until' unless instance.respond_to?(:trialing_until) || !EffectiveOrders.trial?
|
|
13
|
+
# raise 'must respond to subscription_status' unless instance.respond_to?(:subscription_status)
|
|
14
|
+
# end
|
|
15
|
+
|
|
16
|
+
include ::ActsAsSubscribable
|
|
17
|
+
(ActsAsSubscribable.descendants ||= []) << self
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
included do
|
|
22
|
+
has_one :subscription, as: :subscribable, class_name: 'Effective::Subscription', inverse_of: :subscribable
|
|
23
|
+
has_one :customer, through: :subscription, class_name: 'Effective::Customer'
|
|
24
|
+
|
|
25
|
+
before_validation(if: -> { EffectiveOrders.trial? && trialing_until.blank? }) do
|
|
26
|
+
self.trialing_until = (Time.zone.now + EffectiveOrders.trial.fetch(:length)).beginning_of_day
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
before_destroy(if: -> { subscribed? }) do
|
|
30
|
+
raise :abort unless (subscripter.destroy! rescue false)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if EffectiveOrders.trial?
|
|
34
|
+
validates :trialing_until, presence: true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
validates :subscription_status, inclusion: { allow_nil: true, in: EffectiveOrders::STATUSES.keys }
|
|
38
|
+
|
|
39
|
+
scope :trialing, -> { where(subscription_status: nil).where('trialing_until > ?', Time.zone.now) }
|
|
40
|
+
scope :trial_past_due, -> { where(subscription_status: nil).where('trialing_until < ?', Time.zone.now) }
|
|
41
|
+
scope :not_trialing, -> { where.not(subscription_status: nil) }
|
|
42
|
+
|
|
43
|
+
scope :subscribed, -> { where(subscription_status: EffectiveOrders::ACTIVE) }
|
|
44
|
+
scope :subscription_past_due, -> { where(subscription_status: EffectiveOrders::PAST_DUE) }
|
|
45
|
+
scope :not_subscribed, -> { where(subscription_status: nil) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
module ClassMethods
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def subscripter
|
|
52
|
+
@_effective_subscripter ||= begin
|
|
53
|
+
Effective::Subscripter.new(subscribable: self, user: subscribable_buyer, quantity: subscription&.quantity, stripe_plan_id: subscription&.stripe_plan_id)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def subscribed?(stripe_plan_id = nil)
|
|
58
|
+
return false if subscription_status.blank?
|
|
59
|
+
stripe_plan_id ? (subscription&.stripe_plan_id == stripe_plan_id) : true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def subscription_active?
|
|
63
|
+
subscribed? && subscription_status == EffectiveOrders::ACTIVE
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def subscription_past_due?
|
|
67
|
+
subscribed? && subscription_status == EffectiveOrders::PAST_DUE
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def trialing?
|
|
71
|
+
subscription_status.blank?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def trial_active?
|
|
75
|
+
trialing? && trialing_until > Time.zone.now
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def trial_past_due?
|
|
79
|
+
trialing? && trialing_until < Time.zone.now
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def subscribable_buyer
|
|
83
|
+
raise 'acts_as_subscribable object requires the subscribable_buyer method be defined to return the User buying this item.'
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def subscribable_quantity_used
|
|
87
|
+
raise 'acts_as_subscribable object requires the subscribable_quantity_used method be defined to determine how many are in use.'
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module ActsAsSubscribableBuyer
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
module Base
|
|
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] update customer: #{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
|
+
module ClassMethods
|
|
22
|
+
def after_invoice_payment_succeeded(&block)
|
|
23
|
+
send :define_method, :after_invoice_payment_succeeded do |event| self.instance_exec(event, &block) end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def after_invoice_payment_failed(&block)
|
|
27
|
+
send :define_method, :after_invoice_payment_failed do |event| self.instance_exec(event, &block) end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def after_customer_subscription_created(&block)
|
|
31
|
+
send :define_method, :after_customer_subscription_created do |event| self.instance_exec(event, &block) end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def after_customer_subscription_updated(&block)
|
|
35
|
+
send :define_method, :after_customer_subscription_updated do |event| self.instance_exec(event, &block) end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def after_customer_subscription_deleted(&block)
|
|
39
|
+
send :define_method, :after_customer_subscription_deleted do |event| self.instance_exec(event, &block) end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def after_customer_updated(&block)
|
|
43
|
+
send :define_method, :after_customer_updated do |event| self.instance_exec(event, &block) end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
end
|
|
49
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
unless defined?(Effective::AccessDenied)
|
|
2
|
+
module Effective
|
|
3
|
+
class AccessDenied < StandardError
|
|
4
|
+
attr_reader :action, :subject
|
|
5
|
+
|
|
6
|
+
def initialize(message = nil, action = nil, subject = nil)
|
|
7
|
+
@message = message
|
|
8
|
+
@action = action
|
|
9
|
+
@subject = subject
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_s
|
|
13
|
+
@message || I18n.t(:'unauthorized.default', :default => 'Access Denied')
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
module Effective
|
|
2
|
+
class Cart < ActiveRecord::Base
|
|
3
|
+
self.table_name = EffectiveOrders.carts_table_name.to_s
|
|
4
|
+
|
|
5
|
+
belongs_to :user, optional: true # Optional. We want non-logged-in users to have carts too.
|
|
6
|
+
has_many :cart_items, -> { order(:id) }, dependent: :delete_all, class_name: 'Effective::CartItem'
|
|
7
|
+
|
|
8
|
+
accepts_nested_attributes_for :cart_items
|
|
9
|
+
|
|
10
|
+
# Attributes
|
|
11
|
+
# cart_items_count :integer
|
|
12
|
+
# timestamps
|
|
13
|
+
|
|
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)
|
|
20
|
+
raise 'expecting an acts_as_purchasable object' unless item.kind_of?(ActsAsPurchasable)
|
|
21
|
+
|
|
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
|
+
)
|
|
32
|
+
|
|
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
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if item.quantity_enabled? && (existing ? existing.quantity : quantity) > item.quantity_remaining
|
|
42
|
+
raise EffectiveOrders::SoldOutException, "#{item.purchasable_name} is sold out"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
existing ||= cart_items.build(purchasable: item, quantity: quantity, unique: (unique.to_s unless unique.kind_of?(Proc)))
|
|
46
|
+
save!
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def clear!
|
|
50
|
+
cart_items.each { |cart_item| cart_item.mark_for_destruction }
|
|
51
|
+
cart_items.present? ? save! : true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def remove(item)
|
|
55
|
+
find(item).try(:mark_for_destruction)
|
|
56
|
+
save!
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def includes?(item)
|
|
60
|
+
find(item).present?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def find(item)
|
|
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 }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def size
|
|
72
|
+
cart_items_count || cart_items.length
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def present?
|
|
76
|
+
size > 0
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def blank?
|
|
80
|
+
size <= 0
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def subtotal
|
|
84
|
+
cart_items.map { |ci| ci.subtotal }.sum
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Effective
|
|
2
|
+
class CartItem < ActiveRecord::Base
|
|
3
|
+
self.table_name = EffectiveOrders.cart_items_table_name.to_s
|
|
4
|
+
|
|
5
|
+
belongs_to :cart, counter_cache: true, class_name: 'Effective::Cart'
|
|
6
|
+
belongs_to :purchasable, polymorphic: true
|
|
7
|
+
|
|
8
|
+
# Attributes
|
|
9
|
+
# quantity :integer
|
|
10
|
+
# timestamps
|
|
11
|
+
|
|
12
|
+
validates :purchasable, presence: true
|
|
13
|
+
validates :quantity, presence: true
|
|
14
|
+
|
|
15
|
+
def to_s
|
|
16
|
+
name || 'New Cart Item'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def name
|
|
20
|
+
purchasable&.purchasable_name
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def price
|
|
24
|
+
if (purchasable.price || 0).kind_of?(Integer)
|
|
25
|
+
purchasable.price || 0
|
|
26
|
+
else
|
|
27
|
+
raise 'expected price to be an Integer representing the number of cents.'
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def tax_exempt
|
|
32
|
+
purchasable&.tax_exempt || false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def subtotal
|
|
36
|
+
price * quantity
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
module Effective
|
|
2
|
+
class Customer < ActiveRecord::Base
|
|
3
|
+
self.table_name = EffectiveOrders.customers_table_name.to_s
|
|
4
|
+
|
|
5
|
+
attr_accessor :stripe_customer
|
|
6
|
+
|
|
7
|
+
belongs_to :user
|
|
8
|
+
has_many :subscriptions, -> { includes(:subscribable) }, class_name: 'Effective::Subscription', foreign_key: 'customer_id'
|
|
9
|
+
accepts_nested_attributes_for :subscriptions
|
|
10
|
+
|
|
11
|
+
# Attributes
|
|
12
|
+
# stripe_customer_id :string # cus_xja7acoa03
|
|
13
|
+
# payment_method_id :string # Last payment method used
|
|
14
|
+
# active_card :string # **** **** **** 4242 Visa 05/12
|
|
15
|
+
|
|
16
|
+
# timestamps
|
|
17
|
+
|
|
18
|
+
scope :deep, -> { includes(subscriptions: :subscribable) }
|
|
19
|
+
|
|
20
|
+
validates :user, presence: true
|
|
21
|
+
validates :stripe_customer_id, presence: true
|
|
22
|
+
|
|
23
|
+
def self.for_user(user)
|
|
24
|
+
Effective::Customer.where(user: user).first_or_initialize
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_s
|
|
28
|
+
user.to_s.presence || 'New Customer'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def email
|
|
32
|
+
user.email if user
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def create_stripe_customer!
|
|
36
|
+
return if stripe_customer.present?
|
|
37
|
+
raise('expected a user') unless user.present?
|
|
38
|
+
|
|
39
|
+
Rails.logger.info "[STRIPE] create customer: #{user.email}"
|
|
40
|
+
|
|
41
|
+
self.stripe_customer = Stripe::Customer.create(email: user.email, description: user.to_s, metadata: { user_id: user.id })
|
|
42
|
+
self.stripe_customer_id = stripe_customer.id
|
|
43
|
+
|
|
44
|
+
save!
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def stripe_customer
|
|
48
|
+
@stripe_customer ||= if stripe_customer_id.present?
|
|
49
|
+
Rails.logger.info "[STRIPE] get customer: #{stripe_customer_id}"
|
|
50
|
+
::Stripe::Customer.retrieve(stripe_customer_id)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def invoices
|
|
55
|
+
@invoices ||= if stripe_customer_id.present?
|
|
56
|
+
Rails.logger.info "[STRIPE] list invoices: #{stripe_customer_id}"
|
|
57
|
+
::Stripe::Invoice.list(customer: stripe_customer_id) rescue nil
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def upcoming_invoice
|
|
62
|
+
@upcoming_invoice ||= if stripe_customer_id.present?
|
|
63
|
+
Rails.logger.info "[STRIPE] get upcoming invoice: #{stripe_customer_id}"
|
|
64
|
+
::Stripe::Invoice.upcoming(customer: stripe_customer_id) rescue nil
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def token_required?
|
|
69
|
+
active_card.blank? || past_due?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def past_due?
|
|
73
|
+
subscriptions.any? { |subscription| subscription.past_due? }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def active?
|
|
77
|
+
subscriptions.present? && subscriptions.all? { |subscription| subscription.active? }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def payment_status
|
|
81
|
+
if past_due?
|
|
82
|
+
'We ran into an error processing your last payment. Please update or confirm your card details to continue.'
|
|
83
|
+
elsif active?
|
|
84
|
+
"Your payment is in good standing. Thanks so much for your support!"
|
|
85
|
+
elsif active_card.blank?
|
|
86
|
+
'No credit card on file. Please add a card.'
|
|
87
|
+
else
|
|
88
|
+
'Please update or confirm your card details to continue.'
|
|
89
|
+
end.html_safe
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|