effective_orders 6.11.0 → 6.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/effective_orders/providers/deluxe_delayed.js +32 -0
  3. data/app/assets/stylesheets/effective_orders/_order.scss +4 -0
  4. data/app/controllers/effective/concerns/purchase.rb +17 -0
  5. data/app/controllers/effective/orders_controller.rb +2 -0
  6. data/app/controllers/effective/providers/deluxe.rb +13 -25
  7. data/app/controllers/effective/providers/deluxe_delayed.rb +48 -0
  8. data/app/controllers/effective/providers/deluxe_delayed_purchase.rb +50 -0
  9. data/app/datatables/admin/effective_orders_datatable.rb +8 -0
  10. data/app/datatables/effective_orders_datatable.rb +6 -0
  11. data/app/helpers/effective_deluxe_delayed_helper.rb +15 -0
  12. data/app/helpers/effective_orders_helper.rb +2 -0
  13. data/app/models/effective/deluxe_api.rb +61 -0
  14. data/app/models/effective/order.rb +126 -43
  15. data/app/views/effective/orders/_checkout_step2.html.haml +7 -0
  16. data/app/views/effective/orders/_datatable_actions.html.haml +1 -1
  17. data/app/views/effective/orders/_order_deferred.html.haml +5 -1
  18. data/app/views/effective/orders/delayed/_form.html.haml +25 -0
  19. data/app/views/effective/orders/delayed/_form_purchase.html.haml +30 -0
  20. data/app/views/effective/orders/deluxe/_form.html.haml +1 -4
  21. data/app/views/effective/orders/deluxe_delayed/_css.html.haml +12 -0
  22. data/app/views/effective/orders/deluxe_delayed/_element.html.haml +9 -0
  23. data/app/views/effective/orders/deluxe_delayed/_form.html.haml +10 -0
  24. data/app/views/effective/orders/deluxe_delayed/_form_purchase.html.haml +18 -0
  25. data/app/views/effective/orders/mark_as_paid/_form.html.haml +2 -0
  26. data/config/effective_orders.rb +9 -0
  27. data/config/routes.rb +4 -1
  28. data/db/migrate/101_create_effective_orders.rb +6 -0
  29. data/lib/effective_orders/version.rb +1 -1
  30. data/lib/effective_orders.rb +14 -2
  31. data/lib/tasks/effective_orders_tasks.rake +6 -0
  32. metadata +12 -3
  33. data/app/assets/images/effective_orders/deluxe.png +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a3716b7b619276755bac8d33027424eed7b97519fdc1454d498349074e2a022
4
- data.tar.gz: 5071eb8c4733b659c1d6e72f0ad7974301af0c32d0df6701547eedddaefc723b
3
+ metadata.gz: 4fac0b561dd727abc645294816cf623fb2c9bd345645b19272e510d064b0c3e8
4
+ data.tar.gz: 63728a03677c11b462de51955b4add5434fcbdd0cd7e015013ab81f1672d3da0
5
5
  SHA512:
6
- metadata.gz: 30ee25cf450e876da9a219c303c51d1469994fc055d039fe610e7abcddd3d988bc7fa5558ac17dcf05f06ff8afc0d23d6094a1aa106271b091b0fe2c69390310
7
- data.tar.gz: abc8e9d7fac67cac46be97ad522f7ffce5e64a47a4a12e8bc9d6f1c17368788dceab075b9ef302f7d937872ebf7f16cbcaabd5d5f9b38ba4f316a46c5dcedc9e
6
+ metadata.gz: 78754fcf99effaba035ec15bb3ae31f322b84991c9b3a40d80c995493016589d5bdfd28bab4655a5a4f8a0a01c99e5c16a01c95bc275e04ecc33e72bf499b590
7
+ data.tar.gz: 5d2ed250eca616729e602f5f5b3374ad16b71f00e8701ac70e5545b3c370900a36d58ab507835f611aab9caa5b3cf114c40ebb4f1bad4e45d6efe61b2bb727c3
@@ -0,0 +1,32 @@
1
+ // https://developer.deluxe.com/s/article-hosted-payment-form
2
+
3
+ function initializeDeluxeDelayed() {
4
+ let $deluxe = $('form[data-deluxe-delayed-checkout]:not(.initialized)');
5
+ if($deluxe.length == 0) return;
6
+
7
+ let options = $deluxe.data('deluxe-delayed-checkout');
8
+
9
+ HostedForm.init(options, {
10
+ onFailure: (data) => { $('#deluxe-delayed-checkout-errors').text(JSON.stringify(data)); },
11
+ onInvalid: (data) => { $('#deluxe-delayed-checkout-errors').text(JSON.stringify(data)); },
12
+
13
+ onSuccess: (data) => {
14
+ let value = btoa(JSON.stringify(data)); // A base64 encoded JSON object
15
+
16
+ $form = $('form[data-deluxe-delayed-checkout]').first();
17
+ $form.find('input[name="deluxe_delayed[payment_intent]"]').val(value);
18
+ $form.submit();
19
+
20
+ $('#deluxeDelayedCheckout').fadeOut('slow');
21
+ $('#deluxe-delayed-checkout-loading').text('Thank you! Saving card information. Please wait...');
22
+ },
23
+ }).then((instance) => {
24
+ $('#deluxe-delayed-checkout-loading').text('');
25
+ instance.renderHpf();
26
+ });
27
+
28
+ $deluxe.addClass('initialized');
29
+ };
30
+
31
+ $(document).ready(function() { initializeDeluxeDelayed() });
32
+ $(document).on('turbolinks:load', function() { initializeDeluxeDelayed() });
@@ -60,6 +60,10 @@ form.new_effective_order {
60
60
  #deluxeCheckout { height: 280px; }
61
61
  }
62
62
 
