effective_orders 4.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (135) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +1004 -0
  4. data/app/assets/images/effective_orders/stripe.png +0 -0
  5. data/app/assets/javascripts/effective_orders.js +6 -0
  6. data/app/assets/javascripts/effective_orders/customers.js.coffee +32 -0
  7. data/app/assets/javascripts/effective_orders/providers/stripe.js.coffee +77 -0
  8. data/app/assets/javascripts/effective_orders/subscriptions.js.coffee +81 -0
  9. data/app/assets/stylesheets/effective_orders.scss +2 -0
  10. data/app/assets/stylesheets/effective_orders/_cart.scss +4 -0
  11. data/app/assets/stylesheets/effective_orders/_order.scss +58 -0
  12. data/app/controllers/admin/customers_controller.rb +24 -0
  13. data/app/controllers/admin/order_items_controller.rb +16 -0
  14. data/app/controllers/admin/orders_controller.rb +223 -0
  15. data/app/controllers/effective/carts_controller.rb +85 -0
  16. data/app/controllers/effective/concerns/purchase.rb +62 -0
  17. data/app/controllers/effective/customers_controller.rb +20 -0
  18. data/app/controllers/effective/orders_controller.rb +162 -0
  19. data/app/controllers/effective/providers/cheque.rb +22 -0
  20. data/app/controllers/effective/providers/free.rb +33 -0
  21. data/app/controllers/effective/providers/mark_as_paid.rb +33 -0
  22. data/app/controllers/effective/providers/moneris.rb +60 -0
  23. data/app/controllers/effective/providers/paypal.rb +33 -0
  24. data/app/controllers/effective/providers/phone.rb +22 -0
  25. data/app/controllers/effective/providers/pretend.rb +26 -0
  26. data/app/controllers/effective/providers/refund.rb +33 -0
  27. data/app/controllers/effective/providers/stripe.rb +72 -0
  28. data/app/controllers/effective/subscripter_controller.rb +18 -0
  29. data/app/controllers/effective/webhooks_controller.rb +109 -0
  30. data/app/datatables/admin/effective_customers_datatable.rb +22 -0
  31. data/app/datatables/admin/effective_orders_datatable.rb +100 -0
  32. data/app/datatables/effective_orders_datatable.rb +79 -0
  33. data/app/helpers/effective_carts_helper.rb +113 -0
  34. data/app/helpers/effective_orders_helper.rb +143 -0
  35. data/app/helpers/effective_paypal_helper.rb +49 -0
  36. data/app/helpers/effective_stripe_helper.rb +85 -0
  37. data/app/helpers/effective_subscriptions_helper.rb +34 -0
  38. data/app/mailers/effective/orders_mailer.rb +196 -0
  39. data/app/models/concerns/acts_as_purchasable.rb +118 -0
  40. data/app/models/concerns/acts_as_subscribable.rb +90 -0
  41. data/app/models/concerns/acts_as_subscribable_buyer.rb +49 -0
  42. data/app/models/effective/access_denied.rb +17 -0
  43. data/app/models/effective/cart.rb +88 -0
  44. data/app/models/effective/cart_item.rb +40 -0
  45. data/app/models/effective/customer.rb +92 -0
  46. data/app/models/effective/order.rb +541 -0
  47. data/app/models/effective/order_item.rb +63 -0
  48. data/app/models/effective/product.rb +23 -0
  49. data/app/models/effective/sold_out_validator.rb +7 -0
  50. data/app/models/effective/subscripter.rb +185 -0
  51. data/app/models/effective/subscription.rb +95 -0
  52. data/app/models/effective/tax_rate_calculator.rb +48 -0
  53. data/app/views/admin/customers/_actions.html.haml +2 -0
  54. data/app/views/admin/customers/index.html.haml +6 -0
  55. data/app/views/admin/customers/show.html.haml +6 -0
  56. data/app/views/admin/order_items/index.html.haml +3 -0
  57. data/app/views/admin/orders/_datatable_actions.html.haml +18 -0
  58. data/app/views/admin/orders/_form.html.haml +35 -0
  59. data/app/views/admin/orders/_form_note_internal.html.haml +7 -0
  60. data/app/views/admin/orders/_order_actions.html.haml +9 -0
  61. data/app/views/admin/orders/_order_item_fields.html.haml +14 -0
  62. data/app/views/admin/orders/checkout.html.haml +3 -0
  63. data/app/views/admin/orders/edit.html.haml +6 -0
  64. data/app/views/admin/orders/index.html.haml +6 -0
  65. data/app/views/admin/orders/new.html.haml +4 -0
  66. data/app/views/admin/orders/show.html.haml +4 -0
  67. data/app/views/effective/carts/_cart.html.haml +28 -0
  68. data/app/views/effective/carts/_cart_actions.html.haml +3 -0
  69. data/app/views/effective/carts/show.html.haml +17 -0
  70. data/app/views/effective/customers/_customer.html.haml +72 -0
  71. data/app/views/effective/customers/_form.html.haml +21 -0
  72. data/app/views/effective/customers/edit.html.haml +4 -0
  73. data/app/views/effective/customers/update.js.erb +5 -0
  74. data/app/views/effective/orders/_checkout_actions.html.haml +3 -0
  75. data/app/views/effective/orders/_checkout_step1.html.haml +4 -0
  76. data/app/views/effective/orders/_checkout_step2.html.haml +37 -0
  77. data/app/views/effective/orders/_datatable_actions.html.haml +2 -0
  78. data/app/views/effective/orders/_fields.html.haml +31 -0
  79. data/app/views/effective/orders/_fields_note.html.haml +7 -0
  80. data/app/views/effective/orders/_fields_terms.html.haml +8 -0
  81. data/app/views/effective/orders/_order.html.haml +11 -0
  82. data/app/views/effective/orders/_order_actions.html.haml +18 -0
  83. data/app/views/effective/orders/_order_deferred.html.haml +9 -0
  84. data/app/views/effective/orders/_order_footer.html.haml +1 -0
  85. data/app/views/effective/orders/_order_header.html.haml +23 -0
  86. data/app/views/effective/orders/_order_items.html.haml +72 -0
  87. data/app/views/effective/orders/_order_notes.html.haml +17 -0
  88. data/app/views/effective/orders/_order_payment.html.haml +24 -0
  89. data/app/views/effective/orders/_order_shipping.html.haml +30 -0
  90. data/app/views/effective/orders/_orders_table.html.haml +23 -0
  91. data/app/views/effective/orders/cheque/_form.html.haml +4 -0
  92. data/app/views/effective/orders/declined.html.haml +12 -0
  93. data/app/views/effective/orders/deferred.html.haml +13 -0
  94. data/app/views/effective/orders/deferred/_form.html.haml +16 -0
  95. data/app/views/effective/orders/edit.html.haml +3 -0
  96. data/app/views/effective/orders/free/_form.html.haml +5 -0
  97. data/app/views/effective/orders/index.html.haml +3 -0
  98. data/app/views/effective/orders/mark_as_paid/_form.html.haml +23 -0
  99. data/app/views/effective/orders/moneris/_form.html.haml +47 -0
  100. data/app/views/effective/orders/new.html.haml +3 -0
  101. data/app/views/effective/orders/paypal/_form.html.haml +5 -0
  102. data/app/views/effective/orders/phone/_form.html.haml +4 -0
  103. data/app/views/effective/orders/pretend/_form.html.haml +8 -0
  104. data/app/views/effective/orders/purchased.html.haml +11 -0
  105. data/app/views/effective/orders/refund/_form.html.haml +5 -0
  106. data/app/views/effective/orders/show.html.haml +6 -0
  107. data/app/views/effective/orders/stripe/_element.html.haml +8 -0
  108. data/app/views/effective/orders/stripe/_form.html.haml +31 -0
  109. data/app/views/effective/orders_mailer/order_error.html.haml +11 -0
  110. data/app/views/effective/orders_mailer/order_receipt_to_admin.html.haml +2 -0
  111. data/app/views/effective/orders_mailer/order_receipt_to_buyer.html.haml +2 -0
  112. data/app/views/effective/orders_mailer/payment_request_to_buyer.html.haml +13 -0
  113. data/app/views/effective/orders_mailer/pending_order_invoice_to_buyer.html.haml +13 -0
  114. data/app/views/effective/orders_mailer/refund_notification_to_admin.html.haml +15 -0
  115. data/app/views/effective/orders_mailer/subscription_canceled.html.haml +9 -0
  116. data/app/views/effective/orders_mailer/subscription_created.html.haml +13 -0
  117. data/app/views/effective/orders_mailer/subscription_event_to_admin.html.haml +13 -0
  118. data/app/views/effective/orders_mailer/subscription_payment_failed.html.haml +9 -0
  119. data/app/views/effective/orders_mailer/subscription_payment_succeeded.html.haml +9 -0
  120. data/app/views/effective/orders_mailer/subscription_trial_expired.html.haml +5 -0
  121. data/app/views/effective/orders_mailer/subscription_trialing.html.haml +7 -0
  122. data/app/views/effective/orders_mailer/subscription_updated.html.haml +13 -0
  123. data/app/views/effective/subscripter/_form.html.haml +60 -0
  124. data/app/views/effective/subscripter/_plan.html.haml +23 -0
  125. data/app/views/layouts/effective_orders_mailer_layout.html.haml +25 -0
  126. data/config/effective_orders.rb +279 -0
  127. data/config/routes.rb +70 -0
  128. data/db/migrate/01_create_effective_orders.rb.erb +137 -0
  129. data/lib/effective_orders.rb +243 -0
  130. data/lib/effective_orders/engine.rb +60 -0
  131. data/lib/effective_orders/version.rb +3 -0
  132. data/lib/generators/effective_orders/install_generator.rb +63 -0
  133. data/lib/generators/templates/effective_orders_mailer_preview.rb +120 -0
  134. data/lib/tasks/effective_orders_tasks.rake +69 -0
  135. 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