effective_orders 6.11.1 → 6.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +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_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: 4fac0b561dd727abc645294816cf623fb2c9bd345645b19272e510d064b0c3e8
4
+ data.tar.gz: 63728a03677c11b462de51955b4add5434fcbdd0cd7e015013ab81f1672d3da0
5
5
  SHA512:
6
- metadata.gz: 5bda2f8002c375797ee23647dd585529082a34bb274ca623b18ca2f564a4fba3768ece770ee1182164396886e5e606736b652d678c8dd79f59af8ff22799403a
7
- data.tar.gz: 0a4ab62604bee87a844acb881d69e455b61e14c1b336f9318b0113fb166f1f233f230d9b48a3bfc4e749e5cdf4d1b1ac120c6bd26a5f19a2ad9fe2a648c8f16e
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
@@ -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.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.1
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-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