63
+ .effective-deluxe-delayed-checkout {
64
+ #deluxeDelayedCheckout { height: 280px; }
65
+ }
66
+
63
67
  @media print {
64
68
  .effective-orders-page-content { display: none; }
65
69
 
@@ -50,6 +50,23 @@ module Effective
50
50
  redirect_to deferred_url.gsub(':id', @order.to_param.to_s)
51
51
  end
52
52
 
53
+ def order_delayed(payment:, payment_intent:, provider:, card: 'none', deferred_url: nil, email: false)
54
+ @order.delay!(payment: payment, payment_intent: payment_intent, provider: provider, card: card, email: email)
55
+
56
+ Effective::Cart.where(user: current_user).destroy_all if current_user.present?
57
+
58
+ if flash[:success].blank?
59
+ if email
60
+ flash[:success] = "Delayed payment created! An email has been sent to #{@order.emails_send_to}"
61
+ else
62
+ flash[:success] = "Delayed payment created!"
63
+ end
64
+ end
65
+
66
+ deferred_url = effective_orders.deferred_order_path(':id') if deferred_url.blank?
67
+ redirect_to deferred_url.gsub(':id', @order.to_param.to_s)
68
+ end
69
+
53
70
  def order_declined(payment:, provider:, card: 'none', declined_url: nil)
54
71
  @order.decline!(payment: payment, provider: provider, card: card)
55
72
 
@@ -5,6 +5,8 @@ module Effective
5
5
 
6
6
  include Providers::Cheque
7
7
  include Providers::Deluxe
8
+ include Providers::DeluxeDelayed
9
+ include Providers::DeluxeDelayedPurchase
8
10
  include Providers::Etransfer
9
11
  include Providers::Free
10
12
  include Providers::MarkAsPaid
@@ -11,41 +11,33 @@ module Effective
11
11
  EffectiveResources.authorize!(self, :update, @order)
12
12
 
13
13
  ## Process Payment Intent
14
+ api = Effective::DeluxeApi.new
14
15
 
15
16
  # The payment_intent is set by the Deluxe HostedPaymentForm
16
- payment_intent = deluxe_params[:payment_intent]
17
+ payment_intent_payload = deluxe_params[:payment_intent]
17
18
 
18
- if payment_intent.blank?
19
+ if payment_intent_payload.blank?
19
20
  flash[:danger] = 'Unable to process deluxe order without payment. please try again.'
20
- return order_not_processed(declined_url: payment_intent[:declined_url])
21
+ return order_not_processed(declined_url: deluxe_params[:declined_url])
21
22
  end
22
23
 
23
24
  # Decode the base64 encoded JSON object into a Hash
24
- payment_intent = (JSON.parse(Base64.decode64(payment_intent)) rescue nil)
25
- raise('expected payment_intent to be a Hash') unless payment_intent.kind_of?(Hash)
26
- raise('expected a token payment') unless payment_intent['type'] == 'Token'
25
+ payment_intent = api.decode_payment_intent_payload(payment_intent_payload)
26
+ card_info = api.card_info(payment_intent)
27
27
 
28
- valid = payment_intent['status'] == 'success'
28
+ valid = (payment_intent['status'] == 'success')
29
29
 
30
30
  if valid == false
31
- card_info = deluxe_api.card_info(payment_intent)
32
- return order_declined(payment: card_info, provider: 'deluxe', card: card_info['card'], declined_url: declined_url)
31
+ return order_declined(payment: card_info, provider: 'deluxe', card: card_info['card'], declined_url: deluxe_params[:declined_url])
33
32
  end
34
33
 
35
- ## Process Authorization
36
- authorization = deluxe_api.authorize_payment(@order, payment_intent)
37
- valid = [0].include?(authorization['responseCode'])
34
+ ## Purchase Order right now
35
+ purchased = api.purchase!(@order, payment_intent)
38
36
 
39
- if valid == false
40
- flash[:danger] = "Payment was unsuccessful. The credit card authorization failed with message: #{Array(authorization['responseMessage']).to_sentence.presence || 'none'}. Please try again."
41
- return order_declined(payment: authorization, provider: 'deluxe', card: authorization['card'], declined_url: deluxe_params[:declined_url])
42
- end
43
-
44
- ## Complete Payment
45
- payment = deluxe_api.complete_payment(@order, authorization)
46
- valid = [0].include?(payment['responseCode'])
37
+ payment = api.payment
38
+ raise('expected a payment Hash') unless payment.kind_of?(Hash)
47
39
 
48
- if valid == false
40
+ if purchased == false
49
41
  flash[:danger] = "Payment was unsuccessful. The credit card payment failed with message: #{Array(payment['responseMessage']).to_sentence.presence || 'none'}. Please try again."
50
42
  return order_declined(payment: payment, provider: 'deluxe', card: payment['card'], declined_url: deluxe_params[:declined_url])
51
43
  end
@@ -66,10 +58,6 @@ module Effective
66
58
  params.require(:deluxe).permit(:payment_intent, :purchased_url, :declined_url)
67
59
  end
68
60
 
69
- def deluxe_api
70
- @deluxe_api ||= Effective::DeluxeApi.new
71
- end
72
-
73
61
  end
74
62
  end
75
63
  end
@@ -0,0 +1,48 @@
1
+ module Effective
2
+ module Providers
3
+ module DeluxeDelayed
4
+ extend ActiveSupport::Concern
5
+
6
+ def deluxe_delayed
7
+ raise('deluxe provider is not available') unless EffectiveOrders.deluxe?
8
+ raise('deluxe_delayed provider is not available') unless EffectiveOrders.deluxe_delayed?
9
+
10
+ @order = Effective::Order.deep.find(params[:id])
11
+
12
+ EffectiveResources.authorize!(self, :update, @order)
13
+
14
+ ## Process Payment Intent
15
+ api = Effective::DeluxeApi.new
16
+
17
+ # The payment_intent is set by the Deluxe HostedPaymentForm
18
+ payment_intent_payload = deluxe_delayed_params[:payment_intent]
19
+
20
+ if payment_intent_payload.blank?
21
+ flash[:danger] = 'Unable to process deluxe delayed order without payment intent. please try again.'
22
+ return order_not_processed(declined_url: deluxe_delayed_params[:declined_url])
23
+ end
24
+
25
+ # Decode the base64 encoded JSON object into a Hash
26
+ payment_intent = api.decode_payment_intent_payload(payment_intent_payload)
27
+ card_info = api.card_info(payment_intent)
28
+
29
+ valid = payment_intent['status'] == 'success'
30
+
31
+ if valid == false
32
+ return order_declined(payment: card_info, provider: 'deluxe_delayed', card: card_info['card'], declined_url: deluxe_delayed_params[:declined_url])
33
+ end
34
+
35
+ flash[:success] = EffectiveOrders.deluxe_delayed[:success]
36
+
37
+ order_delayed(payment: card_info, payment_intent: payment_intent_payload, provider: 'deluxe_delayed', card: card_info['card'], deferred_url: deluxe_delayed_params[:deferred_url])
38
+ end
39
+
40
+ private
41
+
42
+ def deluxe_delayed_params
43
+ params.require(:deluxe_delayed).permit(:payment_intent, :deferred_url, :declined_url)
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,50 @@
1
+ module Effective
2
+ module Providers
3
+ module DeluxeDelayedPurchase
4
+ extend ActiveSupport::Concern
5
+
6
+ # Admin action
7
+ def deluxe_delayed_purchase
8
+ raise('deluxe_delayed_purchase provider is not available') unless EffectiveOrders.deluxe_delayed?
9
+
10
+ @order ||= Order.deep.find(params[:id])
11
+
12
+ EffectiveResources.authorize!(self, :update, @order)
13
+ EffectiveResources.authorize!(self, :admin, :effective_orders)
14
+
15
+ raise('expected a delayed? and deferred? order') unless @order.delayed? && @order.deferred?
16
+
17
+ ## Purchase Order right now
18
+ api = Effective::DeluxeApi.new
19
+
20
+ purchased = api.purchase!(@order, @order.delayed_payment_intent)
21
+ payment = api.payment
22
+
23
+ if purchased == false
24
+ flash[:danger] = "Payment was unsuccessful. The credit card payment failed with message: #{Array(payment['responseMessage']).to_sentence.presence || 'none'}. Please try again."
25
+ return order_declined(payment: payment, provider: 'deluxe_delayed', card: payment['card'], declined_url: deluxe_delayed_purchase_params[:declined_url])
26
+ end
27
+
28
+ @order.assign_attributes(deluxe_delayed_purchase_params.except(:purchased_url, :declined_url, :id))
29
+
30
+ order_purchased(
31
+ payment: payment,
32
+ provider: 'deluxe_delayed',
33
+ card: payment['card'],
34
+ email: @order.send_mark_as_paid_email_to_buyer?,
35
+ skip_buyer_validations: true,
36
+ purchased_url: effective_orders.admin_order_path(@order),
37
+ current_user: nil # Admin action, we don't want to assign current_user to the order
38
+ )
39
+ end
40
+
41
+ def deluxe_delayed_purchase_params
42
+ params.require(:effective_order).permit(
43
+ :id, :note_to_buyer, :note_internal, :send_mark_as_paid_email_to_buyer,
44
+ :purchased_url, :declined_url
45
+ )
46
+ end
47
+
48
+ end
49
+ end
50
+ end
@@ -100,6 +100,14 @@ module Admin
100
100
 
101
101
  col :total, as: :price
102
102
 
103
+ if EffectiveOrders.delayed?
104
+ col :delayed_payment, visible: false
105
+ col :delayed_payment_date
106
+ col :delayed_payment_intent, visible: false
107
+ col :delayed_payment_purchase_ran_at, visible: false
108
+ col :delayed_payment_purchase_result, visible: false
109
+ end
110
+
103
111
  if EffectiveOrders.collect_note
104
112
  col :note, visible: false
105
113
  end
@@ -60,6 +60,12 @@ class EffectiveOrdersDatatable < Effective::Datatable
60
60
  col :surcharge, as: :price, visible: false
61
61
  col(:surcharge_percent, visible: false) { |order| rate_to_percentage(order.surcharge_percent) }
62
62
 
63
+ if EffectiveOrders.delayed?
64
+ col :delayed_payment
65
+ col :delayed_payment_date
66
+ col :delayed_payment_intent
67
+ end
68
+
63
69
  col :total, as: :price
64
70
 
65
71
  if EffectiveOrders.collect_note
@@ -0,0 +1,15 @@
1
+ module EffectiveDeluxeDelayedHelper
2
+
3
+ # https://developer.deluxe.com/s/article-hosted-payment-form
4
+ def deluxe_delayed_hosted_payment_form_options(order)
5
+ {
6
+ xtoken: EffectiveOrders.deluxe.fetch(:access_token),
7
+ containerId: "deluxeDelayedCheckout",
8
+ xcssid: "deluxeDelayedCheckoutCss",
9
+ xrtype: "Generate Token",
10
+ xpm: "1", # 0 = CC & ACH, 1 = CC, 2 = ACH
11
+ xautoprompt: false,
12
+ xbtntext: order_checkout_label(:deluxe_delayed)
13
+ }
14
+ end
15
+ end
@@ -44,6 +44,8 @@ module EffectiveOrdersHelper
44
44
  'Pay by Cheque'
45
45
  when :deluxe
46
46
  'Pay Now'
47
+ when :deluxe_delayed
48
+ 'Save now and charge me later'
47
49
  when :etransfer
48
50
  'Pay by E-transfer'
49
51
  when :free
@@ -13,6 +13,8 @@ module Effective
13
13
  attr_accessor :access_token
14
14
  attr_accessor :currency
15
15
 
16
+ attr_accessor :purchase_response
17
+
16
18
  def initialize(environment: nil, client_id: nil, client_secret: nil, access_token: nil, currency: nil)
17
19
  self.environment = environment || EffectiveOrders.deluxe.fetch(:environment)
18
20
  self.client_id = client_id || EffectiveOrders.deluxe.fetch(:client_id)
@@ -21,11 +23,58 @@ module Effective
21
23
  self.currency = currency || EffectiveOrders.deluxe.fetch(:currency)
22
24
  end
23
25
 
26
+ def payment
27
+ raise('expected purchase response to be present') unless purchase_response.kind_of?(Hash)
28
+ purchase_response
29
+ end
30
+
31
+ # This calls Authorize Payment and Complete Payment
32
+ # Returns true if all good.
33
+ # Returns false if there was an error.
34
+ # Always sets the @purchase_response which is api.payment
35
+ def purchase!(order, payment_intent)
36
+ raise('expected a deluxe_delayed payment provider') unless order.payment_provider == 'deluxe_delayed'
37
+
38
+ payment_intent = decode_payment_intent_payload(payment_intent) if payment_intent.kind_of?(String)
39
+ raise('expected payment_intent to be a Hash') unless payment_intent.kind_of?(Hash)
40
+ raise('expected a token payment') unless payment_intent['type'] == 'Token'
41
+
42
+ # Start a purchase. Which is an Authorization and a Completion
43
+ self.purchase_response = nil
44
+
45
+ # Process Authorization
46
+ authorization = authorize_payment(order, payment_intent)
47
+ self.purchase_response = authorization
48
+
49
+ valid = [0].include?(authorization['responseCode'])
50
+ return false unless valid
51
+
52
+ ## Complete Payment
53
+ payment = complete_payment(order, authorization)
54
+ self.purchase_response = payment
55
+
56
+ valid = [0].include?(payment['responseCode'])
57
+ return false unless valid
58
+
59
+ # Valid purchase. This is authorized and completed.
60
+ true
61
+ end
62
+
24
63
  # Health Check
25
64
  def health_check
26
65
  get('/')
27
66
  end
28
67
 
68
+ def healthy?
69
+ response = health_check()
70
+
71
+ return false unless response.kind_of?(Hash)
72
+ return false unless response['timestamp'].to_s.start_with?(Time.zone.now.strftime('%Y-%m-%d'))
73
+ return false unless response['environment'].present?
74
+
75
+ true
76
+ end
77
+
29
78
  # Authorize Payment
30
79
  def authorize_payment(order, payment_intent)
31
80
  response = post('/payments/authorize', params: authorize_payment_params(order, payment_intent))
@@ -191,6 +240,18 @@ module Effective
191
240
  { 'active_card' => active_card, 'card' => card, 'expDate' => date, 'cvv' => cvv }.compact
192
241
  end
193
242
 
243
+ # Decode the base64 encoded JSON object into a Hash
244
+ def decode_payment_intent_payload(payload)
245
+ raise('expected a string') unless payload.kind_of?(String)
246
+
247
+ payment_intent = (JSON.parse(Base64.decode64(payload)) rescue nil)
248
+
249
+ raise('expected payment_intent to be a Hash') unless payment_intent.kind_of?(Hash)
250
+ raise('expected a token payment') unless payment_intent['type'] == 'Token'
251
+
252
+ payment_intent
253
+ end
254
+
194
255
  private
195
256
 
196
257
  def headers
@@ -15,7 +15,7 @@ module Effective
15
15
  acts_as_statused(
16
16
  :pending, # New orders are created in a pending state
17
17
  :confirmed, # Once the order has passed checkout step 1
18
- :deferred, # Deferred providers. cheque, etransfer or phone was selected.
18
+ :deferred, # Deferred providers. cheque, etransfer, phone or deluxe_delayed was selected.
19
19
  :purchased, # Purchased by provider
20
20
  :declined, # Declined by provider
21
21
  :voided, # Voided by admin
@@ -94,6 +94,21 @@ module Effective
94
94
 
95
95
  total :integer # Subtotal + Tax + Surcharge + Surcharge Tax
96
96
 
97
+ # For use with the Deluxe Delayed Payment feature
98
+
99
+ # When an order is created. These two attributes can be set to create a delayed? order
100
+ delayed_payment :boolean
101
+ delayed_payment_date :date
102
+
103
+ # When the order goes to checkout we require the delayed_payment_intent
104
+ # This stores the user's card information in a secure way
105
+ # This is required for the order to become deferred?
106
+ delayed_payment_intent :text
107
+
108
+ # Set by the rake task that runs 1/day and processes any delayed orders before or on that day
109
+ delayed_payment_purchase_ran_at :datetime
110
+ delayed_payment_purchase_result :text
111
+
97
112
  timestamps
98
113
  end
99
114
 
@@ -135,6 +150,16 @@ module Effective
135
150
  scope :refunds, -> { purchased.where('total < ?', 0) }
136
151
  scope :pending_refunds, -> { not_purchased.where('total < ?', 0) }
137
152
 
153
+ scope :delayed, -> { where(delayed_payment: true).where.not(delayed_payment_date: nil) }
154
+ scope :delayed_payment_date_past, -> { delayed.where(arel_table[:delayed_payment_date].lteq(Time.zone.today)) }
155
+ scope :delayed_payment_date_upcoming, -> { delayed.where(arel_table[:delayed_payment_date].gt(Time.zone.today)) }
156
+
157
+ # Used by the rake effective_orders:purchase_delayed_orders task
158
+ scope :delayed_ready_to_purchase, -> {
159
+ delayed.deferred.delayed_payment_date_past.where(delayed_payment_purchase_ran_at: nil)
160
+ }
161
+
162
+
138
163
  # effective_reports
139
164
  def reportable_scopes
140
165
  { purchased: nil, not_purchased: nil, deferred: nil, refunds: nil, pending_refunds: nil }
@@ -165,6 +190,11 @@ module Effective
165
190
 
166
191
  validates :order_items, presence: { message: 'No items are present. Please add additional items.' }
167
192
 
193
+ # Delayed Payment Validations
194
+ validates :delayed_payment_date, presence: true, if: -> { delayed_payment? }
195
+ validates :delayed_payment_date, absence: true, unless: -> { delayed_payment? }
196
+ validates :delayed_payment_intent, presence: { message: 'please provide your card information' }, if: -> { delayed? && deferred? }
197
+
168
198
  validate do
169
199
  if EffectiveOrders.organization_enabled?
170
200
  errors.add(:base, "must have a User or #{EffectiveOrders.organization_class_name || 'Organization'}") if user_id.blank? && organization_id.blank?
@@ -392,46 +422,12 @@ module Effective
392
422
  purchased? ? 'Total Paid' : 'Total Due'
393
423
  end
394
424
 
395
- # Visa - 1234
396
425
  def payment_method
397
- return nil unless purchased?
398
-
399
- provider = payment_provider if ['cheque', 'etransfer', 'phone', 'credit card'].include?(payment_provider)
400
-
401
- # Normalize payment card
402
- card = case payment_card.to_s.downcase.gsub(' ', '').strip
403
- when '' then nil
404
- when 'v', 'visa' then 'Visa'
405
- when 'm', 'mc', 'master', 'mastercard' then 'MasterCard'
406
- when 'a', 'ax', 'american', 'americanexpress' then 'American Express'
407
- when 'd', 'discover' then 'Discover'
408
- else payment_card.to_s
409
- end
410
-
411
- # Try again
412
- if card == 'none' && payment['card_type'].present?
413
- card = case payment['card_type'].to_s.downcase.gsub(' ', '').strip
414
- when '' then nil
415
- when 'v', 'visa' then 'Visa'
416
- when 'm', 'mc', 'master', 'mastercard' then 'MasterCard'
417
- when 'a', 'ax', 'american', 'americanexpress' then 'American Express'
418
- when 'd', 'discover' then 'Discover'
419
- else payment_card.to_s
420
- end
421
- end
422
-
423
- last4 = if payment[:active_card] && payment[:active_card].include?('**** **** ****')
424
- payment[:active_card][15,4]
425
- end
426
-
427
- last4 ||= if payment['active_card'] && payment['active_card'].include?('**** **** ****')
428
- payment['active_card'][15,4]
429
- end
430
-
431
- # stripe, moneris, moneris_checkout
432
- last4 ||= (payment['f4l4'] || payment['first6last4']).to_s.last(4)
426
+ payment_method_value if purchased?
427
+ end
433
428
 
434
- [provider.presence, card.presence, last4.presence].compact.join(' - ')
429
+ def delayed_payment_method
430
+ payment_method_value if delayed?
435
431
  end
436
432
 
437
433
  def duplicate
@@ -538,6 +534,39 @@ module Effective
538
534
  def refund?
539
535
  total.to_i < 0
540
536
  end
537
+
538
+ # A new order is created.
539
+ # If the delayed_payment and delayed_payment date are set, it's a delayed order
540
+ # A delayed order is one in which we have to capture a payment intent for the amount of the order.
541
+ # Once it's delayed and deferred we can purchase it at anytime.
542
+ def delayed?
543
+ delayed_payment? && delayed_payment_date.present?
544
+ end
545
+
546
+ def delayed_ready_to_purchase?
547
+ return false unless delayed?
548
+ return false unless deferred?
549
+ return false unless delayed_payment_intent.present?
550
+ return false if delayed_payment_date_future?
551
+ return false if delayed_payment_purchase_ran_at.present? # We ran before and probably failed
552
+
553
+ true
554
+ end
555
+
556
+ def delayed_payment_date_future?
557
+ return false unless delayed?
558
+ delayed_payment_date > Time.zone.now.to_date
559
+ end
560
+
561
+ def delayed_payment_date_today?
562
+ return false unless delayed?
563
+ delayed_payment_date == Time.zone.now.to_date
564
+ end
565
+
566
+ def delayed_payment_date_past?
567
+ return false unless delayed?
568
+ delayed_payment_date < Time.zone.now.to_date
569
+ end
541
570
 
542
571
  def pending_refund?
543
572
  return false if EffectiveOrders.buyer_purchases_refund?
@@ -696,6 +725,23 @@ module Effective
696
725
  sync_quickbooks!(skip: true)
697
726
  end
698
727
 
728
+ # This was submitted via the deluxe_delayed provider checkout
729
+ # This is a special case of a deferred provider. We require the payment_intent and payment info
730
+ def delay!(payment:, payment_intent:, provider:, card:, email: false, validate: true)
731
+ raise('expected payment intent to be a String') unless payment_intent.kind_of?(String)
732
+ raise('expected a delayed payment provider') unless EffectiveOrders.delayed_providers.include?(provider)
733
+ raise('expected a delayed payment order with a delayed_payment_date') unless delayed_payment? && delayed_payment_date.present?
734
+
735
+ assign_attributes(
736
+ delayed_payment_intent: payment_intent,
737
+
738
+ payment: payment_to_h(payment),
739
+ payment_card: (card.presence || 'none')
740
+ )
741
+
742
+ defer!(provider: provider, email: email, validate: validate)
743
+ end
744
+
699
745
  def defer!(provider: 'none', email: true, validate: true)
700
746
  raise('order already purchased') if purchased?
701
747
 
@@ -725,9 +771,7 @@ module Effective
725
771
  end
726
772
  rescue ActiveRecord::RecordInvalid => e
727
773
  self.status = status_was
728
-
729
774
  error = e.message
730
- raise ::ActiveRecord::Rollback
731
775
  end
732
776
 
733
777
  raise "Failed to defer order: #{error || errors.full_messages.to_sentence}" unless error.nil?
@@ -766,9 +810,7 @@ module Effective
766
810
  run_purchasable_callbacks(:after_decline)
767
811
  rescue ActiveRecord::RecordInvalid => e
768
812
  self.status = status_was
769
-
770
813
  error = e.message
771
- raise ::ActiveRecord::Rollback
772
814
  end
773
815
  end
774
816
 
@@ -884,6 +926,47 @@ module Effective
884
926
  subtotal + tax
885
927
  end
886
928
 
929
+ # Visa - 1234
930
+ def payment_method_value
931
+ provider = payment_provider if ['cheque', 'etransfer', 'phone', 'credit card'].include?(payment_provider)
932
+ provider = 'credit card' if ['deluxe_delayed'].include?(payment_provider)
933
+
934
+ # Normalize payment card
935
+ card = case payment_card.to_s.downcase.gsub(' ', '').strip
936
+ when '' then nil
937
+ when 'v', 'visa' then 'Visa'
938
+ when 'm', 'mc', 'master', 'mastercard' then 'MasterCard'
939
+ when 'a', 'ax', 'american', 'americanexpress' then 'American Express'
940
+ when 'd', 'discover' then 'Discover'
941
+ else payment_card.to_s
942
+ end
943
+
944
+ # Try again
945
+ if card == 'none' && payment['card_type'].present?
946
+ card = case payment['card_type'].to_s.downcase.gsub(' ', '').strip
947
+ when '' then nil
948
+ when 'v', 'visa' then 'Visa'
949
+ when 'm', 'mc', 'master', 'mastercard' then 'MasterCard'
950
+ when 'a', 'ax', 'american', 'americanexpress' then 'American Express'
951
+ when 'd', 'discover' then 'Discover'
952
+ else payment_card.to_s
953
+ end
954
+ end
955
+
956
+ last4 = if payment[:active_card] && payment[:active_card].include?('**** **** ****')
957
+ payment[:active_card][15,4]
958
+ end
959
+
960
+ last4 ||= if payment['active_card'] && payment['active_card'].include?('**** **** ****')
961
+ payment['active_card'][15,4]
962
+ end
963
+
964
+ # stripe, moneris, moneris_checkout
965
+ last4 ||= (payment['f4l4'] || payment['first6last4']).to_s.last(4)
966
+
967
+ [provider.presence, card.presence, last4.presence].compact.join(' - ')
968
+ end
969
+
887
970
  private
888
971
 
889
972
  def present_order_items
@@ -14,6 +14,9 @@
14
14
  - elsif EffectiveOrders.refund? && order.refund?
15
15
  = render partial: '/effective/orders/refund/form', locals: provider_locals
16
16
 
17
+ - elsif EffectiveOrders.delayed? && order.delayed?
18
+ = render partial: '/effective/orders/delayed/form', locals: provider_locals
19
+
17
20
  - else
18
21
  - if EffectiveOrders.pretend?
19
22
  = render partial: '/effective/orders/pretend/form', locals: provider_locals
@@ -38,6 +41,10 @@
38
41
  = render partial: '/effective/orders/deferred/form', locals: provider_locals
39
42
 
40
43
  - if EffectiveResources.authorized?(controller, :admin, :effective_orders) && order.user != current_user
44
+ - if EffectiveOrders.delayed? && order.delayed? && order.deferred?
45
+ .effective-order-admin-purchase-actions
46
+ = render partial: '/effective/orders/delayed/form_purchase', locals: provider_locals
47
+
41
48
  - if EffectiveOrders.mark_as_paid?
42
49
  .effective-order-admin-purchase-actions
43
50
  = render partial: '/effective/orders/mark_as_paid/form', locals: provider_locals
@@ -1,6 +1,6 @@
1
1
  = dropdown(variation: :dropleft) do
2
2
  - if EffectiveResources.authorized?(controller, :checkout, order)
3
- = dropdown_link_to 'Checkout', effective_orders.order_path(order)
3
+ = dropdown_link_to 'Checkout', effective_orders.order_path(order), 'data-turbolinks': false
4
4
  - else
5
5
  = dropdown_link_to 'View', effective_orders.order_path(order)
6
6
 
@@ -6,4 +6,8 @@
6
6
  %th Payment
7
7
  %tbody
8
8
  %tr
9
- %td Waiting for payment by #{order.payment_provider}
9
+ %td
10
+ - if order.delayed?
11
+ Your #{order.delayed_payment_method} will be charged on #{order.delayed_payment_date.strftime('%F')} for the full amount
12
+ - else
13
+ Waiting for payment by #{order.payment_provider}
@@ -0,0 +1,25 @@
1
+ .card
2
+ .card-body
3
+ %h2 Save Card Info
4
+
5
+ %p
6
+ = succeed('.') do
7
+ - distance = distance_of_time_in_words(Time.zone.now, order.delayed_payment_date)
8
+
9
+ The payment date for this order
10
+
11
+ - if order.delayed_payment_date_future?
12
+ is in #{distance} from now on #{order.delayed_payment_date.strftime('%F')}
13
+ - elsif order.delayed_payment_date_today?
14
+ was today
15
+ - else
16
+ was #{distance} ago on #{order.delayed_payment_date.strftime('%F')}
17
+
18
+ %p
19
+ Instead of charging your card right away, the following action will securely save a token
20
+ representing your card information. The full amount will be charged on the payment date.
21
+
22
+ - provider_locals = { order: order, deferred_url: deferred_url, declined_url: declined_url }
23
+
24
+ - EffectiveOrders.delayed_providers.each do |provider|
25
+ = render partial: "/effective/orders/#{provider}/form", locals: provider_locals
@@ -0,0 +1,30 @@
1
+ .card
2
+ .card-body
3
+ %h2 Admin: Purchase Delayed Order
4
+
5
+ - raise('unexpected purchased order') if order.purchased?
6
+ - raise('expected a deferred delayed order') unless order.delayed? && order.deferred?
7
+ - raise('expecting a payment intent') unless order.delayed_payment_intent.present?
8
+ - raise('expecting a payment method') unless order.delayed_payment_method.present?
9
+
10
+ %p
11
+ = succeed('.') do
12
+ - distance = distance_of_time_in_words(Time.zone.now, order.delayed_payment_date)
13
+
14
+ The payment date for this order
15
+
16
+ - if order.delayed_payment_date_future?
17
+ is in #{distance} from now on #{order.delayed_payment_date.strftime('%F')}
18
+ - elsif order.delayed_payment_date_today?
19
+ was today
20
+ - else
21
+ was #{distance} ago on #{order.delayed_payment_date.strftime('%F')}
22
+
23
+ %p The #{order.delayed_payment_method} on file will be charged automatically on the payment date.
24
+
25
+ %p You can also charge it right now.
26
+
27
+ - provider_locals = { order: order, purchased_url: purchased_url, declined_url: declined_url }
28
+
29
+ - EffectiveOrders.delayed_providers.each do |provider|
30
+ = render partial: "/effective/orders/#{provider}/form_purchase", locals: provider_locals
@@ -3,11 +3,8 @@
3
3
  .card
4
4
  .card-body
5
5
  %h2 Checkout
6
- %p
7
- %em This checkout is powered by #{link_to('Deluxe', 'https://www.deluxe.com/', target: '_blank', class: 'btn-link')}
8
6
 
9
- .my-4.text-center
10
- = image_tag('effective_orders/deluxe.png', alt: 'Deluxe.com Logo', width: 200)
7
+ .mt-4
11
8
 
12
9
  = effective_form_with(scope: :deluxe, url: effective_orders.deluxe_order_path(order), data: { 'deluxe-checkout': deluxe.to_json }) do |f|
13
10
  = f.hidden_field :purchased_url, value: purchased_url
@@ -0,0 +1,12 @@
1
+ :css
2
+ #dppPaymentContainer {
3
+ .form-control { }
4
+ .form-label { }
5
+
6
+ button {
7
+ color: #fff;
8
+ background-color: #0d6efd;
9
+ border_color: #0d6efd;
10
+ padding: 0.375rem 0.75rem;
11
+ }
12
+ }
@@ -0,0 +1,9 @@
1
+ .effective-deluxe-delayed-checkout
2
+ #deluxe-delayed-checkout-loading.text-center Loading...
3
+ #deluxe-delayed-checkout-errors.text-danger
4
+
5
+ %style#deluxeDelayedCheckoutCss
6
+ -# Pass in custom CSS to the Deluxe Delayed hosted payment form iframe
7
+ = render('effective/orders/deluxe_delayed/css')
8
+
9
+ #deluxeDelayedCheckout
@@ -0,0 +1,10 @@
1
+ - deluxe_delayed = deluxe_delayed_hosted_payment_form_options(order)
2
+
3
+ = effective_form_with(scope: :deluxe_delayed, url: effective_orders.deluxe_delayed_order_path(order), data: { 'deluxe-delayed-checkout': deluxe_delayed.to_json }) do |f|
4
+ = f.hidden_field :declined_url, value: declined_url
5
+ = f.hidden_field :deferred_url, value: deferred_url
6
+
7
+ -# This is set by the deluxe.js javascript on Submit
8
+ = f.hidden_field :payment_intent, required: true
9
+
10
+ = render('effective/orders/deluxe_delayed/element')
@@ -0,0 +1,18 @@
1
+ = effective_form_with(model: order, url: effective_orders.deluxe_delayed_purchase_order_path(order), method: :post) do |f|
2
+ = f.hidden_field :id
3
+
4
+ = f.hidden_field :purchased_url, value: purchased_url
5
+ = f.hidden_field :declined_url, value: declined_url
6
+
7
+ = f.check_box :send_mark_as_paid_email_to_buyer,
8
+ label: 'Yes, send a receipt email to the buyer.',
9
+ input_html: { checked: (f.object.send_mark_as_paid_email_to_buyer.nil? ? EffectiveOrders.send_order_receipts_when_mark_as_paid : f.object.send_mark_as_paid_email_to_buyer?) }
10
+
11
+ .row
12
+ .col
13
+ = f.text_area :note_to_buyer, hint: 'This message will be displayed to the buyer on the receipt.'
14
+ .col
15
+ = f.text_area :note_internal, hint: 'For or internal admin use only. This note will never be displayed to the buyer.'
16
+
17
+ = f.submit(center: true) do
18
+ = f.save 'Purchase Order from Saved Card'
@@ -4,6 +4,8 @@
4
4
  Admin:
5
5
  = order.refund? ? 'Complete Refund' : 'Mark as Paid'
6
6
 
7
+ - raise('unexpected purchased order') if order.purchased?
8
+
7
9
  = effective_form_with(model: order, url: effective_orders.mark_as_paid_order_path(order), method: :post) do |f|
8
10
  .row
9
11
  .col-6
@@ -133,6 +133,15 @@ EffectiveOrders.setup do |config|
133
133
  # currency: 'CAD'
134
134
  # }
135
135
 
136
+ # Deluxe Delayed
137
+ # This is a deferred payment
138
+ config.deluxe_delayed = false
139
+
140
+ # config.deluxe_delayed = {
141
+ # confirm: 'Save card info for later payment',
142
+ # success: 'Thank you! You have indicated that this order will be purchased by credit card at a later date. We will save your card information securely and process the payment when the order is ready'
143
+ # }
144
+
136
145
  # E-transfer
137
146
  # This is an deferred payment
138
147
  config.etransfer = false
data/config/routes.rb CHANGED
@@ -8,7 +8,10 @@ EffectiveOrders::Engine.routes.draw do
8
8
  post :send_buyer_receipt
9
9
 
10
10
  post :cheque
11
- post :deluxe
11
+ post :deluxe # 1-off payment and purchase
12
+ post :deluxe_delayed # 1-off payment_intent saving
13
+ post :deluxe_delayed_purchase # Admin action to purchase a delayed order
14
+
12
15
  post :etransfer
13
16
  post :free
14
17
  post :mark_as_paid
@@ -40,6 +40,12 @@ class CreateEffectiveOrders < ActiveRecord::Migration[6.0]
40
40
  t.integer :surcharge_tax
41
41
  t.integer :total
42
42
 
43
+ t.boolean :delayed_payment, default: false
44
+ t.date :delayed_payment_date
45
+ t.text :delayed_payment_intent
46
+ t.datetime :delayed_payment_purchase_ran_at
47
+ t.text :delayed_payment_purchase_result
48
+
43
49
  t.timestamps
44
50
  end
45
51
 
@@ -1,3 +1,3 @@
1
1
  module EffectiveOrders
2
- VERSION = '6.11.0'.freeze
2
+ VERSION = '6.12.0'.freeze
3
3
  end
@@ -42,7 +42,7 @@ module EffectiveOrders
42
42
  :free_enabled, :mark_as_paid_enabled, :pretend_enabled, :pretend_message, :buyer_purchases_refund,
43
43
 
44
44
  # Payment processors. false or Hash
45
- :cheque, :deluxe, :etransfer, :moneris, :moneris_checkout, :paypal, :phone, :refund, :stripe, :subscriptions, :trial
45
+ :cheque, :deluxe, :deluxe_delayed, :etransfer, :moneris, :moneris_checkout, :paypal, :phone, :refund, :stripe, :subscriptions, :trial
46
46
  ]
