spree_vpago 2.3.4 → 2.3.5.pre.pre.pre.1
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/Gemfile.lock +1 -1
- data/app/helpers/vpago/admin/base_helper_decorator.rb +7 -0
- data/app/models/spree/gateway/acleda_v2.rb +93 -0
- data/app/models/vpago/payment_method_decorator.rb +6 -0
- data/app/services/vpago/payment_finder.rb +10 -2
- data/app/views/spree/admin/payments/source_views/_acleda_v2.html.erb +1 -0
- data/app/views/spree/checkout/payment/_acleda_v2_checkout_form.html.erb +12 -0
- data/app/views/spree/vpago_payments/forms/spree/gateway/_acleda_v2.html.erb +47 -0
- data/lib/spree_vpago/engine.rb +1 -0
- data/lib/spree_vpago/version.rb +1 -1
- data/lib/vpago/acleda_v2/base.rb +149 -0
- data/lib/vpago/acleda_v2/checkout.rb +124 -0
- data/lib/vpago/acleda_v2/transaction_status.rb +80 -0
- metadata +11 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c0262a0bad21d6ed9d53d73ba6805f307e6b9f82db837ac96fe4f3c4725ae49b
|
|
4
|
+
data.tar.gz: 71f83ae18d1c306419bbef87f291185e1835a653fde1d1f0a09e1c783cf6f54c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ffc03cf77c82129ee5cf13ca1d3abb1646a9d39878ea647e1fd2451c30c027a885d8e8cb99fe45fabef2d7308c73f70c11b654ef9043b59814ef4737a983392c
|
|
7
|
+
data.tar.gz: cde29375e88d3428cd5854b47bb597fac4697eaf60550dfb74573dd0d5edff55624ea0df78659328ba40343548c39b60d4284fe7c6bfcd495a24ba405b82fcea
|
data/Gemfile.lock
CHANGED
|
@@ -12,6 +12,7 @@ module Vpago
|
|
|
12
12
|
payway_abapay
|
|
13
13
|
payway_alipay
|
|
14
14
|
payway_wechat
|
|
15
|
+
acleda_v2
|
|
15
16
|
acleda
|
|
16
17
|
acleda_khqr
|
|
17
18
|
acleda_cards
|
|
@@ -28,6 +29,10 @@ module Vpago
|
|
|
28
29
|
%w[khqr deeplink all]
|
|
29
30
|
end
|
|
30
31
|
|
|
32
|
+
def available_acleda_v2_modes
|
|
33
|
+
Spree::Gateway::AcledaV2::MODES
|
|
34
|
+
end
|
|
35
|
+
|
|
31
36
|
def preference_field_for(form, field, options)
|
|
32
37
|
case field
|
|
33
38
|
when 'preferred_acleda_type'
|
|
@@ -45,6 +50,8 @@ module Vpago
|
|
|
45
50
|
return form.select(:preferred_abapay_khqr_deeplink_option, Spree::Gateway::PaywayV2::ABAPAY_KHQR_DEEPLINK_OPTIONS, {}, class: 'fullwidth select2')
|
|
46
51
|
when 'preferred_vattanac_payment_option'
|
|
47
52
|
return form.select(:preferred_vattanac_payment_option, available_vattanac_payment_options, {}, class: 'fullwidth select2')
|
|
53
|
+
when 'preferred_acleda_v2_mode'
|
|
54
|
+
return form.select(:preferred_acleda_v2_mode, available_acleda_v2_modes, {}, class: 'fullwidth select2')
|
|
48
55
|
end
|
|
49
56
|
super
|
|
50
57
|
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class Gateway::AcledaV2 < PaymentMethod
|
|
3
|
+
MODES = %w[khqr card deeplink].freeze
|
|
4
|
+
|
|
5
|
+
# BOOKMEPLUS — sent as the "merchantName" field in getTxnStatus requests
|
|
6
|
+
preference :merchant_name, :string
|
|
7
|
+
# e.g. https://epaymentuat.acledabank.com.kh/BOOKMEPLUS/XPAYConnectorServiceInterfaceImplV2/XPAYConnectorServiceInterfaceImplV2RS
|
|
8
|
+
preference :xpay_service_base_url, :string
|
|
9
|
+
# e.g. https://epaymentuat.acledabank.com.kh/BOOKMEPLUS/paymentPage.jsp
|
|
10
|
+
preference :payment_page_url, :string
|
|
11
|
+
preference :merchant_id, :string
|
|
12
|
+
preference :login_id, :string
|
|
13
|
+
preference :password, :string
|
|
14
|
+
preference :secret, :string # HMAC-SHA512 key
|
|
15
|
+
preference :payment_expiry_time_in_mn, :integer, default: 5
|
|
16
|
+
preference :acleda_v2_mode, :string, default: 'khqr'
|
|
17
|
+
|
|
18
|
+
validates :preferred_acleda_v2_mode, inclusion: { in: MODES }
|
|
19
|
+
|
|
20
|
+
def khqr?
|
|
21
|
+
preferred_acleda_v2_mode == 'khqr'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def card?
|
|
25
|
+
preferred_acleda_v2_mode == 'card'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def deeplink?
|
|
29
|
+
preferred_acleda_v2_mode == 'deeplink'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# KHQR (Scan with Bank under Bakong) = "0"; Credit/debit card = "1"
|
|
33
|
+
def payment_card_value
|
|
34
|
+
card? ? '1' : '0'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# override: partial to render in admin
|
|
38
|
+
def method_type
|
|
39
|
+
'acleda_v2'
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# override
|
|
43
|
+
def payment_source_class
|
|
44
|
+
Spree::VpagoPaymentSource
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# override
|
|
48
|
+
def payment_profiles_supported?
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# override
|
|
53
|
+
def available_for_order?(_order)
|
|
54
|
+
true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# override: process! -> purchase! immediately (no pre-auth for ACLEDA V2)
|
|
58
|
+
def auto_capture?
|
|
59
|
+
true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# override
|
|
63
|
+
# mirror PayWay V2: gateway_options[:order_id] == "<order_number>-<payment_number>"
|
|
64
|
+
def purchase(_amount, _source, gateway_options = {})
|
|
65
|
+
_, payment_number = gateway_options[:order_id].split('-')
|
|
66
|
+
payment = Spree::Payment.find_by(number: payment_number)
|
|
67
|
+
|
|
68
|
+
checker = check_transaction(payment)
|
|
69
|
+
payment.update(transaction_response: checker.json_response)
|
|
70
|
+
|
|
71
|
+
params = { check: { response: checker.json_response } }
|
|
72
|
+
|
|
73
|
+
if checker.success?
|
|
74
|
+
ActiveMerchant::Billing::Response.new(true, 'Acleda V2: Purchased', params)
|
|
75
|
+
else
|
|
76
|
+
ActiveMerchant::Billing::Response.new(false, 'Acleda V2: Purchasing Failed', params)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Defining check_transaction makes support_check_transaction_api? true,
|
|
81
|
+
# enabling the /vpago_payments/check_transaction poller.
|
|
82
|
+
def check_transaction(payment)
|
|
83
|
+
checker = Vpago::AcledaV2::TransactionStatus.new(payment)
|
|
84
|
+
checker.call
|
|
85
|
+
checker
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# override
|
|
89
|
+
def cancel(_response_code, _payment = nil)
|
|
90
|
+
ActiveMerchant::Billing::Response.new(true, 'Acleda V2: Payment has been canceled.')
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -4,6 +4,7 @@ module Vpago
|
|
|
4
4
|
TYPE_PAYWAY_V2 = 'Spree::Gateway::PaywayV2'.freeze
|
|
5
5
|
TYPE_WINGSDK = 'Spree::Gateway::WingSdk'.freeze
|
|
6
6
|
TYPE_ACLEDA = 'Spree::Gateway::Acleda'.freeze
|
|
7
|
+
TYPE_ACLEDA_V2 = 'Spree::Gateway::AcledaV2'.freeze
|
|
7
8
|
TYPE_ACLEDA_MOBILE = 'Spree::Gateway::AcledaMobile'.freeze
|
|
8
9
|
TYPE_TRUE_MONEY = 'Spree::Gateway::TrueMoney'.freeze
|
|
9
10
|
TYPE_VATTANAC = 'Spree::Gateway::Vattanac'.freeze
|
|
@@ -21,6 +22,7 @@ module Vpago
|
|
|
21
22
|
Spree::PaymentMethod::TYPE_PAYWAY,
|
|
22
23
|
Spree::PaymentMethod::TYPE_WINGSDK,
|
|
23
24
|
Spree::PaymentMethod::TYPE_ACLEDA,
|
|
25
|
+
Spree::PaymentMethod::TYPE_ACLEDA_V2,
|
|
24
26
|
Spree::PaymentMethod::TYPE_ACLEDA_MOBILE,
|
|
25
27
|
Spree::PaymentMethod::TYPE_VATTANAC,
|
|
26
28
|
Spree::PaymentMethod::TYPE_VATTANAC_MINI_APP,
|
|
@@ -89,6 +91,10 @@ module Vpago
|
|
|
89
91
|
type == Spree::PaymentMethod::TYPE_ACLEDA
|
|
90
92
|
end
|
|
91
93
|
|
|
94
|
+
def type_acleda_v2?
|
|
95
|
+
type == Spree::PaymentMethod::TYPE_ACLEDA_V2
|
|
96
|
+
end
|
|
97
|
+
|
|
92
98
|
def type_payway?
|
|
93
99
|
type == Spree::PaymentMethod::TYPE_PAYWAY
|
|
94
100
|
end
|
|
@@ -14,11 +14,15 @@ module Vpago
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def find_and_verify!
|
|
17
|
-
if
|
|
17
|
+
if vattanac_mini_app_callback?
|
|
18
18
|
payload = Vpago::VattanacMiniAppDataHandler.new.decrypt_data(@params_hash[:data])
|
|
19
19
|
payment = Spree::Payment.find_by!(number: payload['paymentId'])
|
|
20
20
|
payment.update(transaction_response: payload)
|
|
21
21
|
payment
|
|
22
|
+
elsif acleda_v2_callback?
|
|
23
|
+
# ACLEDA V2's static portal callback carries only _paymenttokenid, which we
|
|
24
|
+
# stored on the payment source at openSessionV2. Resolve the payment by it.
|
|
25
|
+
Spree::VpagoPaymentSource.find_by!(transaction_id: params_hash[:_paymenttokenid]).payment
|
|
22
26
|
else
|
|
23
27
|
order = Spree::Order.find_by!(number: params_hash[:order_number])
|
|
24
28
|
verify_jwt!(order)
|
|
@@ -31,8 +35,12 @@ module Vpago
|
|
|
31
35
|
JWT.decode(params_hash[:order_jwt_token], order.token, 'HS256')
|
|
32
36
|
end
|
|
33
37
|
|
|
34
|
-
def
|
|
38
|
+
def vattanac_mini_app_callback?
|
|
35
39
|
params_hash[:data].present?
|
|
36
40
|
end
|
|
41
|
+
|
|
42
|
+
def acleda_v2_callback?
|
|
43
|
+
params_hash[:_paymenttokenid].present?
|
|
44
|
+
end
|
|
37
45
|
end
|
|
38
46
|
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= render 'spree/admin/payments/source_views/vpago_payment_tmpl', payment: payment %>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<form method="post" action="<%= action_url %>" id="acleda_v2_checkout_form" style="display:none;">
|
|
2
|
+
<% gateway_params.each do |name, value| %>
|
|
3
|
+
<input type="hidden" name="<%= name %>" value="<%= value %>">
|
|
4
|
+
<% end %>
|
|
5
|
+
</form>
|
|
6
|
+
|
|
7
|
+
<script>
|
|
8
|
+
(function () {
|
|
9
|
+
var form = document.getElementById("acleda_v2_checkout_form");
|
|
10
|
+
if (form) form.submit();
|
|
11
|
+
})();
|
|
12
|
+
</script>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<%
|
|
2
|
+
opr_device = request.user_agent.to_s.downcase.match?(/iphone|ipad|ipod/) ? 'ios' : 'android'
|
|
3
|
+
checkout = ::Vpago::AcledaV2::Checkout.new(@payment, { platform: params[:platform], opr_device: opr_device })
|
|
4
|
+
|
|
5
|
+
begin
|
|
6
|
+
checkout.call
|
|
7
|
+
rescue => e
|
|
8
|
+
Rails.logger.error("Acleda V2 checkout.call failed: #{e.message}")
|
|
9
|
+
end
|
|
10
|
+
%>
|
|
11
|
+
|
|
12
|
+
<% if checkout.failed? %>
|
|
13
|
+
<script>
|
|
14
|
+
console.error("[Vpago] Acleda V2 checkout error: <%= j checkout.error_message.to_s %>");
|
|
15
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
16
|
+
const loadingContainer = document.getElementById('vpago_loading_container');
|
|
17
|
+
if (loadingContainer) {
|
|
18
|
+
loadingContainer.textContent = "We couldn't start your payment. Please go back and try again.";
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
</script>
|
|
22
|
+
<% elsif @payment.payment_method.deeplink? %>
|
|
23
|
+
<script>
|
|
24
|
+
function showRedirectContainer(buttonLabel, onClick) {
|
|
25
|
+
const loadingContainer = document.getElementById('vpago_loading_container');
|
|
26
|
+
const redirectContainer = document.getElementById('vpago_redirect_container');
|
|
27
|
+
const redirectButton = document.getElementById('vpago_redirect_button');
|
|
28
|
+
|
|
29
|
+
redirectButton.textContent = buttonLabel;
|
|
30
|
+
redirectButton.addEventListener('click', onClick);
|
|
31
|
+
|
|
32
|
+
loadingContainer.style.display = 'none';
|
|
33
|
+
redirectContainer.style.removeProperty('display');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const deeplinkUrl = "<%= raw checkout.deeplink_url %>";
|
|
37
|
+
const redirectTo = (url) => { if (url) window.location.href = url; };
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
redirectTo(deeplinkUrl);
|
|
41
|
+
showRedirectContainer("Open ACLEDA", () => redirectTo(deeplinkUrl));
|
|
42
|
+
</script>
|
|
43
|
+
<% else %>
|
|
44
|
+
<%= render 'spree/checkout/payment/acleda_v2_checkout_form',
|
|
45
|
+
gateway_params: checkout.gateway_params,
|
|
46
|
+
action_url: checkout.action_url %>
|
|
47
|
+
<% end %>
|
data/lib/spree_vpago/engine.rb
CHANGED
data/lib/spree_vpago/version.rb
CHANGED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
module Vpago
|
|
2
|
+
module AcledaV2
|
|
3
|
+
class Base
|
|
4
|
+
CONTENT_TYPE_JSON = 'application/json'.freeze
|
|
5
|
+
# ACLEDA requires purchaseDesc to be <= 16 chars, English only.
|
|
6
|
+
PURCHASE_DESC = 'Purchase goods'.freeze
|
|
7
|
+
|
|
8
|
+
def initialize(payment, options = {})
|
|
9
|
+
@options = options
|
|
10
|
+
@payment = payment
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def payment_method
|
|
14
|
+
@payment.payment_method
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def payment_number
|
|
18
|
+
@payment.number
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# PDF samples quote every xpayTransaction field as a JSON string.
|
|
22
|
+
def amount
|
|
23
|
+
format('%.2f', @payment.amount)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# txid <= 16 chars; payment.number fits.
|
|
27
|
+
alias transaction_id payment_number
|
|
28
|
+
|
|
29
|
+
def order_number
|
|
30
|
+
@payment.order.number
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def merchant_name
|
|
34
|
+
payment_method.preferences[:merchant_name]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def merchant_id
|
|
38
|
+
payment_method.preferences[:merchant_id]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def login_id
|
|
42
|
+
payment_method.preferences[:login_id]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def password
|
|
46
|
+
payment_method.preferences[:password]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def secret
|
|
50
|
+
payment_method.preferences[:secret]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# PDF samples quote every xpayTransaction field as a JSON string, even ones
|
|
54
|
+
# labeled "Numeric" in the field tables (paymentCard, quantity, expiryTime).
|
|
55
|
+
def expiry_time
|
|
56
|
+
payment_method.preferences[:payment_expiry_time_in_mn].to_s
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# PDF: "Date of Transaction in UTC Time", sent as a YYYY-MM-DD string in the
|
|
60
|
+
# samples. Explicit .utc so this stays correct even if config.time_zone is
|
|
61
|
+
# ever changed away from UTC.
|
|
62
|
+
def purchase_date
|
|
63
|
+
@payment.created_at.utc.strftime('%Y-%m-%d')
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def purchase_currency
|
|
67
|
+
@payment.order.currency
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# --- Endpoints (each stored as a full URL preference) ---
|
|
71
|
+
# Strip embedded whitespace: these URLs are long and wrap across lines in
|
|
72
|
+
# the bank's PDFs, so a copy-paste into the admin preference easily carries
|
|
73
|
+
# over a stray space/line-break where the PDF wrapped.
|
|
74
|
+
|
|
75
|
+
def xpay_service_base_url
|
|
76
|
+
strip_whitespace(payment_method.preferences[:xpay_service_base_url])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def open_session_url
|
|
80
|
+
"#{xpay_service_base_url}/openSessionV2"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def get_txn_status_url # rubocop:disable Naming/AccessorMethodName
|
|
84
|
+
"#{xpay_service_base_url}/getTxnStatus"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# KHQR/Card secure web redirect target
|
|
88
|
+
def action_url
|
|
89
|
+
strip_whitespace(payment_method.preferences[:payment_page_url])
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# successUrlToReturn / errorUrl. The bank appends its own query string
|
|
93
|
+
# (_paymenttokenid, _resultCode, ...) when it redirects back.
|
|
94
|
+
def continue_success_url
|
|
95
|
+
@payment.processing_url
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def callback_url
|
|
99
|
+
if platform == 'app' && app_scheme.present?
|
|
100
|
+
uri = URI.parse(@payment.processing_url)
|
|
101
|
+
uri.scheme = app_scheme
|
|
102
|
+
uri.to_s
|
|
103
|
+
else
|
|
104
|
+
@payment.processing_url
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def app_scheme
|
|
109
|
+
payment_method.preferences[:app_scheme]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# android | ios (defaults to android when not supplied by the caller)
|
|
113
|
+
def opr_device
|
|
114
|
+
@options[:opr_device].presence || 'android'
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def platform
|
|
118
|
+
@options[:platform]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# --- HMAC-SHA512 (uppercase hex) signing ---
|
|
122
|
+
# openSessionV2 message: merchantID + loginId + password + txid
|
|
123
|
+
def open_session_hash
|
|
124
|
+
hmac512("#{merchant_id}#{login_id}#{password}#{payment_number}")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# getTxnStatus message: merchantId + loginId + password + paymentTokenid
|
|
128
|
+
def txn_status_hash(token)
|
|
129
|
+
hmac512("#{merchant_id}#{login_id}#{password}#{token}")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def hmac512(message)
|
|
133
|
+
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha512'), secret, message).upcase
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def parse_json(body)
|
|
137
|
+
JSON.parse(body)
|
|
138
|
+
rescue JSON::ParserError
|
|
139
|
+
{}
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
def strip_whitespace(value)
|
|
145
|
+
value.to_s.gsub(/\s+/, '')
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
require 'faraday'
|
|
2
|
+
|
|
3
|
+
module Vpago
|
|
4
|
+
module AcledaV2
|
|
5
|
+
# Single class handling all three modes (khqr, card, deeplink).
|
|
6
|
+
# Two-step ACLEDA flow: openSessionV2 (JSON, here) yields paymentTokenid +
|
|
7
|
+
# sessionid, then either an auto-POST form to paymentPage.jsp (KHQR/Card) or
|
|
8
|
+
# a JS deeplink redirect (Deep Link).
|
|
9
|
+
class Checkout < Base
|
|
10
|
+
attr_reader :error_message
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
@error_message = nil
|
|
14
|
+
@response = open_session
|
|
15
|
+
|
|
16
|
+
if @response.status != 500 && @response.status != 200
|
|
17
|
+
@error_message = 'Server Error'
|
|
18
|
+
elsif @response.status == 500 # mostly when passing a wrong parameter name
|
|
19
|
+
@error_message = @response.body
|
|
20
|
+
elsif json_response.dig('result', 'code') != 0 # invalid payload, wrong loginId, etc.
|
|
21
|
+
@error_message = json_response.dig('result', 'errorDetails') || 'Unknown Error'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
update_payment_source
|
|
25
|
+
|
|
26
|
+
self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def failed?
|
|
30
|
+
@error_message.present?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def json_response
|
|
34
|
+
@json_response ||= parse_json(@response.body)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# KHQR/Card secure web redirect form fields (auto-POSTed to paymentPage.jsp).
|
|
38
|
+
def gateway_params
|
|
39
|
+
return {} if failed?
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
merchantID: merchant_id,
|
|
43
|
+
sessionid: json_response.dig('result', 'sessionid'),
|
|
44
|
+
paymenttokenid: payment_token_id,
|
|
45
|
+
description: Base::PURCHASE_DESC,
|
|
46
|
+
expirytime: expiry_time,
|
|
47
|
+
amount: amount,
|
|
48
|
+
quantity: '1',
|
|
49
|
+
item: '1',
|
|
50
|
+
invoiceid: order_number,
|
|
51
|
+
currencytype: purchase_currency,
|
|
52
|
+
transactionID: payment_number,
|
|
53
|
+
successUrlToReturn: continue_success_url,
|
|
54
|
+
errorUrl: continue_success_url
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Deep Link redirect target.
|
|
59
|
+
def deeplink_url
|
|
60
|
+
return nil if failed?
|
|
61
|
+
|
|
62
|
+
json_response.dig('result', 'deeplinkUrl')
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def open_session
|
|
68
|
+
conn = Faraday::Connection.new
|
|
69
|
+
|
|
70
|
+
conn.post(open_session_url) do |request|
|
|
71
|
+
request.headers['Content-Type'] = Base::CONTENT_TYPE_JSON
|
|
72
|
+
request.body = open_session_payload.to_json
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def open_session_payload
|
|
77
|
+
base = {
|
|
78
|
+
password: password,
|
|
79
|
+
loginId: login_id,
|
|
80
|
+
merchantID: merchant_id,
|
|
81
|
+
hash: open_session_hash,
|
|
82
|
+
xpayTransaction: xpay_transaction_base
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if payment_method.deeplink?
|
|
86
|
+
base[:xpayTransaction].merge!(oprDevice: opr_device, callBackUrl: callback_url)
|
|
87
|
+
else
|
|
88
|
+
base[:xpayTransaction][:paymentCard] = payment_method.payment_card_value
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
base
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def xpay_transaction_base
|
|
95
|
+
{
|
|
96
|
+
txid: payment_number,
|
|
97
|
+
invoiceid: order_number,
|
|
98
|
+
purchaseAmount: amount,
|
|
99
|
+
purchaseCurrency: purchase_currency,
|
|
100
|
+
purchaseDate: purchase_date,
|
|
101
|
+
purchaseDesc: Base::PURCHASE_DESC,
|
|
102
|
+
item: '1',
|
|
103
|
+
quantity: '1',
|
|
104
|
+
expiryTime: expiry_time
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def payment_token_id
|
|
109
|
+
json_response.dig('result', 'xTran', 'paymentTokenid')
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Persist the paymentTokenid so check_transaction / the bank callback can
|
|
113
|
+
# resolve the payment later. Wrap only the write in the writing role, since
|
|
114
|
+
# the checkout form partial runs on a GET request (replica connection).
|
|
115
|
+
def update_payment_source
|
|
116
|
+
return if failed?
|
|
117
|
+
|
|
118
|
+
ActiveRecord::Base.connected_to(role: :writing) do
|
|
119
|
+
@payment.source.update_column(:transaction_id, payment_token_id)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
require 'faraday'
|
|
2
|
+
|
|
3
|
+
module Vpago
|
|
4
|
+
module AcledaV2
|
|
5
|
+
class TransactionStatus < Base
|
|
6
|
+
# Terminal failure indicators in result.errorDetails. The PDFs only document
|
|
7
|
+
# code 0 / "SUCCESS"; decline/expiry wording is conservative and should be
|
|
8
|
+
# confirmed against real responses during UAT. Anything not success and not
|
|
9
|
+
# terminal is treated as pending so the poller keeps trying.
|
|
10
|
+
FAILED_ERROR_DETAILS = %w[EXPIRED DECLINED CANCELLED FAILED REJECTED].freeze
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
@response = check_remote_status
|
|
14
|
+
self
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def json_response
|
|
18
|
+
@json_response ||= parse_json(@response.body)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def result
|
|
22
|
+
json_response['result'] || {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def result_code
|
|
26
|
+
result['code']
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def error_message
|
|
30
|
+
result['errorDetails']
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def success?
|
|
34
|
+
http_success? && result_code == 0 && error_message == 'SUCCESS'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def failed?
|
|
38
|
+
return false unless http_success?
|
|
39
|
+
|
|
40
|
+
FAILED_ERROR_DETAILS.include?(error_message.to_s.upcase)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def pending?
|
|
44
|
+
!success? && !failed?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def http_success?
|
|
50
|
+
@response&.status == 200
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def token
|
|
54
|
+
@payment.source&.transaction_id
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def check_remote_status
|
|
58
|
+
conn = Faraday::Connection.new
|
|
59
|
+
|
|
60
|
+
conn.post(get_txn_status_url) do |request|
|
|
61
|
+
request.headers['Content-Type'] = Base::CONTENT_TYPE_JSON
|
|
62
|
+
request.body = payload.to_json
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# NOTE: getTxnStatus uses merchantId (lowercase d) and an extra merchantName
|
|
67
|
+
# field. merchantName is NOT part of the HMAC message.
|
|
68
|
+
def payload
|
|
69
|
+
{
|
|
70
|
+
loginId: login_id,
|
|
71
|
+
password: password,
|
|
72
|
+
merchantId: merchant_id,
|
|
73
|
+
merchantName: merchant_name,
|
|
74
|
+
hash: txn_status_hash(token),
|
|
75
|
+
paymentTokenid: token
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree_vpago
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.3.
|
|
4
|
+
version: 2.3.5.pre.pre.pre.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- You
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -192,6 +192,7 @@ files:
|
|
|
192
192
|
- app/models/concerns/vpago/payoutable.rb
|
|
193
193
|
- app/models/spree/gateway/acleda.rb
|
|
194
194
|
- app/models/spree/gateway/acleda_mobile.rb
|
|
195
|
+
- app/models/spree/gateway/acleda_v2.rb
|
|
195
196
|
- app/models/spree/gateway/payway.rb
|
|
196
197
|
- app/models/spree/gateway/payway_v2.rb
|
|
197
198
|
- app/models/spree/gateway/true_money.rb
|
|
@@ -280,6 +281,7 @@ files:
|
|
|
280
281
|
- app/views/spree/admin/payments/source_forms/_payway_v2.html.erb
|
|
281
282
|
- app/views/spree/admin/payments/source_views/_acleda.html.erb
|
|
282
283
|
- app/views/spree/admin/payments/source_views/_acleda_mobile.html.erb
|
|
284
|
+
- app/views/spree/admin/payments/source_views/_acleda_v2.html.erb
|
|
283
285
|
- app/views/spree/admin/payments/source_views/_payment_payway.html.erb
|
|
284
286
|
- app/views/spree/admin/payments/source_views/_payway_v2.html.erb
|
|
285
287
|
- app/views/spree/admin/payments/source_views/_true_money.html.erb
|
|
@@ -309,6 +311,7 @@ files:
|
|
|
309
311
|
- app/views/spree/checkout/payment/_acleda.html.erb
|
|
310
312
|
- app/views/spree/checkout/payment/_acleda_checkout_form.html.erb
|
|
311
313
|
- app/views/spree/checkout/payment/_acleda_mobile.html.erb
|
|
314
|
+
- app/views/spree/checkout/payment/_acleda_v2_checkout_form.html.erb
|
|
312
315
|
- app/views/spree/checkout/payment/_payment_payway.html.erb
|
|
313
316
|
- app/views/spree/checkout/payment/_payway_loader.html.erb
|
|
314
317
|
- app/views/spree/checkout/payment/_payway_root.html.erb
|
|
@@ -331,6 +334,7 @@ files:
|
|
|
331
334
|
- app/views/spree/payway_v2_card_popups/show.html.erb
|
|
332
335
|
- app/views/spree/vpago_payments/_transaction_checker.html.erb
|
|
333
336
|
- app/views/spree/vpago_payments/checkout.html.erb
|
|
337
|
+
- app/views/spree/vpago_payments/forms/spree/gateway/_acleda_v2.html.erb
|
|
334
338
|
- app/views/spree/vpago_payments/forms/spree/gateway/_payway_v2.html.erb
|
|
335
339
|
- app/views/spree/vpago_payments/forms/spree/gateway/_true_money.html.erb
|
|
336
340
|
- app/views/spree/vpago_payments/forms/spree/gateway/_vattanac.html.erb
|
|
@@ -388,6 +392,9 @@ files:
|
|
|
388
392
|
- lib/vpago/acleda_mobile/payment_request_updater.rb
|
|
389
393
|
- lib/vpago/acleda_mobile/payment_retriever.rb
|
|
390
394
|
- lib/vpago/acleda_mobile/transaction_status.rb
|
|
395
|
+
- lib/vpago/acleda_v2/base.rb
|
|
396
|
+
- lib/vpago/acleda_v2/checkout.rb
|
|
397
|
+
- lib/vpago/acleda_v2/transaction_status.rb
|
|
391
398
|
- lib/vpago/payment_amount_calculator.rb
|
|
392
399
|
- lib/vpago/payment_request_updater.rb
|
|
393
400
|
- lib/vpago/payment_status_marker.rb
|
|
@@ -443,9 +450,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
443
450
|
version: 3.2.0
|
|
444
451
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
445
452
|
requirements:
|
|
446
|
-
- - "
|
|
453
|
+
- - ">"
|
|
447
454
|
- !ruby/object:Gem::Version
|
|
448
|
-
version:
|
|
455
|
+
version: 1.3.1
|
|
449
456
|
requirements:
|
|
450
457
|
- none
|
|
451
458
|
rubygems_version: 3.4.1
|