spree_vpago 2.3.5.pre.pre.pre.8 → 2.3.6
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/controllers/spree/admin/payments_controller_decorator.rb +47 -1
- data/app/controllers/spree/admin/suspicious_orders_controller.rb +64 -0
- data/app/helpers/vpago/admin/base_helper_decorator.rb +0 -7
- data/app/models/vpago/order_decorator.rb +6 -13
- data/app/models/vpago/payment_decorator.rb +4 -2
- data/app/models/vpago/payment_method_decorator.rb +1 -7
- data/app/overrides/spree/admin/payments/_form/admin_payment_form_fields.html.erb.deface +36 -0
- data/app/overrides/spree/admin/payments/_list/actions.html.erb.deface +3 -1
- data/app/overrides/spree/admin/shared/sub_menu/_orders/suspicious_orders.html.erb.deface +3 -0
- data/app/services/vpago/payment_finder.rb +2 -10
- data/app/views/spree/admin/suspicious_orders/_payments_table.html.erb +71 -0
- data/app/views/spree/admin/suspicious_orders/index.html.erb +112 -0
- data/app/views/spree/admin/suspicious_orders/payments.html.erb +3 -0
- data/config/locales/en.yml +11 -0
- data/config/locales/km.yml +11 -0
- data/config/routes.rb +7 -0
- data/lib/spree_vpago/engine.rb +0 -1
- data/lib/spree_vpago/version.rb +1 -1
- data/lib/vpago/acleda/checkout.rb +1 -1
- metadata +10 -11
- data/app/models/spree/gateway/acleda_v2.rb +0 -89
- data/app/views/spree/admin/payments/source_views/_acleda_v2.html.erb +0 -5
- data/app/views/spree/checkout/payment/_acleda_v2_checkout_form.html.erb +0 -12
- data/app/views/spree/vpago_payments/forms/spree/gateway/_acleda_v2.html.erb +0 -48
- data/lib/vpago/acleda_v2/base.rb +0 -158
- data/lib/vpago/acleda_v2/checkout.rb +0 -120
- data/lib/vpago/acleda_v2/transaction_status.rb +0 -83
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 92861b027f31b9c4239298967116f3cb34106575da30b650c7220a7981341a23
|
|
4
|
+
data.tar.gz: 9a43d50434fb8e0eea79e9413219d62bdd10f32b7599534f2702b0143da61c67
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 791e046282f93cb5bbaf1683e788a9e440c394e5101f88fdb48431a517cbcf0367c1b5c49536577f438a31dc30fa37040bcad464b2ec617b30e3c4e2dcbbe6f9
|
|
7
|
+
data.tar.gz: e31a474f731d18f6a65628bf8f901ed4fd73407a289fc354687ececedb3e0a4f45ab13933d70fba344085e03161b74d139155185db6826a5794f66fab62a0566
|
data/Gemfile.lock
CHANGED
|
@@ -9,9 +9,55 @@ module Spree
|
|
|
9
9
|
payment_method ||= Spree::PaymentMethod.find(params[:payment][:payment_method_id])
|
|
10
10
|
return unless payment_method.vpago_payment?
|
|
11
11
|
|
|
12
|
-
source_params = { payment_option: payment_method.preferred_payment_option }
|
|
12
|
+
source_params = { payment_option: payment_method.try(:preferred_payment_option) }
|
|
13
13
|
params[:payment][:source_attributes] = source_params
|
|
14
14
|
end
|
|
15
|
+
|
|
16
|
+
# override
|
|
17
|
+
# For vpago gateways created from the admin backend, we skip immediate processing
|
|
18
|
+
# and keep the payment in `checkout` state so the admin can open the QR checkout
|
|
19
|
+
# URL and pay on behalf of the customer. The bank webhook
|
|
20
|
+
# (process_payment -> Vpago::PaymentProcessorJob) completes the order once paid.
|
|
21
|
+
def create
|
|
22
|
+
return super unless @payment_method&.vpago_payment?
|
|
23
|
+
|
|
24
|
+
invoke_callbacks(:create, :before)
|
|
25
|
+
|
|
26
|
+
@payment = @order.payments.build(object_params)
|
|
27
|
+
|
|
28
|
+
if @payment.save
|
|
29
|
+
invoke_callbacks(:create, :after)
|
|
30
|
+
flash[:success] = flash_message_for(@payment, :successfully_created)
|
|
31
|
+
redirect_to spree.admin_order_payments_path(@order)
|
|
32
|
+
else
|
|
33
|
+
invoke_callbacks(:create, :fails)
|
|
34
|
+
flash[:error] = Spree.t(:payment_could_not_be_created)
|
|
35
|
+
render :new, status: :unprocessable_entity
|
|
36
|
+
end
|
|
37
|
+
rescue Spree::Core::GatewayError => e
|
|
38
|
+
invoke_callbacks(:create, :fails)
|
|
39
|
+
flash[:error] = e.message.to_s
|
|
40
|
+
redirect_to spree.new_admin_order_payment_path(@order)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# override
|
|
44
|
+
# Before the normal `process!` re-runs the gateway, reset a stuck vpago
|
|
45
|
+
# payment back to `checkout`. Admin reprocess only — the automatic webhook
|
|
46
|
+
# path is untouched (it auto-resets `failed` alone).
|
|
47
|
+
def fire
|
|
48
|
+
reset_stuck_payment_for_reprocess!
|
|
49
|
+
super
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def reset_stuck_payment_for_reprocess!
|
|
55
|
+
return unless params[:e] == 'process'
|
|
56
|
+
return unless @payment&.payment_method&.vpago_payment?
|
|
57
|
+
return unless @payment.can_reset_for_retry?
|
|
58
|
+
|
|
59
|
+
@payment.reset_for_retry!
|
|
60
|
+
end
|
|
15
61
|
end
|
|
16
62
|
end
|
|
17
63
|
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
class SuspiciousOrdersController < Spree::Admin::BaseController
|
|
4
|
+
around_action :set_writing_role, only: %i[check_transaction]
|
|
5
|
+
|
|
6
|
+
def index
|
|
7
|
+
params[:q] = { completed_at_not_null: 1, payment_state_eq: 'balance_due', s: 'completed_at desc' }.dup if params[:q].blank?
|
|
8
|
+
|
|
9
|
+
@search = scope.ransack(params[:q])
|
|
10
|
+
@orders = @search.result(distinct: true)
|
|
11
|
+
.preload(:payments, :reimbursements)
|
|
12
|
+
.page(params[:page])
|
|
13
|
+
.per(params[:per_page] || Spree::Backend::Config[:admin_orders_per_page])
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def payments
|
|
17
|
+
@order = scope.find_by!(number: params[:id])
|
|
18
|
+
@payments = @order.payments.includes(:payment_method)
|
|
19
|
+
|
|
20
|
+
render layout: false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Renders the result inline into this order's payments frame instead of
|
|
24
|
+
# redirecting, so the admin never leaves this list.
|
|
25
|
+
def check_transaction
|
|
26
|
+
@order = scope.find_by!(number: params[:id])
|
|
27
|
+
payment = @order.payments.find_by!(number: params[:payment_number])
|
|
28
|
+
|
|
29
|
+
@check_result = query_payment_transaction(payment)
|
|
30
|
+
@payments = @order.payments.includes(:payment_method)
|
|
31
|
+
|
|
32
|
+
render 'payments', layout: false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Delegates to the gateway's own check_transaction (same method
|
|
38
|
+
# PaywayV2/Vattanac/TrueMoney already implement) - no per-gateway
|
|
39
|
+
# branching needed here.
|
|
40
|
+
def query_payment_transaction(payment)
|
|
41
|
+
method = payment.payment_method
|
|
42
|
+
return nil unless method.support_check_transaction_api?
|
|
43
|
+
|
|
44
|
+
result = method.check_transaction(payment)
|
|
45
|
+
|
|
46
|
+
# Not every gateway's checker implements error_message (e.g. Vattanac,
|
|
47
|
+
# TrueMoney) - fall back to the raw response so this doesn't blow up
|
|
48
|
+
# for gateways that only implement the minimal success?/json_response duck type.
|
|
49
|
+
message =
|
|
50
|
+
if result.success?
|
|
51
|
+
Spree.t('vpago.payments.payment_found_with_result', result: result.try(:json_response))
|
|
52
|
+
else
|
|
53
|
+
Spree.t('vpago.payments.payment_not_found_with_error', error: result.try(:error_message) || result.try(:json_response))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
{ success: result.success?, message: message }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def scope
|
|
60
|
+
current_store.orders.accessible_by(current_ability, :index)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -12,7 +12,6 @@ module Vpago
|
|
|
12
12
|
payway_abapay
|
|
13
13
|
payway_alipay
|
|
14
14
|
payway_wechat
|
|
15
|
-
acleda_v2
|
|
16
15
|
acleda
|
|
17
16
|
acleda_khqr
|
|
18
17
|
acleda_cards
|
|
@@ -29,10 +28,6 @@ module Vpago
|
|
|
29
28
|
%w[khqr deeplink all]
|
|
30
29
|
end
|
|
31
30
|
|
|
32
|
-
def available_acleda_v2_modes
|
|
33
|
-
Spree::Gateway::AcledaV2::MODES
|
|
34
|
-
end
|
|
35
|
-
|
|
36
31
|
def preference_field_for(form, field, options)
|
|
37
32
|
case field
|
|
38
33
|
when 'preferred_acleda_type'
|
|
@@ -50,8 +45,6 @@ module Vpago
|
|
|
50
45
|
return form.select(:preferred_abapay_khqr_deeplink_option, Spree::Gateway::PaywayV2::ABAPAY_KHQR_DEEPLINK_OPTIONS, {}, class: 'fullwidth select2')
|
|
51
46
|
when 'preferred_vattanac_payment_option'
|
|
52
47
|
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')
|
|
55
48
|
end
|
|
56
49
|
super
|
|
57
50
|
end
|
|
@@ -64,6 +64,8 @@ module Vpago
|
|
|
64
64
|
|
|
65
65
|
payment_methods = if early_adopter?
|
|
66
66
|
payment_methods.available_on_frontend_for_early_adopter.select { |pm| pm.available_for_order?(self) }
|
|
67
|
+
elsif ticket_seller?
|
|
68
|
+
payment_methods.available_on_back_end.select { |pm| pm.available_for_order?(self) }
|
|
67
69
|
else
|
|
68
70
|
payment_methods.available_on_front_end.select { |pm| pm.available_for_order?(self) }
|
|
69
71
|
end
|
|
@@ -73,23 +75,14 @@ module Vpago
|
|
|
73
75
|
end
|
|
74
76
|
end
|
|
75
77
|
|
|
76
|
-
# override
|
|
77
|
-
def collect_backend_payment_methods
|
|
78
|
-
payment_methods = if respond_to?(:tenant) && tenant.present?
|
|
79
|
-
tenant_payment_methods
|
|
80
|
-
elsif vendor_payment_methods.any?
|
|
81
|
-
vendor_payment_methods
|
|
82
|
-
else
|
|
83
|
-
store.payment_methods
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
payment_methods.available_on_back_end.select { |pm| pm.available_for_order?(self) }
|
|
87
|
-
end
|
|
88
|
-
|
|
89
78
|
def early_adopter?
|
|
90
79
|
user.present? && user.respond_to?(:early_adopter?) && user.early_adopter?
|
|
91
80
|
end
|
|
92
81
|
|
|
82
|
+
def ticket_seller?
|
|
83
|
+
user.present? && user.respond_to?(:has_spree_role?) && user.has_spree_role?('ticket_seller')
|
|
84
|
+
end
|
|
85
|
+
|
|
93
86
|
def tenant_payment_methods
|
|
94
87
|
tenant.tenant_payment_methods
|
|
95
88
|
end
|
|
@@ -27,9 +27,11 @@ module Vpago
|
|
|
27
27
|
:process_payment_url,
|
|
28
28
|
to: :url_constructor
|
|
29
29
|
|
|
30
|
-
#
|
|
30
|
+
# State machine event for payment retry / admin reprocess.
|
|
31
|
+
# `failed` is auto-reset by process!/capture! below (Sidekiq retry);
|
|
32
|
+
# void/invalid/processing are reset only by the admin reprocess action.
|
|
31
33
|
base.state_machine.event :reset_for_retry do
|
|
32
|
-
transition from: %i[failed], to: :checkout
|
|
34
|
+
transition from: %i[failed void invalid processing], to: :checkout
|
|
33
35
|
end
|
|
34
36
|
end
|
|
35
37
|
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
module Vpago
|
|
2
|
-
module PaymentMethodDecorator
|
|
2
|
+
module PaymentMethodDecorator
|
|
3
3
|
TYPE_PAYWAY = 'Spree::Gateway::Payway'.freeze
|
|
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
|
|
8
7
|
TYPE_ACLEDA_MOBILE = 'Spree::Gateway::AcledaMobile'.freeze
|
|
9
8
|
TYPE_TRUE_MONEY = 'Spree::Gateway::TrueMoney'.freeze
|
|
10
9
|
TYPE_VATTANAC = 'Spree::Gateway::Vattanac'.freeze
|
|
@@ -22,7 +21,6 @@ module Vpago
|
|
|
22
21
|
Spree::PaymentMethod::TYPE_PAYWAY,
|
|
23
22
|
Spree::PaymentMethod::TYPE_WINGSDK,
|
|
24
23
|
Spree::PaymentMethod::TYPE_ACLEDA,
|
|
25
|
-
Spree::PaymentMethod::TYPE_ACLEDA_V2,
|
|
26
24
|
Spree::PaymentMethod::TYPE_ACLEDA_MOBILE,
|
|
27
25
|
Spree::PaymentMethod::TYPE_VATTANAC,
|
|
28
26
|
Spree::PaymentMethod::TYPE_VATTANAC_MINI_APP,
|
|
@@ -91,10 +89,6 @@ module Vpago
|
|
|
91
89
|
type == Spree::PaymentMethod::TYPE_ACLEDA
|
|
92
90
|
end
|
|
93
91
|
|
|
94
|
-
def type_acleda_v2?
|
|
95
|
-
type == Spree::PaymentMethod::TYPE_ACLEDA_V2
|
|
96
|
-
end
|
|
97
|
-
|
|
98
92
|
def type_payway?
|
|
99
93
|
type == Spree::PaymentMethod::TYPE_PAYWAY
|
|
100
94
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<!-- replace "[data-hook='admin_payment_form_fields']" -->
|
|
2
|
+
|
|
3
|
+
<div data-hook="admin_payment_form_fields">
|
|
4
|
+
<div class="form-group">
|
|
5
|
+
<%= f.label :amount, Spree.t(:amount) %>
|
|
6
|
+
<%= f.text_field :amount, value: @order.display_outstanding_balance.money, class: 'form-control' %>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="form-group">
|
|
9
|
+
<strong><%= Spree.t(:payment_method) %></strong>
|
|
10
|
+
<% @payment_methods.each do |method| %>
|
|
11
|
+
<div class="radio" data-id="<%= Spree.t(method.name, scope: :payment_methods, default: method.name).parameterize %>">
|
|
12
|
+
<label data-hook="payment_method_field">
|
|
13
|
+
<%= radio_button_tag 'payment[payment_method_id]', method.id, method == @payment_method, { class: "payment_methods_radios" } %>
|
|
14
|
+
<%= Spree.t(method.name, scope: :payment_methods, default: method.name) %>
|
|
15
|
+
</label>
|
|
16
|
+
</div>
|
|
17
|
+
<% end %>
|
|
18
|
+
|
|
19
|
+
<div class="payment-method-settings">
|
|
20
|
+
<% @payment_methods.each do |method| %>
|
|
21
|
+
<div class="payment-methods my-3" id="payment_method_<%= method.id %>">
|
|
22
|
+
<% if method.source_required? %>
|
|
23
|
+
<% partial_path = "spree/admin/payments/source_forms/#{method.method_type}" %>
|
|
24
|
+
<% if lookup_context.exists?(partial_path, [], true) %>
|
|
25
|
+
<%= render partial: partial_path,
|
|
26
|
+
locals: {
|
|
27
|
+
payment_method: method,
|
|
28
|
+
previous_cards: method.reusable_sources(@order)
|
|
29
|
+
} %>
|
|
30
|
+
<% end %>
|
|
31
|
+
<% end %>
|
|
32
|
+
</div>
|
|
33
|
+
<% end %>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
<% elsif action == 'open_checkout' %>
|
|
8
8
|
<%= link_to_with_icon('qr-code.svg', Spree.t(action), payment.checkout_url, target: '_blank', no_text: true, class: "btn btn-light btn-sm") %>
|
|
9
9
|
<% else %>
|
|
10
|
-
|
|
10
|
+
<% action_data = { action: action } %>
|
|
11
|
+
<% action_data[:confirm] = Spree.t(:reprocess_payment_confirmation, default: 'Reprocess this payment? This will re-run the gateway transaction.') if action == 'process' %>
|
|
12
|
+
<%= link_to_with_icon(action + '.svg', Spree.t(action), fire_admin_order_payment_path(@order, payment, e: action), method: :put, no_text: true, data: action_data, class: "btn btn-light btn-sm") if can?(action.to_sym, payment) %>
|
|
11
13
|
<% end %>
|
|
12
14
|
<% end %>
|
|
13
15
|
</span>
|
|
@@ -14,15 +14,11 @@ module Vpago
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def find_and_verify!
|
|
17
|
-
if
|
|
17
|
+
if vattanac_mini_app_payload?
|
|
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 echoes back _transactionid, which is
|
|
24
|
-
# the payment number we sent as transactionID/txid at openSessionV2.
|
|
25
|
-
Spree::Payment.find_by!(number: params_hash[:_transactionid])
|
|
26
22
|
else
|
|
27
23
|
order = Spree::Order.find_by!(number: params_hash[:order_number])
|
|
28
24
|
verify_jwt!(order)
|
|
@@ -35,12 +31,8 @@ module Vpago
|
|
|
35
31
|
JWT.decode(params_hash[:order_jwt_token], order.token, 'HS256')
|
|
36
32
|
end
|
|
37
33
|
|
|
38
|
-
def
|
|
34
|
+
def vattanac_mini_app_payload?
|
|
39
35
|
params_hash[:data].present?
|
|
40
36
|
end
|
|
41
|
-
|
|
42
|
-
def acleda_v2_callback?
|
|
43
|
-
params_hash[:_paymenttokenid].present?
|
|
44
|
-
end
|
|
45
37
|
end
|
|
46
38
|
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<% if check_result.present? %>
|
|
2
|
+
<script>
|
|
3
|
+
show_flash('<%= check_result[:success] ? "success" : "error" %>', '<%= j check_result[:message] %>')
|
|
4
|
+
</script>
|
|
5
|
+
<% end %>
|
|
6
|
+
|
|
7
|
+
<table class="table mb-0">
|
|
8
|
+
<thead class="text-muted">
|
|
9
|
+
<tr>
|
|
10
|
+
<th><%= Spree::Payment.human_attribute_name(:id) %></th>
|
|
11
|
+
<th><%= Spree::Payment.human_attribute_name(:number) %></th>
|
|
12
|
+
<th><%= "#{Spree.t('date')}/#{Spree.t('time')}" %></th>
|
|
13
|
+
<th class="text-center"><%= Spree.t(:amount) %></th>
|
|
14
|
+
<th class="text-center"><%= Spree.t(:payment_method) %></th>
|
|
15
|
+
<th class="text-center"><%= Spree.t(:payment_state) %></th>
|
|
16
|
+
<th class="actions text-center"></th>
|
|
17
|
+
</tr>
|
|
18
|
+
</thead>
|
|
19
|
+
<tbody>
|
|
20
|
+
<% payments.each do |payment| %>
|
|
21
|
+
<tr>
|
|
22
|
+
<td><%= payment.id %></td>
|
|
23
|
+
<td><%= link_to payment.number, spree.admin_order_payment_path(order, payment), data: { turbo_frame: '_top' } %></td>
|
|
24
|
+
<td><%= pretty_time(payment.created_at) %></td>
|
|
25
|
+
<td class="text-center"><%= payment.display_amount %></td>
|
|
26
|
+
<td class="text-center">
|
|
27
|
+
<% payment_method = payment.payment_method %>
|
|
28
|
+
<% if can?(:update, payment_method) %>
|
|
29
|
+
<%= link_to payment_method.name, spree.edit_admin_payment_method_path(payment_method),
|
|
30
|
+
target: '_blank', rel: 'noopener noreferrer' %>
|
|
31
|
+
<% else %>
|
|
32
|
+
<%= payment_method.name %>
|
|
33
|
+
<% end %>
|
|
34
|
+
</td>
|
|
35
|
+
<td class="text-center">
|
|
36
|
+
<span class="badge badge-pill badge-<%= payment.state %>">
|
|
37
|
+
<%= Spree.t(payment.state, scope: :payment_states, default: payment.state.capitalize) %>
|
|
38
|
+
</span>
|
|
39
|
+
</td>
|
|
40
|
+
<td class="actions">
|
|
41
|
+
<span class="d-flex justify-content-center payment-action-buttons">
|
|
42
|
+
<% if payment.payment_method.support_check_transaction_api? %>
|
|
43
|
+
|
|
44
|
+
<%= button_to spree.check_transaction_admin_suspicious_order_path(order, payment_number: payment.number),
|
|
45
|
+
form: { data: { turbo_frame: "suspicious_order_#{order.id}_payments" }, class: 'd-inline' },
|
|
46
|
+
class: 'btn btn-light btn-sm icon-link with-tip', title: Spree.t('vpago.payments.query_status') do %>
|
|
47
|
+
<%= svg_icon(name: 'search.svg', classes: 'icon icon-search', width: 14, height: 14) %>
|
|
48
|
+
<% end %>
|
|
49
|
+
<% end %>
|
|
50
|
+
|
|
51
|
+
<% payment.actions.each do |action| %>
|
|
52
|
+
<% if action == 'credit' %>
|
|
53
|
+
<%= link_to_with_icon('exit.svg', Spree.t(:refund), spree.new_admin_order_payment_refund_path(order, payment),
|
|
54
|
+
no_text: true, data: { turbo_frame: '_top' }, class: 'btn btn-light btn-sm') if can?(:create, Spree::Refund) %>
|
|
55
|
+
<% elsif action == 'open_checkout' %>
|
|
56
|
+
<%= link_to_with_icon('qr-code.svg', Spree.t(action), payment.checkout_url,
|
|
57
|
+
target: '_blank', rel: 'noopener noreferrer', no_text: true, class: 'btn btn-light btn-sm') %>
|
|
58
|
+
<% else %>
|
|
59
|
+
<% action_data = { action: action, turbo_frame: '_top' } %>
|
|
60
|
+
<% action_data[:confirm] = Spree.t(:reprocess_payment_confirmation, default: 'Reprocess this payment? This will re-run the gateway transaction.') if action == 'process' %>
|
|
61
|
+
<%= link_to_with_icon(action + '.svg', Spree.t(action), spree.fire_admin_order_payment_path(order, payment, e: action),
|
|
62
|
+
method: :put, no_text: true, data: action_data,
|
|
63
|
+
class: 'btn btn-light btn-sm') if can?(action.to_sym, payment) %>
|
|
64
|
+
<% end %>
|
|
65
|
+
<% end %>
|
|
66
|
+
</span>
|
|
67
|
+
</td>
|
|
68
|
+
</tr>
|
|
69
|
+
<% end %>
|
|
70
|
+
</tbody>
|
|
71
|
+
</table>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<% content_for :page_title do %>
|
|
2
|
+
<%= Spree.t('admin.orders.suspicious_orders') %>
|
|
3
|
+
<% end %>
|
|
4
|
+
|
|
5
|
+
<%# Simple, always-visible filters kept intentionally small. %>
|
|
6
|
+
<div class="card mb-3">
|
|
7
|
+
<div class="card-body">
|
|
8
|
+
<%= search_form_for [:admin, @search], url: spree.admin_suspicious_orders_path, html: { method: :get } do |f| %>
|
|
9
|
+
<div class="form-row align-items-end">
|
|
10
|
+
<div class="col-12 col-sm-6 col-lg mb-2 mb-lg-0">
|
|
11
|
+
<div class="form-group mb-0">
|
|
12
|
+
<%= f.label :number_cont, Spree.t(:search_order_number) %>
|
|
13
|
+
<%= f.search_field :number_cont, class: 'form-control' %>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="col-12 col-sm-6 col-lg mb-2 mb-lg-0">
|
|
17
|
+
<div class="form-group mb-0">
|
|
18
|
+
<%= f.label :payments_number_cont, Spree.t('admin.orders.payment_number') %>
|
|
19
|
+
<%= f.search_field :payments_number_cont, class: 'form-control' %>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="col-12 col-sm-6 col-lg mb-2 mb-lg-0">
|
|
23
|
+
<div class="form-group mb-0">
|
|
24
|
+
<%= f.label :email_or_phone_number_cont, Spree.t('admin.orders.email_or_phone') %>
|
|
25
|
+
<%= f.search_field :email_or_phone_number_cont, class: 'form-control' %>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="col-12 col-sm-6 col-lg mb-2 mb-lg-0">
|
|
29
|
+
<div class="form-group mb-0">
|
|
30
|
+
<%= f.label :payment_state_eq, Spree.t(:payment_state) %>
|
|
31
|
+
|
|
32
|
+
<%= f.select :payment_state_eq,
|
|
33
|
+
::Spree::Order::PAYMENT_STATES.map { |s| [Spree.t("payment_states.#{s}"), s] },
|
|
34
|
+
{ include_blank: true }, class: 'select2-clear' %>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="col-12 col-sm-6 col-lg mb-2 mb-lg-0">
|
|
38
|
+
<div class="form-group mb-0">
|
|
39
|
+
<%= f.label :payments_payment_method_id_eq, Spree.t(:payment_method) %>
|
|
40
|
+
<%= f.select :payments_payment_method_id_eq,
|
|
41
|
+
::Spree::PaymentMethod.pluck(:name, :id),
|
|
42
|
+
{ include_blank: true }, class: 'select2-clear' %>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="col-12 col-lg-auto mb-2 mb-lg-0">
|
|
46
|
+
<%= button Spree.t(:search), 'search.svg', 'submit', class: 'btn-primary btn-block' %>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<% end %>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<table class="table" id="listing_suspicious_orders">
|
|
54
|
+
<thead>
|
|
55
|
+
<tr>
|
|
56
|
+
<th><%= Spree.t(:order) %></th>
|
|
57
|
+
<th><%= Spree.t(:date) %></th>
|
|
58
|
+
<th><%= Spree.t(:status) %></th>
|
|
59
|
+
<th><%= Spree.t(:payment_state) %></th>
|
|
60
|
+
<th class="text-right"><%= Spree.t(:total) %></th>
|
|
61
|
+
<th class="text-right"><%= Spree.t('admin.orders.outstanding_balance') %></th>
|
|
62
|
+
<th class="text-right"><%= Spree.t('admin.orders.payments') %></th>
|
|
63
|
+
</tr>
|
|
64
|
+
</thead>
|
|
65
|
+
<tbody>
|
|
66
|
+
<% @orders.each do |order| %>
|
|
67
|
+
<% payments_frame_id = "payments-collapse-#{order.id}" %>
|
|
68
|
+
<tr>
|
|
69
|
+
<td><%= link_to order.number, spree.edit_admin_order_path(order) %></td>
|
|
70
|
+
<td><%= pretty_time(order.completed_at) %></td>
|
|
71
|
+
<td>
|
|
72
|
+
<span class="badge badge-pill badge-<%= order.state %>">
|
|
73
|
+
<%= Spree.t("order_state.#{order.state}") %>
|
|
74
|
+
</span>
|
|
75
|
+
</td>
|
|
76
|
+
<td>
|
|
77
|
+
<span class="badge badge-pill badge-<%= order.payment_state %>">
|
|
78
|
+
<%= Spree.t("payment_states.#{order.payment_state}") %>
|
|
79
|
+
</span>
|
|
80
|
+
</td>
|
|
81
|
+
<td class="text-right"><%= order.display_total %></td>
|
|
82
|
+
<td class="text-right"><%= order.display_outstanding_balance %></td>
|
|
83
|
+
<td class="text-right">
|
|
84
|
+
<button type="button" data-toggle="collapse" data-target="#<%= payments_frame_id %>"
|
|
85
|
+
aria-expanded="false" aria-controls="<%= payments_frame_id %>"
|
|
86
|
+
class="btn btn-sm btn-light">
|
|
87
|
+
<%= Spree.t('admin.orders.payments_count', count: order.payments.size) %>
|
|
88
|
+
<%= svg_icon(name: 'chevron-down.svg', classes: 'icon ml-1', width: 12, height: 12) %>
|
|
89
|
+
</button>
|
|
90
|
+
</td>
|
|
91
|
+
</tr>
|
|
92
|
+
<tr>
|
|
93
|
+
<td colspan="7" class="p-0 border-top-0">
|
|
94
|
+
<div class="collapse" id="<%= payments_frame_id %>">
|
|
95
|
+
<div class="mb-3 border border-primary rounded-lg overflow-hidden bg-white">
|
|
96
|
+
<div class="bg-light px-3 py-2 small font-weight-bold text-uppercase text-muted">
|
|
97
|
+
<%= svg_icon(name: 'receipt.svg', classes: 'icon mr-2', width: 14, height: 14) %>
|
|
98
|
+
<%= Spree.t('admin.orders.payments_for_order', number: order.number) %>
|
|
99
|
+
</div>
|
|
100
|
+
<%= turbo_frame_tag "suspicious_order_#{order.id}_payments",
|
|
101
|
+
src: spree.payments_admin_suspicious_order_path(order), loading: 'lazy' do %>
|
|
102
|
+
<div class="p-3 text-center text-muted"><%= Spree.t(:loading) %>…</div>
|
|
103
|
+
<% end %>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</td>
|
|
107
|
+
</tr>
|
|
108
|
+
<% end %>
|
|
109
|
+
</tbody>
|
|
110
|
+
</table>
|
|
111
|
+
|
|
112
|
+
<%= render 'spree/admin/shared/index_table_options', collection: @orders %>
|
data/config/locales/en.yml
CHANGED
|
@@ -10,6 +10,17 @@ en:
|
|
|
10
10
|
insufficient_payment_amount_to_cover_the_total: "Payment amount is insufficient to cover the total"
|
|
11
11
|
qr_code: "QR code"
|
|
12
12
|
open_checkout: "Open Checkout"
|
|
13
|
+
admin:
|
|
14
|
+
orders:
|
|
15
|
+
suspicious_orders: "Suspicious Orders"
|
|
16
|
+
outstanding_balance: "Outstanding Balance"
|
|
17
|
+
payments: "Payments"
|
|
18
|
+
payment_number: "Payment Number"
|
|
19
|
+
email_or_phone: "Email / Phone Number"
|
|
20
|
+
payments_count:
|
|
21
|
+
one: "1 Payment"
|
|
22
|
+
other: "%{count} Payments"
|
|
23
|
+
payments_for_order: "Payments for %{number}"
|
|
13
24
|
vpago:
|
|
14
25
|
payments:
|
|
15
26
|
check_and_heal: "Auto correct"
|
data/config/locales/km.yml
CHANGED
|
@@ -6,6 +6,17 @@ spree:
|
|
|
6
6
|
insufficient_payment_amount_to_cover_the_total: "ចំនួនទឹកប្រាក់នៃការទូទាត់គឺមិនគ្រប់គ្រាន់"
|
|
7
7
|
qr_code: "QR code"
|
|
8
8
|
open_checkout: "Open Checkout"
|
|
9
|
+
admin:
|
|
10
|
+
orders:
|
|
11
|
+
suspicious_orders: "ការបង់ប្រាក់គួរឱ្យសង្ស័យ"
|
|
12
|
+
outstanding_balance: "សមតុល្យនៅសល់"
|
|
13
|
+
payments: "ការបង់ប្រាក់"
|
|
14
|
+
payment_number: "លេខការបង់ប្រាក់"
|
|
15
|
+
email_or_phone: "អ៊ីមែល / លេខទូរស័ព្ទ"
|
|
16
|
+
payments_count:
|
|
17
|
+
one: "ការបង់ប្រាក់ %{count}"
|
|
18
|
+
other: "ការបង់ប្រាក់ %{count}"
|
|
19
|
+
payments_for_order: "ការបង់ប្រាក់សម្រាប់ %{number}"
|
|
9
20
|
vpago:
|
|
10
21
|
payments:
|
|
11
22
|
check_and_heal: "កែស្វ័យប្រវត្តិ"
|
data/config/routes.rb
CHANGED
|
@@ -90,5 +90,12 @@ Spree::Core::Engine.add_routes do
|
|
|
90
90
|
post :verify_with_bank
|
|
91
91
|
end
|
|
92
92
|
end
|
|
93
|
+
|
|
94
|
+
resources :suspicious_orders, only: [:index] do
|
|
95
|
+
member do
|
|
96
|
+
get :payments
|
|
97
|
+
post 'payments/:payment_number/check_transaction', action: :check_transaction, as: :check_transaction
|
|
98
|
+
end
|
|
99
|
+
end
|
|
93
100
|
end
|
|
94
101
|
end
|
data/lib/spree_vpago/engine.rb
CHANGED
data/lib/spree_vpago/version.rb
CHANGED
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.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- You
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -165,6 +165,7 @@ files:
|
|
|
165
165
|
- app/controllers/spree/admin/payout_profile_shipping_methods_controller.rb
|
|
166
166
|
- app/controllers/spree/admin/payout_profiles_controller.rb
|
|
167
167
|
- app/controllers/spree/admin/payouts_controller.rb
|
|
168
|
+
- app/controllers/spree/admin/suspicious_orders_controller.rb
|
|
168
169
|
- app/controllers/spree/api/v2/storefront/checkout_controller_decorator.rb
|
|
169
170
|
- app/controllers/spree/payway_card_popups_controller.rb
|
|
170
171
|
- app/controllers/spree/payway_results_controller.rb
|
|
@@ -192,7 +193,6 @@ files:
|
|
|
192
193
|
- app/models/concerns/vpago/payoutable.rb
|
|
193
194
|
- app/models/spree/gateway/acleda.rb
|
|
194
195
|
- app/models/spree/gateway/acleda_mobile.rb
|
|
195
|
-
- app/models/spree/gateway/acleda_v2.rb
|
|
196
196
|
- app/models/spree/gateway/payway.rb
|
|
197
197
|
- app/models/spree/gateway/payway_v2.rb
|
|
198
198
|
- app/models/spree/gateway/true_money.rb
|
|
@@ -237,11 +237,13 @@ files:
|
|
|
237
237
|
- app/overrides/spree/admin/payment_methods/index/tenant_header.html.erb.deface
|
|
238
238
|
- app/overrides/spree/admin/payment_methods/index/vendor_body.html.erb.deface
|
|
239
239
|
- app/overrides/spree/admin/payment_methods/index/vendor_header.html.erb.deface
|
|
240
|
+
- app/overrides/spree/admin/payments/_form/admin_payment_form_fields.html.erb.deface
|
|
240
241
|
- app/overrides/spree/admin/payments/_list/actions.html.erb.deface
|
|
241
242
|
- app/overrides/spree/admin/promotions/_promotion_action/run_by_field.html.erb.deface
|
|
242
243
|
- app/overrides/spree/admin/shared/_order_tabs/payouts.html.erb.deface
|
|
243
244
|
- app/overrides/spree/admin/shared/_product_tabs/payout_profile_products.html.erb.deface
|
|
244
245
|
- app/overrides/spree/admin/shared/sub_menu/_integrations/payout_profiles.html.erb.deface
|
|
246
|
+
- app/overrides/spree/admin/shared/sub_menu/_orders/suspicious_orders.html.erb.deface
|
|
245
247
|
- app/overrides/spree/admin/shipping_methods/_form/handle_by.html.erb.deface
|
|
246
248
|
- app/overrides/spree/admin/shipping_methods/edit/shipping_method_tabs.html.erb.deface
|
|
247
249
|
- app/overrides/spree/admin/tax_categories/_form/collect_by_field.html.erb.deface
|
|
@@ -281,7 +283,6 @@ files:
|
|
|
281
283
|
- app/views/spree/admin/payments/source_forms/_payway_v2.html.erb
|
|
282
284
|
- app/views/spree/admin/payments/source_views/_acleda.html.erb
|
|
283
285
|
- app/views/spree/admin/payments/source_views/_acleda_mobile.html.erb
|
|
284
|
-
- app/views/spree/admin/payments/source_views/_acleda_v2.html.erb
|
|
285
286
|
- app/views/spree/admin/payments/source_views/_payment_payway.html.erb
|
|
286
287
|
- app/views/spree/admin/payments/source_views/_payway_v2.html.erb
|
|
287
288
|
- app/views/spree/admin/payments/source_views/_true_money.html.erb
|
|
@@ -308,10 +309,12 @@ files:
|
|
|
308
309
|
- app/views/spree/admin/shared/_shipping_method_tabs.html.erb
|
|
309
310
|
- app/views/spree/admin/shared/payout_profile/_info_card.html.erb
|
|
310
311
|
- app/views/spree/admin/shared/payout_profile/_status.html.erb
|
|
312
|
+
- app/views/spree/admin/suspicious_orders/_payments_table.html.erb
|
|
313
|
+
- app/views/spree/admin/suspicious_orders/index.html.erb
|
|
314
|
+
- app/views/spree/admin/suspicious_orders/payments.html.erb
|
|
311
315
|
- app/views/spree/checkout/payment/_acleda.html.erb
|
|
312
316
|
- app/views/spree/checkout/payment/_acleda_checkout_form.html.erb
|
|
313
317
|
- app/views/spree/checkout/payment/_acleda_mobile.html.erb
|
|
314
|
-
- app/views/spree/checkout/payment/_acleda_v2_checkout_form.html.erb
|
|
315
318
|
- app/views/spree/checkout/payment/_payment_payway.html.erb
|
|
316
319
|
- app/views/spree/checkout/payment/_payway_loader.html.erb
|
|
317
320
|
- app/views/spree/checkout/payment/_payway_root.html.erb
|
|
@@ -334,7 +337,6 @@ files:
|
|
|
334
337
|
- app/views/spree/payway_v2_card_popups/show.html.erb
|
|
335
338
|
- app/views/spree/vpago_payments/_transaction_checker.html.erb
|
|
336
339
|
- app/views/spree/vpago_payments/checkout.html.erb
|
|
337
|
-
- app/views/spree/vpago_payments/forms/spree/gateway/_acleda_v2.html.erb
|
|
338
340
|
- app/views/spree/vpago_payments/forms/spree/gateway/_payway_v2.html.erb
|
|
339
341
|
- app/views/spree/vpago_payments/forms/spree/gateway/_true_money.html.erb
|
|
340
342
|
- app/views/spree/vpago_payments/forms/spree/gateway/_vattanac.html.erb
|
|
@@ -392,9 +394,6 @@ files:
|
|
|
392
394
|
- lib/vpago/acleda_mobile/payment_request_updater.rb
|
|
393
395
|
- lib/vpago/acleda_mobile/payment_retriever.rb
|
|
394
396
|
- 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
|
|
398
397
|
- lib/vpago/payment_amount_calculator.rb
|
|
399
398
|
- lib/vpago/payment_request_updater.rb
|
|
400
399
|
- lib/vpago/payment_status_marker.rb
|
|
@@ -450,9 +449,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
450
449
|
version: 3.2.0
|
|
451
450
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
452
451
|
requirements:
|
|
453
|
-
- - "
|
|
452
|
+
- - ">="
|
|
454
453
|
- !ruby/object:Gem::Version
|
|
455
|
-
version:
|
|
454
|
+
version: '0'
|
|
456
455
|
requirements:
|
|
457
456
|
- none
|
|
458
457
|
rubygems_version: 3.4.1
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
module Spree
|
|
2
|
-
class Gateway::AcledaV2 < PaymentMethod
|
|
3
|
-
MODES = %w[khqr card deeplink].freeze
|
|
4
|
-
|
|
5
|
-
preference :merchant_name, :string
|
|
6
|
-
preference :xpay_service_base_url, :string
|
|
7
|
-
preference :payment_page_url, :string
|
|
8
|
-
preference :merchant_id, :string
|
|
9
|
-
preference :login_id, :string
|
|
10
|
-
preference :password, :string
|
|
11
|
-
preference :secret, :string # HMAC-SHA512 key
|
|
12
|
-
preference :payment_expiry_time_in_mn, :integer, default: 5
|
|
13
|
-
preference :acleda_v2_mode, :string, default: 'khqr'
|
|
14
|
-
|
|
15
|
-
validates :preferred_acleda_v2_mode, inclusion: { in: MODES }
|
|
16
|
-
|
|
17
|
-
def khqr?
|
|
18
|
-
preferred_acleda_v2_mode == 'khqr'
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def card?
|
|
22
|
-
preferred_acleda_v2_mode == 'card'
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def deeplink?
|
|
26
|
-
preferred_acleda_v2_mode == 'deeplink'
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# KHQR (Scan with Bank under Bakong) = "0"; Credit/debit card = "1"
|
|
30
|
-
def payment_card_value
|
|
31
|
-
card? ? '1' : '0'
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# override: partial to render in admin
|
|
35
|
-
def method_type
|
|
36
|
-
'acleda_v2'
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# override
|
|
40
|
-
def payment_source_class
|
|
41
|
-
Spree::VpagoPaymentSource
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# override
|
|
45
|
-
def payment_profiles_supported?
|
|
46
|
-
false
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# override
|
|
50
|
-
def available_for_order?(_order)
|
|
51
|
-
true
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# override: process! -> purchase! immediately (no pre-auth for ACLEDA V2)
|
|
55
|
-
def auto_capture?
|
|
56
|
-
true
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# override
|
|
60
|
-
def purchase(_amount, _source, gateway_options = {})
|
|
61
|
-
_, payment_number = gateway_options[:order_id].split('-')
|
|
62
|
-
payment = Spree::Payment.find_by(number: payment_number)
|
|
63
|
-
|
|
64
|
-
checker = check_transaction(payment)
|
|
65
|
-
payment.update(transaction_response: checker.json_response)
|
|
66
|
-
|
|
67
|
-
params = { check: { response: checker.json_response } }
|
|
68
|
-
|
|
69
|
-
if checker.success?
|
|
70
|
-
ActiveMerchant::Billing::Response.new(true, 'Acleda V2: Purchased', params)
|
|
71
|
-
else
|
|
72
|
-
ActiveMerchant::Billing::Response.new(false, 'Acleda V2: Purchasing Failed', params)
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# Defining check_transaction makes support_check_transaction_api? true,
|
|
77
|
-
# enabling the /vpago_payments/check_transaction poller.
|
|
78
|
-
def check_transaction(payment)
|
|
79
|
-
checker = Vpago::AcledaV2::TransactionStatus.new(payment)
|
|
80
|
-
checker.call
|
|
81
|
-
checker
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# override
|
|
85
|
-
def cancel(_response_code, _payment = nil)
|
|
86
|
-
ActiveMerchant::Billing::Response.new(true, 'Acleda V2: Payment has been canceled.')
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
|
@@ -1,12 +0,0 @@
|
|
|
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>
|
|
@@ -1,48 +0,0 @@
|
|
|
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 checkout.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
|
-
|
|
43
|
-
</script>
|
|
44
|
-
<% else %>
|
|
45
|
-
<%= render 'spree/checkout/payment/acleda_v2_checkout_form',
|
|
46
|
-
gateway_params: checkout.gateway_params,
|
|
47
|
-
action_url: checkout.action_url %>
|
|
48
|
-
<% end %>
|
data/lib/vpago/acleda_v2/base.rb
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
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
|
-
def amount
|
|
22
|
-
format('%.2f', @payment.amount)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# txid <= 16 chars; payment.number fits.
|
|
26
|
-
alias transaction_id payment_number
|
|
27
|
-
|
|
28
|
-
def order_number
|
|
29
|
-
@payment.order.number
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def merchant_name
|
|
33
|
-
payment_method.preferences[:merchant_name]
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def merchant_id
|
|
37
|
-
payment_method.preferences[:merchant_id]
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def login_id
|
|
41
|
-
payment_method.preferences[:login_id]
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def password
|
|
45
|
-
payment_method.preferences[:password]
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def secret
|
|
49
|
-
payment_method.preferences[:secret]
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Dynamically mirrors the shopper's remaining seat/inventory hold time so the bank's
|
|
53
|
-
# QR/session doesn't outlive the hold. Falls back to the admin-configured preference
|
|
54
|
-
# when the order has no hold (e.g. non-inventory items) or the hold has already lapsed.
|
|
55
|
-
def expiry_time
|
|
56
|
-
@expiry_time ||= (remaining_hold_minutes || payment_method.preferences[:payment_expiry_time_in_mn]).to_s
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def purchase_date
|
|
60
|
-
@payment.created_at.utc.strftime('%Y-%m-%d')
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def purchase_currency
|
|
64
|
-
@payment.order.currency
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def xpay_service_base_url
|
|
68
|
-
strip_whitespace(payment_method.preferences[:xpay_service_base_url])
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def open_session_url
|
|
72
|
-
"#{xpay_service_base_url}/openSessionV2"
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def get_txn_status_url # rubocop:disable Naming/AccessorMethodName
|
|
76
|
-
"#{xpay_service_base_url}/getTxnStatus"
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# KHQR/Card secure web redirect target
|
|
80
|
-
def action_url
|
|
81
|
-
strip_whitespace(payment_method.preferences[:payment_page_url])
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# successUrlToReturn / errorUrl. The bank appends its own query string
|
|
85
|
-
# (_paymenttokenid, _resultCode, ...) when it redirects back.
|
|
86
|
-
def continue_success_url
|
|
87
|
-
@payment.processing_url
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def callback_url
|
|
91
|
-
# return continue_success_url unless deeplink? && app_scheme.present?
|
|
92
|
-
|
|
93
|
-
# uri = URI.parse(continue_success_url)
|
|
94
|
-
# uri.scheme = app_scheme
|
|
95
|
-
# uri.to_s
|
|
96
|
-
|
|
97
|
-
continue_success_url
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def app_scheme
|
|
101
|
-
payment_method.preferences[:app_scheme]
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# android | ios (defaults to android when not supplied by the caller)
|
|
105
|
-
def opr_device
|
|
106
|
-
@options[:opr_device].presence || 'android'
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def platform
|
|
110
|
-
@options[:platform]
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def deeplink?
|
|
114
|
-
payment_method.deeplink? && platform == 'app'
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# --- HMAC-SHA512 (uppercase hex) signing ---
|
|
118
|
-
# openSessionV2 message: merchantID + loginId + password + txid
|
|
119
|
-
def open_session_hash
|
|
120
|
-
hmac512("#{merchant_id}#{login_id}#{password}#{payment_number}")
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
# getTxnStatus message: merchantId + loginId + password + paymentTokenid
|
|
124
|
-
def txn_status_hash(token)
|
|
125
|
-
hmac512("#{merchant_id}#{login_id}#{password}#{token}")
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def hmac512(message)
|
|
129
|
-
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha512'), secret, message).upcase
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def parse_json(body)
|
|
133
|
-
JSON.parse(body)
|
|
134
|
-
rescue JSON::ParserError
|
|
135
|
-
{}
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
private
|
|
139
|
-
|
|
140
|
-
# Minutes left on the order's seat/inventory hold, rounded up so we never quote the
|
|
141
|
-
# bank less time than is actually left. nil when the order has no hold, the hold has
|
|
142
|
-
# already lapsed, or hold_expires_at isn't defined (spree_cm_commissioner not loaded).
|
|
143
|
-
def remaining_hold_minutes
|
|
144
|
-
return nil unless @payment.order.respond_to?(:hold_expires_at)
|
|
145
|
-
|
|
146
|
-
hold_expires_at = @payment.order.hold_expires_at
|
|
147
|
-
return nil if hold_expires_at.blank?
|
|
148
|
-
|
|
149
|
-
minutes = ((hold_expires_at - Time.current) / 60.0).ceil
|
|
150
|
-
minutes if minutes.positive?
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def strip_whitespace(value)
|
|
154
|
-
value.to_s.gsub(/\s+/, '')
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
end
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
require 'faraday'
|
|
2
|
-
|
|
3
|
-
module Vpago
|
|
4
|
-
module AcledaV2
|
|
5
|
-
class Checkout < Base
|
|
6
|
-
attr_reader :error_message
|
|
7
|
-
|
|
8
|
-
def call
|
|
9
|
-
@error_message = nil
|
|
10
|
-
@response = open_session
|
|
11
|
-
|
|
12
|
-
if @response.status != 500 && @response.status != 200
|
|
13
|
-
@error_message = 'Server Error'
|
|
14
|
-
elsif @response.status == 500 # mostly when passing a wrong parameter name
|
|
15
|
-
@error_message = @response.body
|
|
16
|
-
elsif json_response.dig('result', 'code') != 0 # invalid payload, wrong loginId, etc.
|
|
17
|
-
@error_message = json_response.dig('result', 'errorDetails') || 'Unknown Error'
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
update_payment_source
|
|
21
|
-
|
|
22
|
-
self
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def failed?
|
|
26
|
-
@error_message.present?
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def json_response
|
|
30
|
-
@json_response ||= parse_json(@response.body)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# KHQR/Card secure web redirect form fields (auto-POSTed to paymentPage.jsp).
|
|
34
|
-
def gateway_params
|
|
35
|
-
return {} if failed?
|
|
36
|
-
|
|
37
|
-
{
|
|
38
|
-
merchantID: merchant_id,
|
|
39
|
-
sessionid: json_response.dig('result', 'sessionid'),
|
|
40
|
-
paymenttokenid: payment_token_id,
|
|
41
|
-
description: Base::PURCHASE_DESC,
|
|
42
|
-
expirytime: expiry_time,
|
|
43
|
-
amount: amount,
|
|
44
|
-
quantity: '1',
|
|
45
|
-
item: '1',
|
|
46
|
-
invoiceid: order_number,
|
|
47
|
-
currencytype: purchase_currency,
|
|
48
|
-
transactionID: payment_number,
|
|
49
|
-
successUrlToReturn: continue_success_url,
|
|
50
|
-
errorUrl: continue_success_url
|
|
51
|
-
}
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Deep Link redirect target.
|
|
55
|
-
def deeplink_url
|
|
56
|
-
return nil if failed?
|
|
57
|
-
|
|
58
|
-
json_response.dig('result', 'deeplinkUrl')
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
private
|
|
62
|
-
|
|
63
|
-
def open_session
|
|
64
|
-
conn = Faraday::Connection.new
|
|
65
|
-
|
|
66
|
-
conn.post(open_session_url) do |request|
|
|
67
|
-
request.headers['Content-Type'] = Base::CONTENT_TYPE_JSON
|
|
68
|
-
request.body = open_session_payload.to_json
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def open_session_payload
|
|
73
|
-
base = {
|
|
74
|
-
password: password,
|
|
75
|
-
loginId: login_id,
|
|
76
|
-
merchantID: merchant_id,
|
|
77
|
-
hash: open_session_hash,
|
|
78
|
-
xpayTransaction: xpay_transaction_base
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if deeplink?
|
|
82
|
-
base[:xpayTransaction].merge!(oprDevice: opr_device, callBackUrl: callback_url)
|
|
83
|
-
else
|
|
84
|
-
base[:xpayTransaction][:paymentCard] = payment_method.payment_card_value
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
base
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def xpay_transaction_base
|
|
91
|
-
{
|
|
92
|
-
txid: payment_number,
|
|
93
|
-
invoiceid: order_number,
|
|
94
|
-
purchaseAmount: amount,
|
|
95
|
-
purchaseCurrency: purchase_currency,
|
|
96
|
-
purchaseDate: purchase_date,
|
|
97
|
-
purchaseDesc: Base::PURCHASE_DESC,
|
|
98
|
-
item: '1',
|
|
99
|
-
quantity: '1',
|
|
100
|
-
expiryTime: expiry_time
|
|
101
|
-
}
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def payment_token_id
|
|
105
|
-
json_response.dig('result', 'xTran', 'paymentTokenid')
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# Persist the paymentTokenid so check_transaction / the bank callback can
|
|
109
|
-
# resolve the payment later. Wrap only the write in the writing role, since
|
|
110
|
-
# the checkout form partial runs on a GET request (replica connection).
|
|
111
|
-
def update_payment_source
|
|
112
|
-
return if failed?
|
|
113
|
-
|
|
114
|
-
ActiveRecord::Base.connected_to(role: :writing) do
|
|
115
|
-
@payment.source.update_column(:transaction_id, payment_token_id)
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
end
|
|
@@ -1,83 +0,0 @@
|
|
|
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
|
-
return false if token.blank?
|
|
35
|
-
|
|
36
|
-
http_success? && result_code.zero? && error_message == 'SUCCESS'
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def failed?
|
|
40
|
-
return false if token.blank?
|
|
41
|
-
return false unless http_success?
|
|
42
|
-
|
|
43
|
-
FAILED_ERROR_DETAILS.include?(error_message.to_s.upcase)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def pending?
|
|
47
|
-
!success? && !failed?
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
def http_success?
|
|
53
|
-
@response&.status == 200
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def token
|
|
57
|
-
@payment.source&.transaction_id
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def check_remote_status
|
|
61
|
-
conn = Faraday::Connection.new
|
|
62
|
-
|
|
63
|
-
conn.post(get_txn_status_url) do |request|
|
|
64
|
-
request.headers['Content-Type'] = Base::CONTENT_TYPE_JSON
|
|
65
|
-
request.body = payload.to_json
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# NOTE: getTxnStatus uses merchantId (lowercase d) and an extra merchantName
|
|
70
|
-
# field. merchantName is NOT part of the HMAC message.
|
|
71
|
-
def payload
|
|
72
|
-
{
|
|
73
|
-
loginId: login_id,
|
|
74
|
-
password: password,
|
|
75
|
-
merchantId: merchant_id,
|
|
76
|
-
merchantName: merchant_name,
|
|
77
|
-
hash: txn_status_hash(token),
|
|
78
|
-
paymentTokenid: token
|
|
79
|
-
}
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|