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,541 @@
|
|
|
1
|
+
# When an Order is first initialized it is done in the pending state
|
|
2
|
+
# - when it's in the pending state, none of the buyer entered information is required
|
|
3
|
+
# - when a pending order is rendered:
|
|
4
|
+
# - if the user has a billing address, go to step 2
|
|
5
|
+
# - if the user has no billing address, go to step 1
|
|
6
|
+
#
|
|
7
|
+
# After Step1, we go to the confirmed state
|
|
8
|
+
# After Step2, we are in the purchased or declined state
|
|
9
|
+
|
|
10
|
+
module Effective
|
|
11
|
+
class Order < ActiveRecord::Base
|
|
12
|
+
self.table_name = EffectiveOrders.orders_table_name.to_s
|
|
13
|
+
|
|
14
|
+
if EffectiveOrders.obfuscate_order_ids
|
|
15
|
+
acts_as_obfuscated format: '###-####-###'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
acts_as_addressable(
|
|
19
|
+
billing: { singular: true, use_full_name: EffectiveOrders.use_address_full_name },
|
|
20
|
+
shipping: { singular: true, use_full_name: EffectiveOrders.use_address_full_name }
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
attr_accessor :terms_and_conditions # Yes, I agree to the terms and conditions
|
|
24
|
+
attr_accessor :confirmed_checkout # Set on the Checkout Step 1
|
|
25
|
+
|
|
26
|
+
# Settings in the /admin action forms
|
|
27
|
+
attr_accessor :send_payment_request_to_buyer # Set by Admin::Orders#new. Should the payment request email be sent after creating an order?
|
|
28
|
+
attr_accessor :send_mark_as_paid_email_to_buyer # Set by Admin::Orders#mark_as_paid
|
|
29
|
+
attr_accessor :skip_buyer_validations # Set by Admin::Orders#create
|
|
30
|
+
|
|
31
|
+
# If we want to use orders in a has_many way
|
|
32
|
+
belongs_to :parent, polymorphic: true, optional: true
|
|
33
|
+
|
|
34
|
+
belongs_to :user, validate: false # This is the buyer/user of the order. We validate it below.
|
|
35
|
+
has_many :order_items, -> { order(:id) }, inverse_of: :order, class_name: 'Effective::OrderItem', dependent: :delete_all
|
|
36
|
+
|
|
37
|
+
accepts_nested_attributes_for :order_items, allow_destroy: false, reject_if: :all_blank
|
|
38
|
+
accepts_nested_attributes_for :user, allow_destroy: false, update_only: true
|
|
39
|
+
|
|
40
|
+
# Attributes
|
|
41
|
+
# state :string
|
|
42
|
+
# purchased_at :datetime
|
|
43
|
+
#
|
|
44
|
+
# note :text # From buyer to admin
|
|
45
|
+
# note_to_buyer :text # From admin to buyer
|
|
46
|
+
# note_internal :text # Internal admin only
|
|
47
|
+
#
|
|
48
|
+
# billing_name :string # name of buyer
|
|
49
|
+
# email :string # same as user.email
|
|
50
|
+
# cc :string # can be set by admin
|
|
51
|
+
#
|
|
52
|
+
# payment :text # serialized hash containing all the payment details.
|
|
53
|
+
# payment_provider :string
|
|
54
|
+
# payment_card :string
|
|
55
|
+
#
|
|
56
|
+
# tax_rate :decimal, precision: 6, scale: 3
|
|
57
|
+
#
|
|
58
|
+
# subtotal :integer
|
|
59
|
+
# tax :integer
|
|
60
|
+
# total :integer
|
|
61
|
+
#
|
|
62
|
+
# timestamps
|
|
63
|
+
|
|
64
|
+
serialize :payment, Hash
|
|
65
|
+
|
|
66
|
+
before_validation { assign_order_totals }
|
|
67
|
+
before_validation { assign_billing_name }
|
|
68
|
+
before_validation { assign_email }
|
|
69
|
+
before_validation { assign_last_address }
|
|
70
|
+
|
|
71
|
+
before_validation(if: -> { confirmed_checkout }) do
|
|
72
|
+
self.state = EffectiveOrders::CONFIRMED if pending?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Order validations
|
|
76
|
+
validates :user_id, presence: true
|
|
77
|
+
validates :email, presence: true, email: true # email and cc validators are from effective_resources
|
|
78
|
+
validates :cc, email_cc: true
|
|
79
|
+
|
|
80
|
+
validates :order_items, presence: { message: 'No items are present. Please add additional items.' }
|
|
81
|
+
validates :state, inclusion: { in: EffectiveOrders::STATES.keys }
|
|
82
|
+
validates :subtotal, presence: true
|
|
83
|
+
|
|
84
|
+
if EffectiveOrders.minimum_charge.to_i > 0
|
|
85
|
+
validates :total, presence: true, numericality: {
|
|
86
|
+
greater_than_or_equal_to: EffectiveOrders.minimum_charge.to_i,
|
|
87
|
+
message: "must be $#{'%0.2f' % (EffectiveOrders.minimum_charge.to_i / 100.0)} or more. Please add additional items."
|
|
88
|
+
}, unless: -> { (free? && EffectiveOrders.free?) || (refund? && EffectiveOrders.refund?) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
validate(if: -> { tax_rate.present? }) do
|
|
92
|
+
if (tax_rate > 100.0 || (tax_rate < 0.25 && tax_rate > 0.0000))
|
|
93
|
+
errors.add(:tax_rate, "is invalid. expected a value between 100.0 (100%) and 0.25 (0.25%) or 0")
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# User validations -- An admin skips these when working in the admin/ namespace
|
|
98
|
+
with_options unless: -> { pending? || skip_buyer_validations? } do
|
|
99
|
+
validates :tax_rate, presence: { message: "can't be determined based on billing address" }
|
|
100
|
+
validates :tax, presence: true
|
|
101
|
+
|
|
102
|
+
if EffectiveOrders.billing_address
|
|
103
|
+
validates :billing_address, presence: true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
if EffectiveOrders.shipping_address
|
|
107
|
+
validates :shipping_address, presence: true
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
if EffectiveOrders.collect_note_required
|
|
111
|
+
validates :note, presence: true
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
with_options if: -> { confirmed? && !skip_buyer_validations? } do
|
|
116
|
+
if EffectiveOrders.terms_and_conditions
|
|
117
|
+
validates :terms_and_conditions, presence: true
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# When Purchased
|
|
122
|
+
with_options if: -> { purchased? } do
|
|
123
|
+
validates :purchased_at, presence: true
|
|
124
|
+
validates :payment, presence: true
|
|
125
|
+
|
|
126
|
+
validates :payment_provider, presence: true, inclusion: { in: EffectiveOrders.payment_providers }
|
|
127
|
+
validates :payment_card, presence: true
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
with_options if: -> { deferred? } do
|
|
131
|
+
validates :payment_provider, presence: true, inclusion: { in: EffectiveOrders.deferred_providers }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
scope :deep, -> { includes(:user, order_items: :purchasable) }
|
|
135
|
+
scope :sorted, -> { order(:id) }
|
|
136
|
+
|
|
137
|
+
scope :purchased, -> { where(state: EffectiveOrders::PURCHASED) }
|
|
138
|
+
scope :purchased_by, lambda { |user| purchased.where(user: user) }
|
|
139
|
+
scope :not_purchased, -> { where.not(state: EffectiveOrders::PURCHASED) }
|
|
140
|
+
|
|
141
|
+
scope :pending, -> { where(state: EffectiveOrders::PENDING) }
|
|
142
|
+
scope :confirmed, -> { where(state: EffectiveOrders::CONFIRMED) }
|
|
143
|
+
scope :deferred, -> { where(state: EffectiveOrders::DEFERRED) }
|
|
144
|
+
scope :declined, -> { where(state: EffectiveOrders::DECLINED) }
|
|
145
|
+
scope :refunds, -> { purchased.where('total < ?', 0) }
|
|
146
|
+
|
|
147
|
+
# Effective::Order.new()
|
|
148
|
+
# Effective::Order.new(Product.first)
|
|
149
|
+
# Effective::Order.new(current_cart)
|
|
150
|
+
# Effective::Order.new(Effective::Order.last)
|
|
151
|
+
|
|
152
|
+
# Effective::Order.new(items: Product.first)
|
|
153
|
+
# Effective::Order.new(items: [Product.first, Product.second], user: User.first)
|
|
154
|
+
# Effective::Order.new(items: Product.first, user: User.first, billing_address: Effective::Address.new, shipping_address: Effective::Address.new)
|
|
155
|
+
|
|
156
|
+
def initialize(atts = nil, &block)
|
|
157
|
+
super(state: EffectiveOrders::PENDING) # Initialize with state: PENDING
|
|
158
|
+
|
|
159
|
+
return unless atts.present?
|
|
160
|
+
|
|
161
|
+
if atts.kind_of?(Hash)
|
|
162
|
+
items = Array(atts.delete(:item)) + Array(atts.delete(:items))
|
|
163
|
+
|
|
164
|
+
self.user = atts.delete(:user) || (items.first.user if items.first.respond_to?(:user))
|
|
165
|
+
|
|
166
|
+
if (address = atts.delete(:billing_address)).present?
|
|
167
|
+
self.billing_address = address
|
|
168
|
+
self.billing_address.full_name ||= user.to_s.presence
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
if (address = atts.delete(:shipping_address)).present?
|
|
172
|
+
self.shipping_address = address
|
|
173
|
+
self.shipping_address.full_name ||= user.to_s.presence
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
atts.each { |key, value| self.send("#{key}=", value) }
|
|
177
|
+
|
|
178
|
+
add(items) if items.present?
|
|
179
|
+
else # Attributes are not a Hash
|
|
180
|
+
self.user = atts.user if atts.respond_to?(:user)
|
|
181
|
+
add(atts)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Items can be an Effective::Cart, an Effective::order, a single acts_as_purchasable, or multiple acts_as_purchasables
|
|
186
|
+
# add(Product.first) => returns an Effective::OrderItem
|
|
187
|
+
# add(Product.first, current_cart) => returns an array of Effective::OrderItems
|
|
188
|
+
def add(*items, quantity: 1)
|
|
189
|
+
raise 'unable to alter a purchased order' if purchased?
|
|
190
|
+
raise 'unable to alter a declined order' if declined?
|
|
191
|
+
|
|
192
|
+
cart_items = items.flatten.flat_map do |item|
|
|
193
|
+
if item.kind_of?(Effective::Cart)
|
|
194
|
+
item.cart_items.to_a
|
|
195
|
+
elsif item.kind_of?(ActsAsPurchasable)
|
|
196
|
+
Effective::CartItem.new(quantity: quantity, purchasable: item)
|
|
197
|
+
elsif item.kind_of?(Effective::Order)
|
|
198
|
+
# Duplicate an existing order
|
|
199
|
+
self.note_to_buyer ||= item.note_to_buyer
|
|
200
|
+
self.note_internal ||= item.note_internal
|
|
201
|
+
|
|
202
|
+
item.order_items.select { |oi| oi.purchasable.kind_of?(Effective::Product) }.map do |oi|
|
|
203
|
+
product = Effective::Product.new(name: oi.purchasable.purchasable_name, price: oi.purchasable.price, tax_exempt: oi.purchasable.tax_exempt)
|
|
204
|
+
Effective::CartItem.new(quantity: oi.quantity, purchasable: product)
|
|
205
|
+
end
|
|
206
|
+
else
|
|
207
|
+
raise 'add() expects one or more acts_as_purchasable objects, or an Effective::Cart'
|
|
208
|
+
end
|
|
209
|
+
end.compact
|
|
210
|
+
|
|
211
|
+
# Make sure to reset stored aggregates
|
|
212
|
+
self.total = nil
|
|
213
|
+
self.subtotal = nil
|
|
214
|
+
self.tax = nil
|
|
215
|
+
|
|
216
|
+
retval = cart_items.map do |item|
|
|
217
|
+
order_items.build(
|
|
218
|
+
name: item.name,
|
|
219
|
+
quantity: item.quantity,
|
|
220
|
+
price: item.price,
|
|
221
|
+
tax_exempt: (item.tax_exempt || false),
|
|
222
|
+
).tap { |order_item| order_item.purchasable = item.purchasable }
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
retval.size == 1 ? retval.first : retval
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def to_s
|
|
229
|
+
if refund?
|
|
230
|
+
"Refund ##{to_param}"
|
|
231
|
+
elsif purchased?
|
|
232
|
+
"Receipt ##{to_param}"
|
|
233
|
+
elsif pending?
|
|
234
|
+
"Pending Order ##{to_param}"
|
|
235
|
+
else
|
|
236
|
+
"Order ##{to_param}"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def pending?
|
|
241
|
+
state == EffectiveOrders::PENDING
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def confirmed?
|
|
245
|
+
state == EffectiveOrders::CONFIRMED
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def deferred?
|
|
249
|
+
state == EffectiveOrders::DEFERRED
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def purchased?(provider = nil)
|
|
253
|
+
return false if (state != EffectiveOrders::PURCHASED)
|
|
254
|
+
return true if provider.nil? || payment_provider == provider.to_s
|
|
255
|
+
false
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def declined?
|
|
259
|
+
state == EffectiveOrders::DECLINED
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def purchasables
|
|
263
|
+
order_items.map { |order_item| order_item.purchasable }
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def subtotal
|
|
267
|
+
self[:subtotal] || order_items.map { |oi| oi.subtotal }.sum
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def tax_rate
|
|
271
|
+
self[:tax_rate] || get_tax_rate()
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def tax
|
|
275
|
+
self[:tax] || get_tax()
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def total
|
|
279
|
+
(self[:total] || (subtotal + tax.to_i)).to_i
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def free?
|
|
283
|
+
total == 0
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def refund?
|
|
287
|
+
total.to_i < 0
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def num_items
|
|
291
|
+
order_items.map { |oi| oi.quantity }.sum
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def send_payment_request_to_buyer?
|
|
295
|
+
truthy?(send_payment_request_to_buyer) && !free? && !refund?
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def send_mark_as_paid_email_to_buyer?
|
|
299
|
+
truthy?(send_mark_as_paid_email_to_buyer)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def skip_buyer_validations?
|
|
303
|
+
truthy?(skip_buyer_validations)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# This is called from admin/orders#create
|
|
307
|
+
# This is intended for use as an admin action only
|
|
308
|
+
# It skips any address or bad user validations
|
|
309
|
+
# It's basically the same as save! on a new order, except it might send the payment request to buyer
|
|
310
|
+
def pending!
|
|
311
|
+
self.state = EffectiveOrders::PENDING
|
|
312
|
+
self.addresses.clear if addresses.any? { |address| address.valid? == false }
|
|
313
|
+
save!
|
|
314
|
+
|
|
315
|
+
send_payment_request_to_buyer! if send_payment_request_to_buyer?
|
|
316
|
+
true
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Used by admin checkout only
|
|
320
|
+
def confirm!
|
|
321
|
+
update!(state: EffectiveOrders::CONFIRMED)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# This lets us skip to the confirmed workflow for an admin...
|
|
325
|
+
def assign_confirmed_if_valid!
|
|
326
|
+
return unless pending?
|
|
327
|
+
|
|
328
|
+
self.state = EffectiveOrders::CONFIRMED
|
|
329
|
+
return true if valid?
|
|
330
|
+
|
|
331
|
+
self.errors.clear
|
|
332
|
+
self.state = EffectiveOrders::PENDING
|
|
333
|
+
false
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Effective::Order.new(items: Product.first, user: User.first).purchase!(email: false)
|
|
337
|
+
def purchase!(payment: 'none', provider: 'none', card: 'none', email: true, skip_buyer_validations: false)
|
|
338
|
+
return false if purchased?
|
|
339
|
+
error = nil
|
|
340
|
+
|
|
341
|
+
assign_attributes(
|
|
342
|
+
state: EffectiveOrders::PURCHASED,
|
|
343
|
+
payment: payment_to_h(payment),
|
|
344
|
+
payment_provider: provider,
|
|
345
|
+
payment_card: (card.presence || 'none'),
|
|
346
|
+
skip_buyer_validations: skip_buyer_validations
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
self.purchased_at ||= Time.zone.now
|
|
350
|
+
|
|
351
|
+
Effective::Order.transaction do
|
|
352
|
+
begin
|
|
353
|
+
run_purchasable_callbacks(:before_purchase)
|
|
354
|
+
save!
|
|
355
|
+
update_purchasables_purchased_order!
|
|
356
|
+
rescue => e
|
|
357
|
+
self.state = state_was
|
|
358
|
+
self.purchased_at = nil
|
|
359
|
+
|
|
360
|
+
error = e.message
|
|
361
|
+
raise ::ActiveRecord::Rollback
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
raise "Failed to purchase order: #{error || errors.full_messages.to_sentence}" unless error.nil?
|
|
366
|
+
|
|
367
|
+
send_refund_notification! if email && refund?
|
|
368
|
+
send_order_receipts! if email
|
|
369
|
+
|
|
370
|
+
run_purchasable_callbacks(:after_purchase)
|
|
371
|
+
|
|
372
|
+
true
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def defer!(provider: 'none', email: true)
|
|
376
|
+
return false if purchased?
|
|
377
|
+
|
|
378
|
+
assign_attributes(
|
|
379
|
+
state: EffectiveOrders::DEFERRED,
|
|
380
|
+
payment_provider: provider
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
save!
|
|
384
|
+
|
|
385
|
+
send_payment_request_to_buyer! if email
|
|
386
|
+
|
|
387
|
+
true
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def decline!(payment: 'none', provider: 'none', card: 'none', validate: true)
|
|
391
|
+
return false if declined?
|
|
392
|
+
|
|
393
|
+
raise EffectiveOrders::AlreadyPurchasedException.new('order already purchased') if purchased?
|
|
394
|
+
|
|
395
|
+
error = nil
|
|
396
|
+
|
|
397
|
+
assign_attributes(
|
|
398
|
+
state: EffectiveOrders::DECLINED,
|
|
399
|
+
purchased_at: nil,
|
|
400
|
+
payment: payment_to_h(payment),
|
|
401
|
+
payment_provider: provider,
|
|
402
|
+
payment_card: (card.presence || 'none'),
|
|
403
|
+
skip_buyer_validations: true
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
Effective::Order.transaction do
|
|
407
|
+
begin
|
|
408
|
+
save!(validate: validate)
|
|
409
|
+
rescue => e
|
|
410
|
+
self.state = state_was
|
|
411
|
+
|
|
412
|
+
error = e.message
|
|
413
|
+
raise ::ActiveRecord::Rollback
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
raise "Failed to decline order: #{error || errors.full_messages.to_sentence}" unless error.nil?
|
|
418
|
+
|
|
419
|
+
run_purchasable_callbacks(:after_decline)
|
|
420
|
+
|
|
421
|
+
true
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Doesn't control anything. Purely for the flash messaging
|
|
425
|
+
def emails_send_to
|
|
426
|
+
[email, cc.presence].compact.to_sentence
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def send_order_receipts!
|
|
430
|
+
send_order_receipt_to_admin! if EffectiveOrders.mailer[:send_order_receipt_to_admin]
|
|
431
|
+
send_order_receipt_to_buyer! if EffectiveOrders.mailer[:send_order_receipt_to_buyer]
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def send_order_receipt_to_admin!
|
|
435
|
+
send_email(:order_receipt_to_admin, to_param) if purchased?
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def send_order_receipt_to_buyer!
|
|
439
|
+
send_email(:order_receipt_to_buyer, to_param) if purchased?
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def send_payment_request_to_buyer!
|
|
443
|
+
send_email(:payment_request_to_buyer, to_param) unless purchased?
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def send_pending_order_invoice_to_buyer!
|
|
447
|
+
send_email(:pending_order_invoice_to_buyer, to_param) unless purchased?
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def send_refund_notification!
|
|
451
|
+
send_email(:refund_notification_to_admin, to_param) if purchased? && refund?
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def skip_qb_sync!
|
|
455
|
+
defined?(EffectiveQbSync) ? EffectiveQbSync.skip_order!(self) : true
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
protected
|
|
459
|
+
|
|
460
|
+
def get_tax_rate
|
|
461
|
+
rate = instance_exec(self, &EffectiveOrders.order_tax_rate_method).to_f
|
|
462
|
+
|
|
463
|
+
if (rate > 100.0 || (rate < 0.25 && rate > 0.0000))
|
|
464
|
+
raise "expected EffectiveOrders.order_tax_rate_method to return a value between 100.0 (100%) and 0.25 (0.25%) or 0 or nil. Received #{rate}. Please return 5.25 for 5.25% tax."
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
rate
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def get_tax
|
|
471
|
+
return nil unless tax_rate.present?
|
|
472
|
+
order_items.reject { |oi| oi.tax_exempt? }.map { |oi| (oi.subtotal * (tax_rate / 100.0)).round(0).to_i }.sum
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
private
|
|
476
|
+
|
|
477
|
+
def assign_order_totals
|
|
478
|
+
self.subtotal = order_items.map { |oi| oi.subtotal }.sum
|
|
479
|
+
self.tax_rate = get_tax_rate() unless (tax_rate || 0) > 0
|
|
480
|
+
self.tax = get_tax()
|
|
481
|
+
self.total = subtotal + (tax || 0)
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def assign_billing_name
|
|
485
|
+
self.billing_name = [(billing_address.full_name.presence if billing_address.present?), (user.to_s.presence)].compact.first
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def assign_email
|
|
489
|
+
self.email = user&.email
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def assign_last_address
|
|
493
|
+
return unless user.present?
|
|
494
|
+
return unless (EffectiveOrders.billing_address || EffectiveOrders.shipping_address)
|
|
495
|
+
return if EffectiveOrders.billing_address && billing_address.present?
|
|
496
|
+
return if EffectiveOrders.shipping_address && shipping_address.present?
|
|
497
|
+
|
|
498
|
+
last_order = Effective::Order.sorted.where(user: user).last
|
|
499
|
+
return unless last_order.present?
|
|
500
|
+
|
|
501
|
+
if EffectiveOrders.billing_address && last_order.billing_address.present?
|
|
502
|
+
self.billing_address = last_order.billing_address
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
if EffectiveOrders.shipping_address && last_order.shipping_address.present?
|
|
506
|
+
self.shipping_address = last_order.shipping_address
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def update_purchasables_purchased_order!
|
|
511
|
+
order_items.each { |oi| oi.purchasable&.update_column(:purchased_order_id, self.id) }
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def run_purchasable_callbacks(name)
|
|
515
|
+
order_items.each { |oi| oi.purchasable.public_send(name, self, oi) if oi.purchasable.respond_to?(name) }
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def send_email(email, *mailer_args)
|
|
519
|
+
Effective::OrdersMailer.public_send(email, *mailer_args).public_send(EffectiveOrders.mailer[:deliver_method])
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
def truthy?(value)
|
|
523
|
+
if defined?(::ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES) # Rails <5
|
|
524
|
+
::ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(value)
|
|
525
|
+
else
|
|
526
|
+
::ActiveRecord::Type::Boolean.new.cast(value)
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def payment_to_h(payment)
|
|
531
|
+
if payment.respond_to?(:to_unsafe_h)
|
|
532
|
+
payment.to_unsafe_h.to_h
|
|
533
|
+
elsif payment.respond_to?(:to_h)
|
|
534
|
+
payment.to_h
|
|
535
|
+
else
|
|
536
|
+
{ details: (payment.to_s.presence || 'none') }
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
end
|
|
541
|
+
end
|