effective_orders 6.11.1 → 6.12.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) 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 +125 -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_delayed/_css.html.haml +12 -0
  21. data/app/views/effective/orders/deluxe_delayed/_element.html.haml +9 -0
  22. data/app/views/effective/orders/deluxe_delayed/_form.html.haml +10 -0
  23. data/app/views/effective/orders/deluxe_delayed/_form_purchase.html.haml +18 -0
  24. data/app/views/effective/orders/mark_as_paid/_form.html.haml +2 -0
  25. data/config/effective_orders.rb +9 -0
  26. data/config/routes.rb +4 -1
  27. data/db/migrate/101_create_effective_orders.rb +6 -0
  28. data/lib/effective_orders/version.rb +1 -1
  29. data/lib/effective_orders.rb +14 -2
  30. data/lib/tasks/effective_orders_tasks.rake +6 -0
  31. metadata +12 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8ac80cab29f40a538adba24d649e4d31ca08d15af77b8bd15e906eec5a4afcc4
4
- data.tar.gz: 46791fff44cb8f78ead4b829fe79e59fd8202862a05a89448e5844a2fc1e5417
3
+ metadata.gz: a5a55ea6c14b02fce58218a825c9f936516c0d46a1246b7190a503b97070d394
4
+ data.tar.gz: 97c6253dfe17a99e7113755e935432b7953c89714ef1a77dff48f6a014204a9e
5
5
  SHA512:
6
- metadata.gz: 5bda2f8002c375797ee23647dd585529082a34bb274ca623b18ca2f564a4fba3768ece770ee1182164396886e5e606736b652d678c8dd79f59af8ff22799403a
7
- data.tar.gz: 0a4ab62604bee87a844acb881d69e455b61e14c1b336f9318b0113fb166f1f233f230d9b48a3bfc4e749e5cdf4d1b1ac120c6bd26a5f19a2ad9fe2a648c8f16e
6
+ metadata.gz: 1d4f1885182a55a73e39665ab477f31bb1fc6c801fb04f13f0311fd870ad87edb84f27d8bf5ac0abb456f61fdd236961df650da27e157a7b532629e67e7c567a
7
+ data.tar.gz: 4c74eaa371c3e9db830ce252cd611068ab727640a836f6e6dac1c7abc05439b7a9beeb0f80bf2374367afbc6a8b37d8e8c1188057f19c2e062b1d0c73436c214
@@ -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 payment provider') unless ['deluxe', 'deluxe_delayed'].include?(order.payment_provider)
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,15 @@ 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
+
138
162
  # effective_reports
139
163
  def reportable_scopes
140
164
  { purchased: nil, not_purchased: nil, deferred: nil, refunds: nil, pending_refunds: nil }
@@ -165,6 +189,11 @@ module Effective
165
189
 
166
190
  validates :order_items, presence: { message: 'No items are present. Please add additional items.' }
167
191
 
192
+ # Delayed Payment Validations
193
+ validates :delayed_payment_date, presence: true, if: -> { delayed_payment? }
194
+ validates :delayed_payment_date, absence: true, unless: -> { delayed_payment? }
195
+ validates :delayed_payment_intent, presence: { message: 'please provide your card information' }, if: -> { delayed? && deferred? }
196
+
168
197
  validate do
169
198
  if EffectiveOrders.organization_enabled?
170
199
  errors.add(:base, "must have a User or #{EffectiveOrders.organization_class_name || 'Organization'}") if user_id.blank? && organization_id.blank?
@@ -392,46 +421,12 @@ module Effective
392
421
  purchased? ? 'Total Paid' : 'Total Due'
393
422
  end
394
423
 
395
- # Visa - 1234
396
424
  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)
425
+ payment_method_value if purchased?
426
+ end
433
427
 
434
- [provider.presence, card.presence, last4.presence].compact.join(' - ')
428
+ def delayed_payment_method
429
+ payment_method_value if delayed?
435
430
  end
436
431
 