47
47
  end
48
48
 
@@ -85,10 +85,18 @@ module EffectiveOrders
85
85
  deluxe.kind_of?(Hash)
86
86
  end
87
87
 
88
+ def self.deluxe_delayed?
89
+ deluxe_delayed.kind_of?(Hash)
90
+ end
91
+
88
92
  def self.deferred?
89
93
  deferred_providers.present?
90
94
  end
91
95
 
96
+ def self.delayed?
97
+ delayed_providers.present?
98
+ end
99
+
92
100
  def self.mark_as_paid?
93
101
  mark_as_paid_enabled == true
94
102
  end
@@ -178,7 +186,11 @@ module EffectiveOrders
178
186
  end
179
187
 
180
188
  def self.deferred_providers
181
- [('cheque' if cheque?), ('etransfer' if etransfer?), ('phone' if phone?)].compact
189
+ [('cheque' if cheque?), ('deluxe_delayed' if deluxe_delayed?), ('etransfer' if etransfer?), ('phone' if phone?)].compact
190
+ end
191
+
192
+ def self.delayed_providers
193
+ [('deluxe_delayed' if deluxe_delayed?)].compact
182
194
  end
183
195
 
184
196
  def self.credit_card_payment_providers
@@ -66,4 +66,10 @@ namespace :effective_orders do
66
66
  end
