spree_vpago 2.4.2 → 2.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/test_and_publish_gem.yml +3 -1
- data/Gemfile.lock +1 -1
- data/Rakefile +7 -0
- data/app/assets/javascripts/vpago/vpago_payments/user_informers/firebase.js +51 -1
- data/app/controllers/spree/admin/suspicious_orders_controller.rb +7 -1
- data/app/controllers/spree/vpago_payments_controller.rb +4 -1
- data/app/javascripts/vpago/vpago_payments/user_informers/firebase.js +56 -1
- data/app/jobs/concerns/vpago/sidekiq_retry_callbacks.rb +46 -0
- data/app/jobs/vpago/payment_capturer_job.rb +50 -1
- data/app/models/vpago/payment_decorator.rb +46 -0
- data/app/services/vpago/payment_processor.rb +19 -1
- data/app/services/vpago/user_informers/firebase.rb +3 -0
- data/lib/spree_vpago/version.rb +1 -1
- data/lib/vpago/payway_v2/pre_auth_canceler.rb +1 -5
- data/lib/vpago/payway_v2/pre_auth_completer.rb +15 -5
- data/node_modules/.yarn-integrity +143 -15
- data/package.json +1 -1
- data/yarn.lock +784 -69
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c60cd32b8e9acf6a13c8f3190521841f2305566a750750dd8ae050eb65728859
|
|
4
|
+
data.tar.gz: 0c7dfd54ac257faa8ec0690cb529d2c42ac74dfe4f4103daf16f39a95794818d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7306bfdaa203f43b7bd28e1f8237efb3b4c6c72dc98160d6e1ec6afdddace2871ca790fc133b5dde2cc6b7e4703fdcdbf0ed99b2d3f5e258f2a3be20b7a38657
|
|
7
|
+
data.tar.gz: da6a16929e7b9b11427e922ceb8dafdc236ec876e9047666eb062700b767b0030b75ee2fa15543fe3f03ed5a9920ad2efe79add77f4fc3c5471aa918f2081ac1
|
|
@@ -15,7 +15,9 @@ on:
|
|
|
15
15
|
- "*"
|
|
16
16
|
jobs:
|
|
17
17
|
test_and_publish_gem:
|
|
18
|
-
|
|
18
|
+
# Pinned: ubuntu-latest moved to 26.04, where setup-ruby only ships the latest patch of each series, so
|
|
19
|
+
# ruby 3.2.0 below 404s. Bump both together.
|
|
20
|
+
runs-on: ubuntu-24.04
|
|
19
21
|
|
|
20
22
|
services:
|
|
21
23
|
db:
|
data/Gemfile.lock
CHANGED
data/Rakefile
CHANGED
|
@@ -18,4 +18,11 @@ desc 'Generates a dummy app for testing'
|
|
|
18
18
|
task :test_app do
|
|
19
19
|
ENV['LIB_NAME'] = 'spree_vpago'
|
|
20
20
|
Rake::Task['extension:test_app'].invoke
|
|
21
|
+
|
|
22
|
+
# spree_backend 4.5's installer runs an unpinned `yarn add @spree/dashboard`, which now resolves to Spree 6's
|
|
23
|
+
# React dashboard that this dummy app's esbuild can't bundle. spree-dashboard.js is then never built and every
|
|
24
|
+
# spec rendering a page fails, so pin the last 4.x-compatible release and rebuild.
|
|
25
|
+
Dir.chdir(File.expand_path('spec/dummy', __dir__)) do
|
|
26
|
+
system('yarn add @spree/dashboard@0.2.1 && yarn build && bundle exec rake assets:precompile', exception: true)
|
|
27
|
+
end
|
|
21
28
|
end
|
|
@@ -13689,6 +13689,11 @@ This typically indicates that your device does not have a healthy Internet conne
|
|
|
13689
13689
|
};
|
|
13690
13690
|
|
|
13691
13691
|
// app/javascripts/vpago/vpago_payments/user_informers/firebase.js
|
|
13692
|
+
var CAPTURE_PENDING_CODES = [
|
|
13693
|
+
"payment_is_capturing",
|
|
13694
|
+
"payment_capture_retrying",
|
|
13695
|
+
"payment_capture_unconfirmed"
|
|
13696
|
+
];
|
|
13692
13697
|
async function listenToProcessingState({
|
|
13693
13698
|
firebaseConfigs,
|
|
13694
13699
|
documentReferencePath,
|
|
@@ -13696,6 +13701,9 @@ This typically indicates that your device does not have a healthy Internet conne
|
|
|
13696
13701
|
onPaymentIsRetrying,
|
|
13697
13702
|
onOrderIsProcessing,
|
|
13698
13703
|
onOrderIsCompleted,
|
|
13704
|
+
onPaymentIsCapturing,
|
|
13705
|
+
onPaymentCaptureRetrying,
|
|
13706
|
+
onPaymentCaptureUnconfirmed,
|
|
13699
13707
|
onOrderProcessFailed,
|
|
13700
13708
|
onPaymentProcessFailed,
|
|
13701
13709
|
onCompleted
|
|
@@ -13716,7 +13724,7 @@ This typically indicates that your device does not have a healthy Internet conne
|
|
|
13716
13724
|
if (window.parent !== window) {
|
|
13717
13725
|
window.parent.postMessage(messageCode, "*");
|
|
13718
13726
|
}
|
|
13719
|
-
let orderCompleted = orderState === "complete";
|
|
13727
|
+
let orderCompleted = orderState === "complete" && !CAPTURE_PENDING_CODES.includes(messageCode);
|
|
13720
13728
|
if (orderCompleted) {
|
|
13721
13729
|
queueProcessor.queueStateChange({
|
|
13722
13730
|
minDelayInMs: 1500,
|
|
@@ -13788,6 +13796,48 @@ This typically indicates that your device does not have a healthy Internet conne
|
|
|
13788
13796
|
}
|
|
13789
13797
|
});
|
|
13790
13798
|
break;
|
|
13799
|
+
case "payment_is_capturing":
|
|
13800
|
+
queueProcessor.queueStateChange({
|
|
13801
|
+
minDelayInMs: 1500,
|
|
13802
|
+
callback: async () => {
|
|
13803
|
+
await onPaymentIsCapturing?.(
|
|
13804
|
+
orderState,
|
|
13805
|
+
paymentState,
|
|
13806
|
+
processing,
|
|
13807
|
+
reasonCode,
|
|
13808
|
+
reasonMessage
|
|
13809
|
+
);
|
|
13810
|
+
}
|
|
13811
|
+
});
|
|
13812
|
+
break;
|
|
13813
|
+
case "payment_capture_retrying":
|
|
13814
|
+
queueProcessor.queueStateChange({
|
|
13815
|
+
minDelayInMs: 1500,
|
|
13816
|
+
callback: async () => {
|
|
13817
|
+
await onPaymentCaptureRetrying?.(
|
|
13818
|
+
orderState,
|
|
13819
|
+
paymentState,
|
|
13820
|
+
processing,
|
|
13821
|
+
reasonCode,
|
|
13822
|
+
reasonMessage
|
|
13823
|
+
);
|
|
13824
|
+
}
|
|
13825
|
+
});
|
|
13826
|
+
break;
|
|
13827
|
+
case "payment_capture_unconfirmed":
|
|
13828
|
+
queueProcessor.queueStateChange({
|
|
13829
|
+
minDelayInMs: 1500,
|
|
13830
|
+
callback: async () => {
|
|
13831
|
+
await onPaymentCaptureUnconfirmed?.(
|
|
13832
|
+
orderState,
|
|
13833
|
+
paymentState,
|
|
13834
|
+
processing,
|
|
13835
|
+
reasonCode,
|
|
13836
|
+
reasonMessage
|
|
13837
|
+
);
|
|
13838
|
+
}
|
|
13839
|
+
});
|
|
13840
|
+
break;
|
|
13791
13841
|
case "order_process_failed":
|
|
13792
13842
|
queueProcessor.queueStateChange({
|
|
13793
13843
|
minDelayInMs: 1500,
|
|
@@ -4,7 +4,7 @@ module Spree
|
|
|
4
4
|
around_action :set_writing_role, only: %i[check_transaction]
|
|
5
5
|
|
|
6
6
|
def index
|
|
7
|
-
params[:q] =
|
|
7
|
+
params[:q] = default_query if params[:q].blank?
|
|
8
8
|
|
|
9
9
|
@search = exclude_request_bookings(scope).ransack(params[:q])
|
|
10
10
|
@orders = @search.result(distinct: true)
|
|
@@ -60,6 +60,12 @@ module Spree
|
|
|
60
60
|
current_store.orders.accessible_by(current_ability, :index)
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
# `failed` too: a pre-auth capture that failed under the old flow left its order `failed`
|
|
64
|
+
# (its only payment is invalid), which `balance_due` alone never listed.
|
|
65
|
+
def default_query
|
|
66
|
+
{ completed_at_not_null: 1, payment_state_in: %w[balance_due failed], s: 'completed_at desc' }
|
|
67
|
+
end
|
|
68
|
+
|
|
63
69
|
# Request bookings complete before the vendor confirms and the customer
|
|
64
70
|
# pays, so balance_due is expected there rather than a lost payment.
|
|
65
71
|
# `request_state` is added by spree_cm_commissioner, hence the column check.
|
|
@@ -32,7 +32,10 @@ module Spree
|
|
|
32
32
|
# checker often completes the order before the browser lands here. Skipping this page
|
|
33
33
|
# also skips its Firebase listener, so success must post order_is_completed itself
|
|
34
34
|
# (render_success_script) or an embedding iframe modal never closes.
|
|
35
|
-
|
|
35
|
+
#
|
|
36
|
+
# A pre-auth payment is the exception: its order completes on the hold, so the user (often just back
|
|
37
|
+
# from the bank app) stays here on "verifying" until PaymentCapturerJob captures it.
|
|
38
|
+
return redirect_to @payment.success_url, allow_other_host: true if @order.completed? && !@payment.awaiting_pre_auth_capture?
|
|
36
39
|
|
|
37
40
|
VpagoLogger.log(label: 'Spree::VpagoPaymentsController#processing', data: vpago_log_context)
|
|
38
41
|
render 'spree/vpago_shared/processing'
|
|
@@ -2,6 +2,12 @@ import { initializeApp } from "firebase/app";
|
|
|
2
2
|
import { getFirestore, doc, onSnapshot, setDoc } from "firebase/firestore";
|
|
3
3
|
import QueueProcessor from "../../../queue_processor.js";
|
|
4
4
|
|
|
5
|
+
const CAPTURE_PENDING_CODES = [
|
|
6
|
+
"payment_is_capturing",
|
|
7
|
+
"payment_capture_retrying",
|
|
8
|
+
"payment_capture_unconfirmed",
|
|
9
|
+
];
|
|
10
|
+
|
|
5
11
|
async function listenToProcessingState({
|
|
6
12
|
firebaseConfigs,
|
|
7
13
|
documentReferencePath,
|
|
@@ -9,6 +15,9 @@ async function listenToProcessingState({
|
|
|
9
15
|
onPaymentIsRetrying,
|
|
10
16
|
onOrderIsProcessing,
|
|
11
17
|
onOrderIsCompleted,
|
|
18
|
+
onPaymentIsCapturing,
|
|
19
|
+
onPaymentCaptureRetrying,
|
|
20
|
+
onPaymentCaptureUnconfirmed,
|
|
12
21
|
onOrderProcessFailed,
|
|
13
22
|
onPaymentProcessFailed,
|
|
14
23
|
onCompleted,
|
|
@@ -35,7 +44,10 @@ async function listenToProcessingState({
|
|
|
35
44
|
window.parent.postMessage(messageCode, "*");
|
|
36
45
|
}
|
|
37
46
|
|
|
38
|
-
|
|
47
|
+
// A pre-auth order completes on the hold, before the money is captured, so it isn't a success yet:
|
|
48
|
+
// the server sends these capture codes until PaymentCapturerJob reports order_is_completed.
|
|
49
|
+
let orderCompleted =
|
|
50
|
+
orderState === "complete" && !CAPTURE_PENDING_CODES.includes(messageCode);
|
|
39
51
|
if (orderCompleted) {
|
|
40
52
|
queueProcessor.queueStateChange({
|
|
41
53
|
minDelayInMs: 1500,
|
|
@@ -108,6 +120,49 @@ async function listenToProcessingState({
|
|
|
108
120
|
},
|
|
109
121
|
});
|
|
110
122
|
break;
|
|
123
|
+
case "payment_is_capturing":
|
|
124
|
+
queueProcessor.queueStateChange({
|
|
125
|
+
minDelayInMs: 1500,
|
|
126
|
+
callback: async () => {
|
|
127
|
+
// Optional: pages built before this message existed don't pass it.
|
|
128
|
+
await onPaymentIsCapturing?.(
|
|
129
|
+
orderState,
|
|
130
|
+
paymentState,
|
|
131
|
+
processing,
|
|
132
|
+
reasonCode,
|
|
133
|
+
reasonMessage,
|
|
134
|
+
);
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
break;
|
|
138
|
+
case "payment_capture_retrying":
|
|
139
|
+
queueProcessor.queueStateChange({
|
|
140
|
+
minDelayInMs: 1500,
|
|
141
|
+
callback: async () => {
|
|
142
|
+
await onPaymentCaptureRetrying?.(
|
|
143
|
+
orderState,
|
|
144
|
+
paymentState,
|
|
145
|
+
processing,
|
|
146
|
+
reasonCode,
|
|
147
|
+
reasonMessage,
|
|
148
|
+
);
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
break;
|
|
152
|
+
case "payment_capture_unconfirmed":
|
|
153
|
+
queueProcessor.queueStateChange({
|
|
154
|
+
minDelayInMs: 1500,
|
|
155
|
+
callback: async () => {
|
|
156
|
+
await onPaymentCaptureUnconfirmed?.(
|
|
157
|
+
orderState,
|
|
158
|
+
paymentState,
|
|
159
|
+
processing,
|
|
160
|
+
reasonCode,
|
|
161
|
+
reasonMessage,
|
|
162
|
+
);
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
break;
|
|
111
166
|
case "order_process_failed":
|
|
112
167
|
queueProcessor.queueStateChange({
|
|
113
168
|
minDelayInMs: 1500,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Vpago
|
|
2
|
+
# Callbacks for a job retried by Sidekiq itself. ActiveJob's `retry_on` isn't an option for a job with an
|
|
3
|
+
# `until_executed` lock: each retry is a new enqueue, which the job's own lock refuses.
|
|
4
|
+
#
|
|
5
|
+
# after_failed_attempt Spree::Core::GatewayError, with: :notify_retrying # each failed run; the error is re-raised
|
|
6
|
+
# after_retries_exhausted with: :notify_gave_up # once Sidekiq stops retrying
|
|
7
|
+
#
|
|
8
|
+
# Handlers are private instance methods that read the job's `arguments`. A handler error is logged and never
|
|
9
|
+
# replaces the job's own error, so Sidekiq still retries on the original failure.
|
|
10
|
+
module SidekiqRetryCallbacks
|
|
11
|
+
extend ActiveSupport::Concern
|
|
12
|
+
|
|
13
|
+
class_methods do
|
|
14
|
+
def after_failed_attempt(*errors, with:)
|
|
15
|
+
around_perform do |job, block|
|
|
16
|
+
block.call
|
|
17
|
+
rescue *errors => e
|
|
18
|
+
job.send(:run_retry_callback, with)
|
|
19
|
+
raise e
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Sidekiq-only, so a no-op where the host app doesn't run Sidekiq (e.g. this gem's specs). Sidekiq passes
|
|
24
|
+
# its own payload, which wraps the serialized ActiveJob.
|
|
25
|
+
def after_retries_exhausted(with:)
|
|
26
|
+
return unless respond_to?(:sidekiq_retries_exhausted)
|
|
27
|
+
|
|
28
|
+
sidekiq_retries_exhausted do |sidekiq_job, _error|
|
|
29
|
+
serialized_job = sidekiq_job['args'].first
|
|
30
|
+
new(*ActiveJob::Arguments.deserialize(serialized_job['arguments'])).send(:run_retry_callback, with)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def run_retry_callback(method_name)
|
|
38
|
+
send(method_name)
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
VpagoLogger.error(
|
|
41
|
+
label: "#{self.class.name}##{method_name} failed",
|
|
42
|
+
data: { arguments: arguments, error_class: e.class.name, error_message: e.message }
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -1,11 +1,60 @@
|
|
|
1
1
|
# Put :payment_processing at a higher priority in your project: config/sidekiq.yml
|
|
2
2
|
module Vpago
|
|
3
3
|
class PaymentCapturerJob < ::ApplicationUniqueJob
|
|
4
|
+
include Vpago::SidekiqRetryCallbacks
|
|
5
|
+
|
|
4
6
|
queue_as :payment_processing
|
|
5
7
|
|
|
8
|
+
# A failed capture raises, so Sidekiq retries it. Pinned here rather than left to the host app's
|
|
9
|
+
# max_retries, because the count sets how long the processing page says "taking a little longer"
|
|
10
|
+
# before "not confirmed": 5 retries run for ~8 minutes with Sidekiq's default backoff.
|
|
11
|
+
sidekiq_options retry: 5 if respond_to?(:sidekiq_options)
|
|
12
|
+
|
|
13
|
+
# The default 1-day lock outlives a job that exhausted its retries and would silently refuse every
|
|
14
|
+
# re-enqueue for this payment (ABA pushback, a repair run) until the next day; 30 minutes covers the
|
|
15
|
+
# retry window above. The host app's ApplicationUniqueJob provides `unique`, spree_vpago doesn't.
|
|
16
|
+
unique :until_executed, lock_ttl: 30.minutes, on_conflict: :log if respond_to?(:unique)
|
|
17
|
+
|
|
18
|
+
# Keeps the processing page honest while Sidekiq retries a pre-auth capture.
|
|
19
|
+
after_failed_attempt Spree::Core::GatewayError, with: :notify_capture_retrying
|
|
20
|
+
after_retries_exhausted with: :give_up_capture
|
|
21
|
+
|
|
6
22
|
def perform(payment_id)
|
|
7
|
-
payment = Spree::Payment.find(payment_id)
|
|
23
|
+
@payment = Spree::Payment.find(payment_id)
|
|
8
24
|
payment.capture! if payment.actions.include?('capture')
|
|
25
|
+
|
|
26
|
+
# The processing page holds a pre-auth payment on "verifying" until this message arrives (see
|
|
27
|
+
# PaymentProcessor#notify_order_completed). Keyed on the result rather than on this run's capture,
|
|
28
|
+
# so a retry after a failed Firestore write still sends it.
|
|
29
|
+
payment.user_informer.order_is_completed(processing: false) if payment.payment_method.enable_pre_auth? && payment.completed?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def notify_capture_retrying
|
|
35
|
+
return unless payment&.awaiting_pre_auth_capture?
|
|
36
|
+
|
|
37
|
+
payment.user_informer.payment_capture_retrying(processing: true)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Frees the lock now rather than at its TTL, so the page's reload (or ABA pushback) can start a new
|
|
41
|
+
# capture straight away. Released after the page is told, so a reload racing this can't have its
|
|
42
|
+
# "verifying" overwritten by "not confirmed".
|
|
43
|
+
def give_up_capture
|
|
44
|
+
notify_capture_unconfirmed
|
|
45
|
+
ensure
|
|
46
|
+
self.class.unlock!(*arguments) if self.class.respond_to?(:unlock!)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Skipped when the capture itself succeeded and only the later Firestore write kept failing.
|
|
50
|
+
def notify_capture_unconfirmed
|
|
51
|
+
return unless payment&.awaiting_pre_auth_capture?
|
|
52
|
+
|
|
53
|
+
payment.user_informer.payment_capture_unconfirmed(processing: false)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def payment
|
|
57
|
+
@payment ||= Spree::Payment.find_by(id: arguments.first)
|
|
9
58
|
end
|
|
10
59
|
end
|
|
11
60
|
end
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
#
|
|
13
13
|
# This allows process! or capture! to be retried by Sidekiq or by admin via /fire after connection errors
|
|
14
14
|
# or other transient gateway failures.
|
|
15
|
+
#
|
|
16
|
+
# Pre-auth capture! is the exception: it never leaves `pending` on failure, see #pre_auth_capture!.
|
|
15
17
|
module Vpago
|
|
16
18
|
module PaymentDecorator
|
|
17
19
|
def self.prepended(base)
|
|
@@ -55,6 +57,8 @@ module Vpago
|
|
|
55
57
|
|
|
56
58
|
# override
|
|
57
59
|
def capture!(amount = nil)
|
|
60
|
+
return pre_auth_capture!(amount) if payment_method&.enable_pre_auth?
|
|
61
|
+
|
|
58
62
|
reset_for_retry! if failed?
|
|
59
63
|
|
|
60
64
|
super
|
|
@@ -97,6 +101,12 @@ module Vpago
|
|
|
97
101
|
pre_auth_status == 'CANCELLED'
|
|
98
102
|
end
|
|
99
103
|
|
|
104
|
+
# Authorized (ABA holds the money) but not captured yet: the order may already be complete,
|
|
105
|
+
# yet nothing has been charged.
|
|
106
|
+
def awaiting_pre_auth_capture?
|
|
107
|
+
payment_method.enable_pre_auth? && (pending? || processing?)
|
|
108
|
+
end
|
|
109
|
+
|
|
100
110
|
def true_money_payment?
|
|
101
111
|
payment_method.type_true_money?
|
|
102
112
|
end
|
|
@@ -124,6 +134,42 @@ module Vpago
|
|
|
124
134
|
def can_redirect_to_app?
|
|
125
135
|
payment_method.can_redirect_to_app?
|
|
126
136
|
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
# Spree's capture! moves the payment to processing and, when the gateway fails, to failed.
|
|
141
|
+
# For a pre-auth payment that is wrong: ABA still holds the money, and `failed` hides the
|
|
142
|
+
# capture/void actions (VpagoPaymentSource requires `pending`), so PaymentCapturerJob's
|
|
143
|
+
# Sidekiq retries found nothing to do and the capture was attempted only once.
|
|
144
|
+
#
|
|
145
|
+
# Here only a confirmed capture changes the state (pending -> completed). A failed attempt
|
|
146
|
+
# writes nothing, so the payment stays retryable and a slow failing call can't overwrite a
|
|
147
|
+
# concurrent success. The capture event is also written only when money actually moved
|
|
148
|
+
# (Spree writes it before knowing the result).
|
|
149
|
+
def pre_auth_capture!(amount)
|
|
150
|
+
return true if completed?
|
|
151
|
+
|
|
152
|
+
# A `failed` pre-auth payment was left by the old flow; its hold may or may not still be
|
|
153
|
+
# active, so it must be checked with ABA and repaired before capturing again.
|
|
154
|
+
gateway_error("Cannot capture pre-auth payment #{number} in state #{state}") unless pending? || processing?
|
|
155
|
+
|
|
156
|
+
amount ||= money.amount_in_cents
|
|
157
|
+
response = begin
|
|
158
|
+
payment_method.capture(amount, response_code, gateway_options)
|
|
159
|
+
rescue ActiveMerchant::ConnectionError, Faraday::Error => e
|
|
160
|
+
# Spree logs nothing for a connection error; record it so the payment's log shows every attempt.
|
|
161
|
+
record_response(ActiveMerchant::Billing::Response.new(false, "Payway Gateway: #{e.class}: #{e.message}", {}))
|
|
162
|
+
gateway_error(ActiveMerchant::ConnectionError.new(e.message, e))
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
record_response(response)
|
|
166
|
+
gateway_error(response) unless response.success?
|
|
167
|
+
|
|
168
|
+
capture_events.create!(amount: ::Money.new(amount, currency).to_f)
|
|
169
|
+
split_uncaptured_amount
|
|
170
|
+
self.response_code = response.authorization if response.authorization.present?
|
|
171
|
+
complete!
|
|
172
|
+
end
|
|
127
173
|
end
|
|
128
174
|
end
|
|
129
175
|
|
|
@@ -49,7 +49,9 @@ module Vpago
|
|
|
49
49
|
# which will enqueue a capture payment if available.
|
|
50
50
|
def process_order!
|
|
51
51
|
log_process('process_order!') do
|
|
52
|
-
|
|
52
|
+
# On a re-run for a completed pre-auth order (ABA pushback, page reload) this message would carry
|
|
53
|
+
# order_state=complete, which the processing page treats as success before the money is captured.
|
|
54
|
+
user_informer.order_is_processing(processing: true) unless awaiting_pre_auth_capture?
|
|
53
55
|
completer = Spree::Checkout::Complete.new.call(order: @payment.order)
|
|
54
56
|
|
|
55
57
|
if completer.success?
|
|
@@ -63,11 +65,27 @@ module Vpago
|
|
|
63
65
|
|
|
64
66
|
def handle_order_process_completed
|
|
65
67
|
log_process('handle_order_process_completed') do
|
|
68
|
+
# Notify before enqueueing: PaymentCapturerJob reports order_is_completed when the capture lands,
|
|
69
|
+
# and that message must not be overwritten by a later payment_is_capturing.
|
|
70
|
+
notify_order_completed
|
|
66
71
|
enqueue_capture_payment_if_available!
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# A pre-auth order completes on the hold, but the money only moves at capture, so the processing
|
|
76
|
+
# page keeps showing "verifying" until PaymentCapturerJob sends order_is_completed.
|
|
77
|
+
def notify_order_completed
|
|
78
|
+
if awaiting_pre_auth_capture?
|
|
79
|
+
user_informer.payment_is_capturing(processing: true)
|
|
80
|
+
else
|
|
67
81
|
user_informer.order_is_completed(processing: false)
|
|
68
82
|
end
|
|
69
83
|
end
|
|
70
84
|
|
|
85
|
+
def awaiting_pre_auth_capture?
|
|
86
|
+
@payment.reload.awaiting_pre_auth_capture? && @payment.order.complete?
|
|
87
|
+
end
|
|
88
|
+
|
|
71
89
|
def handle_order_process_failure(reason_code, reason_message = nil)
|
|
72
90
|
log_process('handle_order_process_failure', reason_code, reason_message) do
|
|
73
91
|
user_informer.order_process_failed(
|
|
@@ -13,6 +13,9 @@ module Vpago
|
|
|
13
13
|
def payment_is_retrying(processing:) = notify(:payment_is_retrying, processing)
|
|
14
14
|
def order_is_processing(processing:) = notify(:order_is_processing, processing)
|
|
15
15
|
def order_is_completed(processing:) = notify(:order_is_completed, processing)
|
|
16
|
+
def payment_is_capturing(processing:) = notify(:payment_is_capturing, processing)
|
|
17
|
+
def payment_capture_retrying(processing:) = notify(:payment_capture_retrying, processing)
|
|
18
|
+
def payment_capture_unconfirmed(processing:) = notify(:payment_capture_unconfirmed, processing)
|
|
16
19
|
def order_process_failed(processing:, reason_code:, reason_message: nil) = notify(:order_process_failed, processing, reason_code, reason_message)
|
|
17
20
|
def payment_process_failed(processing:, reason_code:, reason_message: nil) = notify(:payment_process_failed, processing, reason_code, reason_message)
|
|
18
21
|
|
data/lib/spree_vpago/version.rb
CHANGED
|
@@ -5,11 +5,7 @@ module Vpago
|
|
|
5
5
|
class PreAuthCanceler < PreAuthCompleter
|
|
6
6
|
# override
|
|
7
7
|
def pre_auth_response
|
|
8
|
-
|
|
9
|
-
faraday.request :url_encoded
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
conn.post(cancel_url, request_data)
|
|
8
|
+
connection.post(cancel_url, request_data)
|
|
13
9
|
end
|
|
14
10
|
|
|
15
11
|
def request_data
|
|
@@ -18,11 +18,17 @@ module Vpago
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def pre_auth_response
|
|
21
|
-
|
|
21
|
+
connection.post(complete_url, request_data)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Without timeouts Net::HTTP waits up to 60s to connect and 60s to read, holding the
|
|
25
|
+
# payment_processing worker thread that also completes every other customer's checkout.
|
|
26
|
+
def connection
|
|
27
|
+
Faraday::Connection.new do |faraday|
|
|
22
28
|
faraday.request :url_encoded
|
|
29
|
+
faraday.options.open_timeout = Vpago::HttpTimeouts::OPEN_TIMEOUT
|
|
30
|
+
faraday.options.timeout = Vpago::HttpTimeouts::TIMEOUT
|
|
23
31
|
end
|
|
24
|
-
|
|
25
|
-
conn.post(complete_url, request_data)
|
|
26
32
|
end
|
|
27
33
|
|
|
28
34
|
def request_data
|
|
@@ -61,8 +67,11 @@ module Vpago
|
|
|
61
67
|
"#{host}/api/merchant-portal/merchant-access/online-transaction/pre-auth-completion"
|
|
62
68
|
end
|
|
63
69
|
|
|
70
|
+
# A non-JSON body (e.g. an HTML 502 page) must be a normal failure. Raising here would skip
|
|
71
|
+
# Spree's failure handling and leave the payment without any log entry.
|
|
64
72
|
def success?
|
|
65
|
-
json_response['status']
|
|
73
|
+
status = json_response.is_a?(Hash) ? json_response['status'] : nil
|
|
74
|
+
status.is_a?(Hash) && status['code'].to_s == '00'
|
|
66
75
|
end
|
|
67
76
|
|
|
68
77
|
def json_response
|
|
@@ -71,7 +80,8 @@ module Vpago
|
|
|
71
80
|
@json_response ||= begin
|
|
72
81
|
JSON.parse(@response.body)
|
|
73
82
|
rescue JSON::ParserError
|
|
74
|
-
|
|
83
|
+
# Kept so the payment's log entry shows what the gateway actually returned.
|
|
84
|
+
{ 'raw_body' => @response.body.to_s[0, 2000], 'http_status' => @response.status }
|
|
75
85
|
end
|
|
76
86
|
end
|
|
77
87
|
end
|