437
432
  def duplicate
@@ -538,6 +533,39 @@ module Effective
538
533
  def refund?
539
534
  total.to_i < 0
540
535
  end
536
+
537
+ # A new order is created.
538
+ # If the delayed_payment and delayed_payment date are set, it's a delayed order
539
+ # A delayed order is one in which we have to capture a payment intent for the amount of the order.
540
+ # Once it's delayed and deferred we can purchase it at anytime.
541
+ def delayed?
542
+ delayed_payment? && delayed_payment_date.present?
543
+ end
544
+
545
+ def delayed_ready_to_purchase?
546
+ return false unless delayed?
547
+ return false unless deferred?
548
+ return false unless delayed_payment_intent.present?
549
+ return false if delayed_payment_date_future?
550
+ return false if delayed_payment_purchase_ran_at.present? # We ran before and probably failed
551
+
552
+ true
553
+ end
554
+
555
+ def delayed_payment_date_future?
556
+ return false unless delayed?
557
+ delayed_payment_date > Time.zone.now.to_date
558
+ end
559
+
560
+ def delayed_payment_date_today?
561
+ return false unless delayed?
562
+ delayed_payment_date == Time.zone.now.to_date
563
+ end
564
+
565
+ def delayed_payment_date_past?
566
+ return false unless delayed?
567
+ delayed_payment_date < Time.zone.now.to_date
568
+ end
541
569
 
542
570
  def pending_refund?
543
571
  return false if EffectiveOrders.buyer_purchases_refund?
@@ -696,6 +724,23 @@ module Effective
696
724
  sync_quickbooks!(skip: true)
697
725
  end
698
726
 
727
+ # This was submitted via the deluxe_delayed provider checkout
728
+ # This is a special case of a deferred provider. We require the payment_intent and payment info
729
+ def delay!(payment:, payment_intent:, provider:, card:, email: false, validate: true)
730
+ raise('expected payment intent to be a String') unless payment_intent.kind_of?(String)
731
+ raise('expected a delayed payment provider') unless EffectiveOrders.delayed_providers.include?(provider)
732
+ raise('expected a delayed payment order with a delayed_payment_date') unless delayed_payment? && delayed_payment_date.present?
733
+
734
+ assign_attributes(
735
+ delayed_payment_intent: payment_intent,
736
+
737
+ payment: payment_to_h(payment),
738
+ payment_card: (card.presence || 'none')
739
+ )
740
+
741
+ defer!(provider: provider, email: email, validate: validate)
742
+ end
743
+
699
744
  def defer!(provider: 'none', email: true, validate: true)
700
745
  raise('order already purchased') if purchased?
701
746
 
@@ -725,9 +770,7 @@ module Effective
725
770
  end
726
771
  rescue ActiveRecord::RecordInvalid => e
727
772
  self.status = status_was
728
-
729
773
  error = e.message
730
- raise ::ActiveRecord::Rollback
731
774
  end
732
775
 
733
776
  raise "Failed to defer order: #{error || errors.full_messages.to_sentence}" unless error.nil?
@@ -766,9 +809,7 @@ module Effective
766
809
  run_purchasable_callbacks(:after_decline)
767
810
  rescue ActiveRecord::RecordInvalid => e
768
811
  self.status = status_was
769
-
770
812
  error = e.message
771
- raise ::ActiveRecord::Rollback
772
813
  end
773
814
  end
774
815
 
@@ -884,6 +925,47 @@ module Effective
884
925
  subtotal + tax
885
926
  end
886
927
 