67
67
  end
68
68
 
69
+ # rake effective_orders:purchase_delayed_orders
70
+ desc 'Purchases delayed orders on their delayed_payment_date for effective orders'
71
+ task purchase_delayed_orders: :environment do
72
+ puts 'Todo'
73
+ end
74
+
69
75
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: effective_orders
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.11.0
4
+ version: 6.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Code and Effect
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-05-14 00:00:00.000000000 Z
11
+ date: 2024-05-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -189,12 +189,12 @@ files:
189
189
  - MIT-LICENSE
190
190
  - README.md
191
191
  - app/assets/config/effective_orders_manifest.js
192
- - app/assets/images/effective_orders/deluxe.png
193
192
  - app/assets/images/effective_orders/logo.png
194
193
  - app/assets/images/effective_orders/stripe.png
195
194
  - app/assets/javascripts/effective_orders.js
196
195
  - app/assets/javascripts/effective_orders/customers.js.coffee
197
196
  - app/assets/javascripts/effective_orders/providers/deluxe.js
197
+ - app/assets/javascripts/effective_orders/providers/deluxe_delayed.js
198
198
  - app/assets/javascripts/effective_orders/providers/moneris_checkout.js.coffee
199
199
  - app/assets/javascripts/effective_orders/providers/stripe.js.coffee
