spree_vpago 0.1.0.pre.beta → 2.0.5.pre.beta
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 +2 -0
- data/Gemfile.lock +25 -23
- data/app/assets/javascripts/vpago/vpago_payments/user_informers/firebase.js +1686 -1426
- data/app/controllers/spree/admin/payment_methods_controller_decorator.rb +9 -1
- data/app/controllers/spree/vpago_payments_controller.rb +12 -2
- data/app/helpers/vpago/admin/base_helper_decorator.rb +1 -0
- data/app/javascripts/queue_processor.js +33 -0
- data/app/javascripts/queue_processor.test.js +88 -0
- data/app/javascripts/vpago/vpago_payments/user_informers/firebase.js +92 -13
- data/app/jobs/vpago/payment_capturer_job.rb +11 -0
- data/app/jobs/vpago/payment_processor_job.rb +3 -0
- data/app/models/spree/gateway/vattanac_mini_app.rb +75 -0
- data/app/models/vpago/address_decorator.rb +10 -0
- data/app/models/vpago/order_decorator.rb +13 -1
- data/app/models/vpago/payment_decorator.rb +23 -1
- data/app/models/vpago/payment_method_decorator.rb +7 -1
- data/app/overrides/spree/admin/payment_methods/index/payment_methods_tabs.html.erb.deface +7 -1
- data/app/overrides/spree/admin/payment_methods/index/tenant_body.html.erb.deface +5 -0
- data/app/overrides/spree/admin/payment_methods/index/tenant_header.html.erb.deface +5 -0
- data/app/serializers/spree/v2/storefront/payment_serializer_decorator.rb +1 -1
- data/app/services/vpago/aes_encrypter.rb +56 -0
- data/app/services/vpago/payment_finder.rb +19 -3
- data/app/services/vpago/payment_processable.rb +54 -0
- data/app/services/vpago/payment_processor.rb +58 -41
- data/app/services/vpago/payment_url_constructor.rb +1 -1
- data/app/services/vpago/rsa_handler.rb +27 -0
- data/app/services/vpago/user_informers/firebase.rb +21 -16
- data/app/services/vpago/vattanac_mini_app_data_handler.rb +33 -0
- data/app/views/spree/admin/payments/source_views/_vattanac_mini_app.html.erb +6 -0
- data/app/views/spree/admin/shared/_payment_methods_tabs.html.erb +7 -0
- data/app/views/spree/vpago_payments/forms/spree/gateway/_vattanac_mini_app.html.erb +89 -0
- data/app/views/spree/vpago_payments/processing.html.erb +36 -23
- data/lib/spree_vpago/engine.rb +2 -1
- data/lib/spree_vpago/version.rb +1 -1
- data/lib/vpago/payway_v2/base.rb +8 -8
- data/lib/vpago/payway_v2/checkout.rb +2 -2
- data/lib/vpago/vattanac_mini_app/base.rb +52 -0
- data/lib/vpago/vattanac_mini_app/checkout.rb +9 -0
- data/lib/vpago/vattanac_mini_app/refund_issuer.rb +54 -0
- data/node_modules/.yarn-integrity +93 -2
- data/package.json +6 -1
- data/yarn.lock +556 -2
- metadata +18 -2
@@ -3,7 +3,15 @@ module Spree
|
|
3
3
|
module PaymentMethodsControllerDecorator
|
4
4
|
def scope
|
5
5
|
scope = current_store.payment_methods_including_vendor.accessible_by(current_ability, :index)
|
6
|
-
|
6
|
+
|
7
|
+
if params[:tab] == 'vendors'
|
8
|
+
scope = scope.where.not(vendor_id: nil)
|
9
|
+
elsif params[:tab] == 'tenants'
|
10
|
+
scope = scope.joins(:vendor)
|
11
|
+
.where.not(vendor_id: nil)
|
12
|
+
.where.not(spree_vendors: { tenant_id: nil })
|
13
|
+
end
|
14
|
+
|
7
15
|
scope
|
8
16
|
end
|
9
17
|
|
@@ -39,7 +39,8 @@ module Spree
|
|
39
39
|
def process_payment
|
40
40
|
return render json: { status: :ok }, status: :ok if request.method != 'POST'
|
41
41
|
|
42
|
-
|
42
|
+
return_params = sanitize_return_params
|
43
|
+
@payment = Vpago::PaymentFinder.new(return_params).find_and_verify
|
43
44
|
return render_not_found unless @payment.present?
|
44
45
|
|
45
46
|
unless @payment.order.paid?
|
@@ -54,7 +55,16 @@ module Spree
|
|
54
55
|
render json: { status: :internal_server_error, message: 'Failed to enqueue payment processor job' }, status: :internal_server_error
|
55
56
|
end
|
56
57
|
|
57
|
-
def
|
58
|
+
def sanitize_return_params
|
59
|
+
sanitized_params = params.permit!.to_h
|
60
|
+
|
61
|
+
# In ABA case, it returns params in side return params.
|
62
|
+
sanitized_params.merge!(JSON.parse(sanitized_params.delete(:return_params))) if sanitized_params[:return_params].present?
|
63
|
+
|
64
|
+
sanitized_params
|
65
|
+
end
|
66
|
+
|
67
|
+
def render_not_found
|
58
68
|
respond_to do |format|
|
59
69
|
format.html { render file: Rails.public_path.join('404.html'), status: :not_found, layout: false }
|
60
70
|
format.json { render json: { status: :not_found }, status: :not_found }
|
@@ -0,0 +1,33 @@
|
|
1
|
+
export default class QueueProcessor {
|
2
|
+
constructor() {
|
3
|
+
this.queues = [];
|
4
|
+
this.processing = false;
|
5
|
+
}
|
6
|
+
|
7
|
+
queueStateChange({ callback, minDelayInMs = 1000 }) {
|
8
|
+
this.queues.push({ callback, minDelayInMs });
|
9
|
+
if (!this.processing) this.#processQueue();
|
10
|
+
}
|
11
|
+
|
12
|
+
async #processQueue() {
|
13
|
+
if (this.queues.length === 0) {
|
14
|
+
this.processing = false;
|
15
|
+
return;
|
16
|
+
}
|
17
|
+
|
18
|
+
this.processing = true;
|
19
|
+
const { callback, minDelayInMs } = this.queues.shift();
|
20
|
+
const startTime = Date.now();
|
21
|
+
|
22
|
+
await callback();
|
23
|
+
|
24
|
+
const elapsedTime = Date.now() - startTime;
|
25
|
+
if (elapsedTime < minDelayInMs) {
|
26
|
+
await new Promise((resolve) =>
|
27
|
+
setTimeout(resolve, minDelayInMs - elapsedTime)
|
28
|
+
);
|
29
|
+
}
|
30
|
+
|
31
|
+
this.#processQueue();
|
32
|
+
}
|
33
|
+
}
|
@@ -0,0 +1,88 @@
|
|
1
|
+
import { expect } from "chai";
|
2
|
+
import QueueProcessor from "./queue_processor.js";
|
3
|
+
|
4
|
+
describe("QueueProcessor", () => {
|
5
|
+
let queueProcessor;
|
6
|
+
let minDelayInMs = 100;
|
7
|
+
|
8
|
+
beforeEach(() => {
|
9
|
+
queueProcessor = new QueueProcessor();
|
10
|
+
});
|
11
|
+
|
12
|
+
it("should initially have an empty queue and not be processing", () => {
|
13
|
+
expect(queueProcessor.queues).to.have.lengthOf(0);
|
14
|
+
expect(queueProcessor.processing).to.equal(false);
|
15
|
+
});
|
16
|
+
|
17
|
+
it("should run queue 1 by 1 while keep min delay 100", async () => {
|
18
|
+
let timeToProcess = 50;
|
19
|
+
|
20
|
+
const mockCallback = () =>
|
21
|
+
new Promise((resolve) => setTimeout(resolve, timeToProcess));
|
22
|
+
|
23
|
+
queueProcessor.queueStateChange({
|
24
|
+
callback: mockCallback,
|
25
|
+
minDelayInMs: minDelayInMs,
|
26
|
+
});
|
27
|
+
|
28
|
+
queueProcessor.queueStateChange({
|
29
|
+
callback: mockCallback,
|
30
|
+
minDelayInMs: minDelayInMs,
|
31
|
+
});
|
32
|
+
|
33
|
+
queueProcessor.queueStateChange({
|
34
|
+
callback: mockCallback,
|
35
|
+
minDelayInMs: minDelayInMs,
|
36
|
+
});
|
37
|
+
|
38
|
+
expect(queueProcessor.queues).to.have.lengthOf(2);
|
39
|
+
expect(queueProcessor.processing).to.equal(true);
|
40
|
+
|
41
|
+
await new Promise((resolve) => setTimeout(resolve, minDelayInMs * 3 + 4)); // +4ms for buffer
|
42
|
+
|
43
|
+
expect(queueProcessor.queues).to.have.lengthOf(0);
|
44
|
+
expect(queueProcessor.processing).to.equal(false);
|
45
|
+
});
|
46
|
+
|
47
|
+
describe("when process take less than the delay", () => {
|
48
|
+
it("should process and wait for remaining delay", async () => {
|
49
|
+
let timeToProcess = 50;
|
50
|
+
|
51
|
+
const mockCallback = () =>
|
52
|
+
new Promise((resolve) => setTimeout(resolve, timeToProcess));
|
53
|
+
|
54
|
+
queueProcessor.queueStateChange({
|
55
|
+
callback: mockCallback,
|
56
|
+
minDelayInMs: minDelayInMs,
|
57
|
+
});
|
58
|
+
|
59
|
+
expect(queueProcessor.queues).to.have.lengthOf(0);
|
60
|
+
expect(queueProcessor.processing).to.equal(true);
|
61
|
+
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, minDelayInMs + 1)); // +1ms for buffer
|
63
|
+
|
64
|
+
expect(queueProcessor.queues).to.have.lengthOf(0);
|
65
|
+
expect(queueProcessor.processing).to.equal(false);
|
66
|
+
});
|
67
|
+
});
|
68
|
+
|
69
|
+
describe("when process take longer than the delay", () => {
|
70
|
+
it("should process and not wait for delay", async () => {
|
71
|
+
let timeToProcess = 200;
|
72
|
+
|
73
|
+
const mockCallback = () =>
|
74
|
+
new Promise((resolve) => setTimeout(resolve, timeToProcess));
|
75
|
+
|
76
|
+
queueProcessor.queueStateChange({
|
77
|
+
callback: mockCallback,
|
78
|
+
minDelayInMs: minDelayInMs,
|
79
|
+
});
|
80
|
+
|
81
|
+
expect(queueProcessor.queues).to.have.lengthOf(0);
|
82
|
+
expect(queueProcessor.processing).to.equal(true);
|
83
|
+
|
84
|
+
await new Promise((resolve) => setTimeout(resolve, timeToProcess + 1)); // +1ms for buffer
|
85
|
+
expect(queueProcessor.processing).to.equal(false);
|
86
|
+
});
|
87
|
+
});
|
88
|
+
});
|
@@ -1,9 +1,10 @@
|
|
1
1
|
import { initializeApp } from "firebase/app";
|
2
2
|
import { getFirestore, doc, onSnapshot, setDoc } from "firebase/firestore";
|
3
|
+
import QueueProcessor from "../../../queue_processor.js";
|
3
4
|
|
4
5
|
async function listenToProcessingState({
|
5
6
|
firebaseConfigs,
|
6
|
-
|
7
|
+
documentReferencePath,
|
7
8
|
onPaymentIsProcessing,
|
8
9
|
onOrderIsProcessing,
|
9
10
|
onOrderIsCompleted,
|
@@ -15,43 +16,121 @@ async function listenToProcessingState({
|
|
15
16
|
const app = initializeApp(firebaseConfigs);
|
16
17
|
const db = getFirestore(app);
|
17
18
|
|
18
|
-
const
|
19
|
-
|
20
|
-
const documentRef = doc(db, "statuses", "cart", currentDate, orderNumber);
|
19
|
+
const documentRef = doc(db, documentReferencePath);
|
21
20
|
await setDoc(documentRef, { listening: true }, { merge: true });
|
22
21
|
|
22
|
+
const queueProcessor = new QueueProcessor();
|
23
|
+
|
23
24
|
onSnapshot(documentRef, (doc) => {
|
24
25
|
let documentData = doc.data();
|
25
26
|
|
27
|
+
let messageCode = documentData["message_code"];
|
26
28
|
let orderState = documentData["order_state"];
|
27
29
|
let paymentState = documentData["payment_state"];
|
28
|
-
let
|
29
|
-
let
|
30
|
+
let processing = documentData["processing"] === true;
|
31
|
+
let reasonCode = documentData["reason_code"];
|
32
|
+
let reasonMessage = documentData["reason_message"];
|
30
33
|
|
31
34
|
let orderCompleted = orderState === "complete";
|
32
35
|
if (orderCompleted) {
|
33
|
-
|
36
|
+
queueProcessor.queueStateChange({
|
37
|
+
minDelayInMs: 1500,
|
38
|
+
callback: async () => {
|
39
|
+
await onCompleted(
|
40
|
+
orderState,
|
41
|
+
paymentState,
|
42
|
+
reasonCode,
|
43
|
+
reasonMessage
|
44
|
+
);
|
45
|
+
},
|
46
|
+
});
|
34
47
|
return;
|
35
48
|
}
|
36
49
|
|
37
50
|
switch (messageCode) {
|
38
51
|
case "payment_is_processing":
|
39
|
-
|
52
|
+
queueProcessor.queueStateChange({
|
53
|
+
minDelayInMs: 1500,
|
54
|
+
callback: async () => {
|
55
|
+
await onPaymentIsProcessing(
|
56
|
+
orderState,
|
57
|
+
paymentState,
|
58
|
+
processing,
|
59
|
+
reasonCode,
|
60
|
+
reasonMessage
|
61
|
+
);
|
62
|
+
},
|
63
|
+
});
|
40
64
|
break;
|
41
65
|
case "order_is_processing":
|
42
|
-
|
66
|
+
queueProcessor.queueStateChange({
|
67
|
+
minDelayInMs: 1500,
|
68
|
+
callback: async () => {
|
69
|
+
await onOrderIsProcessing(
|
70
|
+
orderState,
|
71
|
+
paymentState,
|
72
|
+
processing,
|
73
|
+
reasonCode,
|
74
|
+
reasonMessage
|
75
|
+
);
|
76
|
+
},
|
77
|
+
});
|
43
78
|
break;
|
44
79
|
case "order_is_completed":
|
45
|
-
|
80
|
+
queueProcessor.queueStateChange({
|
81
|
+
minDelayInMs: 1500,
|
82
|
+
callback: async () => {
|
83
|
+
await onOrderIsCompleted(
|
84
|
+
orderState,
|
85
|
+
paymentState,
|
86
|
+
processing,
|
87
|
+
reasonCode,
|
88
|
+
reasonMessage
|
89
|
+
);
|
90
|
+
},
|
91
|
+
});
|
46
92
|
break;
|
47
93
|
case "order_process_failed":
|
48
|
-
|
94
|
+
queueProcessor.queueStateChange({
|
95
|
+
minDelayInMs: 1500,
|
96
|
+
callback: async () => {
|
97
|
+
await onOrderProcessFailed(
|
98
|
+
orderState,
|
99
|
+
paymentState,
|
100
|
+
processing,
|
101
|
+
reasonCode,
|
102
|
+
reasonMessage
|
103
|
+
);
|
104
|
+
},
|
105
|
+
});
|
49
106
|
break;
|
50
107
|
case "payment_is_refunded":
|
51
|
-
|
108
|
+
queueProcessor.queueStateChange({
|
109
|
+
minDelayInMs: 1500,
|
110
|
+
callback: async () => {
|
111
|
+
await onPaymentIsRefunded(
|
112
|
+
orderState,
|
113
|
+
paymentState,
|
114
|
+
processing,
|
115
|
+
reasonCode,
|
116
|
+
reasonMessage
|
117
|
+
);
|
118
|
+
},
|
119
|
+
});
|
52
120
|
break;
|
53
121
|
case "payment_process_failed":
|
54
|
-
|
122
|
+
queueProcessor.queueStateChange({
|
123
|
+
minDelayInMs: 1500,
|
124
|
+
callback: async () => {
|
125
|
+
await onPaymentProcessFailed(
|
126
|
+
orderState,
|
127
|
+
paymentState,
|
128
|
+
processing,
|
129
|
+
reasonCode,
|
130
|
+
reasonMessage
|
131
|
+
);
|
132
|
+
},
|
133
|
+
});
|
55
134
|
break;
|
56
135
|
default:
|
57
136
|
break;
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# Put :payment_processing at a higher priority in your project: config/sidekiq.yml
|
2
|
+
module Vpago
|
3
|
+
class PaymentCapturerJob < ::ApplicationUniqueJob
|
4
|
+
queue_as :payment_processing
|
5
|
+
|
6
|
+
def perform(payment_id)
|
7
|
+
payment = Spree::Payment.find(payment_id)
|
8
|
+
payment.capture! if payment.pending?
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -1,5 +1,8 @@
|
|
1
|
+
# Put :payment_processing at a higher priority in your project: config/sidekiq.yml
|
1
2
|
module Vpago
|
2
3
|
class PaymentProcessorJob < ::ApplicationUniqueJob
|
4
|
+
queue_as :payment_processing
|
5
|
+
|
3
6
|
def perform(options)
|
4
7
|
payment = Spree::Payment.find_by(number: options[:payment_number])
|
5
8
|
Vpago::PaymentProcessor.new(payment: payment).call
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Spree
|
2
|
+
class Gateway::VattanacMiniApp < PaymentMethod
|
3
|
+
|
4
|
+
def method_type
|
5
|
+
'vattanac_mini_app'
|
6
|
+
end
|
7
|
+
|
8
|
+
def payment_source_class
|
9
|
+
Spree::VpagoPaymentSource
|
10
|
+
end
|
11
|
+
|
12
|
+
# force to purchase instead of authorize
|
13
|
+
def auto_capture?
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
# override
|
19
|
+
# purchase is used when pre auth disabled
|
20
|
+
def purchase(_amount, _source, gateway_options = {})
|
21
|
+
_, payment_number = gateway_options[:order_id].split('-')
|
22
|
+
payment = Spree::Payment.find_by(number: payment_number)
|
23
|
+
|
24
|
+
params = {}
|
25
|
+
|
26
|
+
params[:payment_response] = payment.transaction_response
|
27
|
+
|
28
|
+
|
29
|
+
if payment.transaction_response["status"] == 'SUCCESS'
|
30
|
+
ActiveMerchant::Billing::Response.new(true, 'Payway Gateway: Purchased', params)
|
31
|
+
else
|
32
|
+
ActiveMerchant::Billing::Response.new(false, 'Payway Gateway: Purchasing Failed', params)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
# override
|
39
|
+
def void(_response_code, gateway_options)
|
40
|
+
_, payment_number = gateway_options[:order_id].split('-')
|
41
|
+
payment = Spree::Payment.find_by(number: payment_number)
|
42
|
+
|
43
|
+
if payment.vattanac_mini_app_payment?
|
44
|
+
params = {}
|
45
|
+
success, params[:refund_response] = vattanac_mini_app_refund(payment)
|
46
|
+
|
47
|
+
if success
|
48
|
+
ActiveMerchant::Billing::Response.new(true, 'Payway Gateway: successfully canceled.', params)
|
49
|
+
else
|
50
|
+
ActiveMerchant::Billing::Response.new(false, 'Payway Gateway: Failed to canceleed', params)
|
51
|
+
end
|
52
|
+
else
|
53
|
+
ActiveMerchant::Billing::Response.new(true, 'Payway Gateway: Payment has been voided.')
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
def vattanac_mini_app_refund(payment)
|
59
|
+
|
60
|
+
refund_issuer = Vpago::VattanacMiniApp::RefundIssuer.new(payment, {})
|
61
|
+
refund_issuer.call
|
62
|
+
|
63
|
+
[refund_issuer.success?, refund_issuer.response]
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
def cancel(_response_code)
|
68
|
+
# we can use this to send request to payment gateway api to cancel the payment ( void )
|
69
|
+
# currently Payway does not support to cancel the gateway
|
70
|
+
|
71
|
+
# in our case don't do anything
|
72
|
+
ActiveMerchant::Billing::Response.new(true, 'Vattanc order has been cancelled.')
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -48,7 +48,9 @@ module Vpago
|
|
48
48
|
|
49
49
|
# override
|
50
50
|
def available_payment_methods(store = nil)
|
51
|
-
payment_methods = if
|
51
|
+
payment_methods = if respond_to?(:tenant) && tenant.present?
|
52
|
+
tenant_payment_methods
|
53
|
+
elsif vendor_payment_methods.any?
|
52
54
|
vendor_payment_methods
|
53
55
|
else
|
54
56
|
collect_payment_methods(store)
|
@@ -61,6 +63,10 @@ module Vpago
|
|
61
63
|
end
|
62
64
|
end
|
63
65
|
|
66
|
+
def tenant_payment_methods
|
67
|
+
tenant.tenant_payment_methods
|
68
|
+
end
|
69
|
+
|
64
70
|
def line_items_count
|
65
71
|
line_items.size
|
66
72
|
end
|
@@ -72,6 +78,12 @@ module Vpago
|
|
72
78
|
def order_adjustment_total
|
73
79
|
adjustments.eligible.sum(:amount)
|
74
80
|
end
|
81
|
+
|
82
|
+
# override this method if you want flexibility
|
83
|
+
# for example, host per payment method or tenant
|
84
|
+
def payment_host
|
85
|
+
ENV.fetch('DEFAULT_URL_HOST')
|
86
|
+
end
|
75
87
|
end
|
76
88
|
end
|
77
89
|
|
@@ -11,13 +11,30 @@ module Vpago
|
|
11
11
|
to: :url_constructor
|
12
12
|
end
|
13
13
|
|
14
|
+
# override:
|
15
|
+
# to give payment another chance to re-process, even if it failed.
|
14
16
|
def process!
|
15
|
-
# give payment another chance to re-process, even if it failed.
|
16
17
|
update!(state: :checkout) if processing? || send(:has_invalid_state?)
|
17
18
|
|
18
19
|
super
|
19
20
|
end
|
20
21
|
|
22
|
+
# override:
|
23
|
+
# to allow capture faraday connection error. gateway_error method already write rails log for this.
|
24
|
+
def protect_from_connection_error
|
25
|
+
yield
|
26
|
+
rescue ActiveMerchant::ConnectionError => e
|
27
|
+
failure!
|
28
|
+
gateway_error(e)
|
29
|
+
rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
|
30
|
+
failure!
|
31
|
+
gateway_error(ActiveMerchant::ConnectionError.new(e.message, e))
|
32
|
+
end
|
33
|
+
|
34
|
+
def user_informer
|
35
|
+
@user_informer ||= ::Vpago::UserInformers::Firebase.new(order)
|
36
|
+
end
|
37
|
+
|
21
38
|
def url_constructor
|
22
39
|
@url_constructor ||= Vpago::PaymentUrlConstructor.new(self)
|
23
40
|
end
|
@@ -38,6 +55,11 @@ module Vpago
|
|
38
55
|
def pre_auth_cancelled?
|
39
56
|
pre_auth_status == 'CANCELLED'
|
40
57
|
end
|
58
|
+
|
59
|
+
def vattanac_mini_app_payment?
|
60
|
+
payment_method.type_vattanac_mini_app?
|
61
|
+
end
|
62
|
+
|
41
63
|
end
|
42
64
|
end
|
43
65
|
|
@@ -5,6 +5,7 @@ module Vpago
|
|
5
5
|
TYPE_WINGSDK = 'Spree::Gateway::WingSdk'.freeze
|
6
6
|
TYPE_ACLEDA = 'Spree::Gateway::Acleda'.freeze
|
7
7
|
TYPE_ACLEDA_MOBILE = 'Spree::Gateway::AcledaMobile'.freeze
|
8
|
+
TYPE_VATTANAC_MINI_APP = 'Spree::Gateway::VattanacMiniApp'.freeze
|
8
9
|
|
9
10
|
def self.prepended(base)
|
10
11
|
base.preference :icon_name, :string, default: 'cheque'
|
@@ -16,7 +17,8 @@ module Vpago
|
|
16
17
|
Spree::PaymentMethod::TYPE_PAYWAY,
|
17
18
|
Spree::PaymentMethod::TYPE_WINGSDK,
|
18
19
|
Spree::PaymentMethod::TYPE_ACLEDA,
|
19
|
-
Spree::PaymentMethod::TYPE_ACLEDA_MOBILE
|
20
|
+
Spree::PaymentMethod::TYPE_ACLEDA_MOBILE,
|
21
|
+
Spree::PaymentMethod::TYPE_VATTANAC_MINI_APP
|
20
22
|
]
|
21
23
|
end
|
22
24
|
end
|
@@ -85,6 +87,10 @@ module Vpago
|
|
85
87
|
def type_wingsdk?
|
86
88
|
type == Spree::PaymentMethod::TYPE_WINGSDK
|
87
89
|
end
|
90
|
+
|
91
|
+
def type_vattanac_mini_app?
|
92
|
+
type == Spree::PaymentMethod::TYPE_VATTANAC_MINI_APP
|
93
|
+
end
|
88
94
|
end
|
89
95
|
end
|
90
96
|
|
@@ -4,6 +4,12 @@
|
|
4
4
|
|
5
5
|
<% if params[:tab] == 'vendors' %>
|
6
6
|
<div class="alert alert-info mb-3">
|
7
|
-
<%= svg_icon name: "info-circle.svg", classes: 'mr-2', width: '16', height: '16' %>
|
7
|
+
<%= svg_icon name: "info-circle.svg", classes: 'mr-2', width: '16', height: '16' %>
|
8
|
+
Payment methods for each vendor. Once set, only those payment methods will be displayed to users.
|
9
|
+
</div>
|
10
|
+
<% elsif params[:tab] == 'tenants' %>
|
11
|
+
<div class="alert alert-info mb-3">
|
12
|
+
<%= svg_icon name: "info-circle.svg", classes: 'mr-2', width: '16', height: '16' %>
|
13
|
+
Payment methods for each tenant. Once set, only those payment methods will be displayed to users.
|
8
14
|
</div>
|
9
15
|
<% end %>
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'openssl'
|
3
|
+
|
4
|
+
module Vpago
|
5
|
+
class AesEncrypter
|
6
|
+
ALGORITHM = 'aes-256-gcm'.freeze
|
7
|
+
KEY_LENGTH = 32
|
8
|
+
IV_LENGTH = 12
|
9
|
+
TAG_LENGTH = 16
|
10
|
+
|
11
|
+
def self.encrypt(plaintext, base64_key)
|
12
|
+
key = Base64.decode64(base64_key)
|
13
|
+
validate_key!(key)
|
14
|
+
|
15
|
+
cipher = OpenSSL::Cipher.new(ALGORITHM)
|
16
|
+
cipher.encrypt
|
17
|
+
cipher.key = key[0, KEY_LENGTH]
|
18
|
+
iv = cipher.random_iv
|
19
|
+
cipher.iv = iv
|
20
|
+
|
21
|
+
ciphertext = cipher.update(plaintext) + cipher.final
|
22
|
+
tag = cipher.auth_tag
|
23
|
+
|
24
|
+
combined = iv + ciphertext + tag
|
25
|
+
Base64.strict_encode64(combined)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.decrypt(encrypted_text, base64_key)
|
29
|
+
key = Base64.decode64(base64_key)
|
30
|
+
validate_key!(key)
|
31
|
+
|
32
|
+
combined = Base64.decode64(encrypted_text)
|
33
|
+
iv = combined[0, IV_LENGTH]
|
34
|
+
tag = combined[-TAG_LENGTH..]
|
35
|
+
ciphertext = combined[IV_LENGTH...-TAG_LENGTH]
|
36
|
+
|
37
|
+
cipher = OpenSSL::Cipher.new(ALGORITHM)
|
38
|
+
cipher.decrypt
|
39
|
+
cipher.key = key[0, KEY_LENGTH]
|
40
|
+
cipher.iv = iv
|
41
|
+
cipher.auth_tag = tag
|
42
|
+
|
43
|
+
cipher.update(ciphertext) + cipher.final
|
44
|
+
rescue OpenSSL::Cipher::CipherError => e
|
45
|
+
raise "Decryption failed: #{e.message}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.validate_key!(key)
|
49
|
+
return if key.is_a?(String) && key.bytesize >= KEY_LENGTH
|
50
|
+
|
51
|
+
raise ArgumentError, "Key must be a string of at least #{KEY_LENGTH} bytes"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
|
@@ -7,6 +7,7 @@ module Vpago
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def find_and_verify
|
10
|
+
|
10
11
|
find_and_verify!
|
11
12
|
rescue StandardError, ActiveRecord::RecordNotFound => e
|
12
13
|
Rails.logger.error("PaymentJwtVerifier#find_and_verify error: #{e.class} - #{e.message}")
|
@@ -14,14 +15,29 @@ module Vpago
|
|
14
15
|
end
|
15
16
|
|
16
17
|
def find_and_verify!
|
17
|
-
order = Spree::Order.find_by!(number: params_hash[:order_number])
|
18
|
-
verify_jwt!(order)
|
19
18
|
|
20
|
-
|
19
|
+
if vattanac_mini_app_payload?
|
20
|
+
payload = Vpago::VattanacMiniAppDataHandler.new.decrypt_data(@params_hash[:data])
|
21
|
+
payment = Spree::Payment.find_by!(number: payload['paymentId'])
|
22
|
+
payment.update(transaction_response: payload)
|
23
|
+
payment
|
24
|
+
else
|
25
|
+
order = Spree::Order.find_by!(number: params_hash[:order_number])
|
26
|
+
verify_jwt!(order)
|
27
|
+
|
28
|
+
Spree::Payment.find_by!(number: params_hash[:payment_number])
|
29
|
+
end
|
21
30
|
end
|
22
31
|
|
23
32
|
def verify_jwt!(order)
|
24
33
|
JWT.decode(params_hash[:order_jwt_token], order.token, 'HS256')
|
25
34
|
end
|
35
|
+
|
36
|
+
def vattanac_mini_app_payload?
|
37
|
+
params_hash[:data].present?
|
38
|
+
end
|
39
|
+
|
26
40
|
end
|
27
41
|
end
|
42
|
+
|
43
|
+
|