928
+ # Visa - 1234
929
+ def payment_method_value
930
+ provider = payment_provider if ['cheque', 'etransfer', 'phone', 'credit card'].include?(payment_provider)
931
+ provider = 'credit card' if ['deluxe_delayed'].include?(payment_provider)
932
+
933
+ # Normalize payment card
934
+ card = case payment_card.to_s.downcase.gsub(' ', '').strip
935
+ when '' then nil
936
+ when 'v', 'visa' then 'Visa'
937
+ when 'm', 'mc', 'master', 'mastercard' then 'MasterCard'
938
+ when 'a', 'ax', 'american', 'americanexpress' then 'American Express'
939
+ when 'd', 'discover' then 'Discover'
940
+ else payment_card.to_s
941
+ end
942
+
943
+ # Try again
944
+ if card == 'none' && payment['card_type'].present?
945
+ card = case payment['card_type'].to_s.downcase.gsub(' ', '').strip
946
+ when '' then nil
947
+ when 'v', 'visa' then 'Visa'
948
+ when 'm', 'mc', 'master', 'mastercard' then 'MasterCard'
949
+ when 'a', 'ax', 'american', 'americanexpress' then 'American Express'
950
+ when 'd', 'discover' then 'Discover'
951
+ else payment_card.to_s
952
+ end
953
+ end
954
+
955
+ last4 = if payment[:active_card] && payment[:active_card].include?('**** **** ****')
956
+ payment[:active_card][15,4]
957
+ end
958
+
959
+ last4 ||= if payment['active_card'] && payment['active_card'].include?('**** **** ****')
960
+ payment['active_card'][15,4]
961
+ end
962
+
963
+ # stripe, moneris, moneris_checkout
964
+ last4 ||= (payment['f4l4'] || payment['first6last4']).to_s.last(4)
965
+
966
+ [provider.presence, card.presence, last4.presence].compact.join(' - ')
967
+ end
968
+
887
969
  private
888
970
 
889
971
  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
@@ -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.1'.freeze
2
+ VERSION = '6.12.1'.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.1
4
+ version: 6.12.1
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-17 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
@@ -194,6 +194,7 @@ files:
194
194
  - app/assets/javascripts/effective_orders.js
195
195
  - app/assets/javascripts/effective_orders/customers.js.coffee
196
196
  - app/assets/javascripts/effective_orders/providers/deluxe.js
197
+ - app/assets/javascripts/effective_orders/providers/deluxe_delayed.js
197
198
  - app/assets/javascripts/effective_orders/providers/moneris_checkout.js.coffee
198
199
  - app/assets/javascripts/effective_orders/providers/stripe.js.coffee
199
200
  - app/assets/javascripts/effective_orders/subscriptions.js.coffee
@@ -210,6 +211,8 @@ files:
210
211
  - app/controllers/effective/orders_controller.rb
211
212
  - app/controllers/effective/providers/cheque.rb
212
213
  - app/controllers/effective/providers/deluxe.rb
214
+ - app/controllers/effective/providers/deluxe_delayed.rb
215
+ - app/controllers/effective/providers/deluxe_delayed_purchase.rb
213
216
  - app/controllers/effective/providers/etransfer.rb
214
217
  - app/controllers/effective/providers/free.rb
215
218
  - app/controllers/effective/providers/mark_as_paid.rb
@@ -230,6 +233,7 @@ files:
230
233
  - app/datatables/admin/report_transactions_grouped_by_qb_name_datatable.rb
231
234
  - app/datatables/effective_orders_datatable.rb
232
235
  - app/helpers/effective_carts_helper.rb
236
+ - app/helpers/effective_deluxe_delayed_helper.rb
233
237
  - app/helpers/effective_deluxe_helper.rb
234
238
  - app/helpers/effective_moneris_checkout_helper.rb
235
239
  - app/helpers/effective_orders_helper.rb
@@ -295,9 +299,15 @@ files:
295
299
  - app/views/effective/orders/declined.html.haml
296
300
  - app/views/effective/orders/deferred.html.haml
297
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
298
304
  - app/views/effective/orders/deluxe/_css.html.haml
299
305
  - app/views/effective/orders/deluxe/_element.html.haml
300
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
301
311
  - app/views/effective/orders/edit.html.haml
302
312
  - app/views/effective/orders/etransfer/_form.html.haml
303
313
  - app/views/effective/orders/free/_form.html.haml