200
200
  - app/assets/javascripts/effective_orders/subscriptions.js.coffee
@@ -211,6 +211,8 @@ files:
211
211
  - app/controllers/effective/orders_controller.rb
212
212
  - app/controllers/effective/providers/cheque.rb
213
213
  - app/controllers/effective/providers/deluxe.rb
214
+ - app/controllers/effective/providers/deluxe_delayed.rb
215
+ - app/controllers/effective/providers/deluxe_delayed_purchase.rb
214
216
  - app/controllers/effective/providers/etransfer.rb
215
217
  - app/controllers/effective/providers/free.rb
216
218
  - app/controllers/effective/providers/mark_as_paid.rb
@@ -231,6 +233,7 @@ files:
231
233
  - app/datatables/admin/report_transactions_grouped_by_qb_name_datatable.rb
232
234
  - app/datatables/effective_orders_datatable.rb
233
235
  - app/helpers/effective_carts_helper.rb
236
+ - app/helpers/effective_deluxe_delayed_helper.rb
234
237
  - app/helpers/effective_deluxe_helper.rb
235
238
  - app/helpers/effective_moneris_checkout_helper.rb
236
239
  - app/helpers/effective_orders_helper.rb
@@ -296,9 +299,15 @@ files:
296
299
  - app/views/effective/orders/declined.html.haml
297
300
  - app/views/effective/orders/deferred.html.haml
298
301
  - app/views/effective/orders/deferred/_form.html.haml
302
+ - app/views/effective/orders/delayed/_form.html.haml
303
+ - app/views/effective/orders/delayed/_form_purchase.html.haml
299
304
  - app/views/effective/orders/deluxe/_css.html.haml
300
305
  - app/views/effective/orders/deluxe/_element.html.haml
301
306
  - app/views/effective/orders/deluxe/_form.html.haml
307
+ - app/views/effective/orders/deluxe_delayed/_css.html.haml
308
+ - app/views/effective/orders/deluxe_delayed/_element.html.haml
309
+ - app/views/effective/orders/deluxe_delayed/_form.html.haml
310
+ - app/views/effective/orders/deluxe_delayed/_form_purchase.html.haml
302
311
  - app/views/effective/orders/edit.html.haml
303
312
  - app/views/effective/orders/etransfer/_form.html.haml
304
313
  - app/views/effective/orders/free/_form.html.haml