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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/controllers/spree/admin/payments_controller_decorator.rb +47 -1
  4. data/app/controllers/spree/admin/suspicious_orders_controller.rb +64 -0
  5. data/app/helpers/vpago/admin/base_helper_decorator.rb +0 -7
  6. data/app/models/vpago/order_decorator.rb +6 -13
  7. data/app/models/vpago/payment_decorator.rb +4 -2
  8. data/app/models/vpago/payment_method_decorator.rb +1 -7
  9. data/app/overrides/spree/admin/payments/_form/admin_payment_form_fields.html.erb.deface +36 -0
  10. data/app/overrides/spree/admin/payments/_list/actions.html.erb.deface +3 -1
  11. data/app/overrides/spree/admin/shared/sub_menu/_orders/suspicious_orders.html.erb.deface +3 -0
  12. data/app/services/vpago/payment_finder.rb +2 -10
  13. data/app/views/spree/admin/suspicious_orders/_payments_table.html.erb +71 -0
  14. data/app/views/spree/admin/suspicious_orders/index.html.erb +112 -0
  15. data/app/views/spree/admin/suspicious_orders/payments.html.erb +3 -0
  16. data/config/locales/en.yml +11 -0
  17. data/config/locales/km.yml +11 -0
  18. data/config/routes.rb +7 -0
  19. data/lib/spree_vpago/engine.rb +0 -1
  20. data/lib/spree_vpago/version.rb +1 -1
  21. data/lib/vpago/acleda/checkout.rb +1 -1
  22. metadata +10 -11
  23. data/app/models/spree/gateway/acleda_v2.rb +0 -89
  24. data/app/views/spree/admin/payments/source_views/_acleda_v2.html.erb +0 -5
  25. data/app/views/spree/checkout/payment/_acleda_v2_checkout_form.html.erb +0 -12
  26. data/app/views/spree/vpago_payments/forms/spree/gateway/_acleda_v2.html.erb +0 -48
  27. data/lib/vpago/acleda_v2/base.rb +0 -158
  28. data/lib/vpago/acleda_v2/checkout.rb +0 -120
  29. 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: 650e7e403578ee0af32eee90b24798adb845e31d47b819346a42cc5f45db8099
4
- data.tar.gz: b052270ee1d51e4bf7c0e01886d23dbf80f97689ae6a27b4a209c3acd241dd94
3
+ metadata.gz: 92861b027f31b9c4239298967116f3cb34106575da30b650c7220a7981341a23
4
+ data.tar.gz: 9a43d50434fb8e0eea79e9413219d62bdd10f32b7599534f2702b0143da61c67
5
5
  SHA512:
6
- metadata.gz: 50547d821d6925afe9ec72dee708200547beef069c8012dbccccf9d3f9910852f5f43c06fa3adb558cac68e84fa56a85aedffdfe2313eac4989ee69614dee577
7
- data.tar.gz: 4b71835adc06d52f88ce1243a50f5409c5605ff9711cf0edf7b25ce4cc4ed9c71650cd1da9631a1a7c1a7a8d7fcf637e4f4056a43df0a7301853e095781bcd44
6
+ metadata.gz: 791e046282f93cb5bbaf1683e788a9e440c394e5101f88fdb48431a517cbcf0367c1b5c49536577f438a31dc30fa37040bcad464b2ec617b30e3c4e2dcbbe6f9
7
+ data.tar.gz: e31a474f731d18f6a65628bf8f901ed4fd73407a289fc354687ececedb3e0a4f45ab13933d70fba344085e03161b74d139155185db6826a5794f66fab62a0566
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- spree_vpago (2.3.5.pre.pre.pre.8)
4
+ spree_vpago (2.3.6)
5
5
  faraday
6
6
  google-cloud-firestore
7
7
  spree_api (>= 4.5)
@@ -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
- # Add state machine event for payment retry
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 # rubocop:disable Metrics/ModuleLength
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
- <%= 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: action }, class: "btn btn-light btn-sm") if can?(action.to_sym, payment) %>
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>
@@ -0,0 +1,3 @@
1
+ <!-- insert_bottom "[data-hook='admin_orders']" -->
2
+
3
+ <%= tab :suspicious_orders, url: spree.admin_suspicious_orders_path, label: Spree.t('admin.orders.suspicious_orders') %>
@@ -14,15 +14,11 @@ module Vpago
14
14
  end
15
15
 
16
16
  def find_and_verify!
17
- if vattanac_mini_app_callback?
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 vattanac_mini_app_callback?
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 %>
@@ -0,0 +1,3 @@
1
+ <%= turbo_frame_tag "suspicious_order_#{@order.id}_payments" do %>
2
+ <%= render 'payments_table', order: @order, payments: @payments, check_result: @check_result %>
3
+ <% end %>
@@ -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"
@@ -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
@@ -21,7 +21,6 @@ module SpreeVpago
21
21
  Spree::Gateway::PaywayV2,
22
22
  Spree::Gateway::WingSdk,
23
23
  Spree::Gateway::Acleda,
24
- Spree::Gateway::AcledaV2,
25
24
  Spree::Gateway::AcledaMobile,
26
25
  Spree::Gateway::Vattanac,
27
26
  Spree::Gateway::VattanacMiniApp,
@@ -1,7 +1,7 @@
1
1
  module SpreeVpago
2
2
  module_function
3
3
 
4
- VERSION = '2.3.5-pre-8'.freeze
4
+ VERSION = '2.3.6'.freeze
5
5
 
6
6
  def version
7
7
  Gem::Version.new VERSION
@@ -60,7 +60,7 @@ module Vpago
60
60
  purchaseDate: purchase_date,
61
61
  purchaseDesc: 'items',
62
62
  item: '1',
63
- quantity: '1', # always 1: one payment = one transaction
63
+ quantity: '1',
64
64
  expiryTime: expiry_time,
65
65
  otherUrl: other_url
66
66
  }
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.5.pre.pre.pre.8
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-14 00:00:00.000000000 Z
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: 1.3.1
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,5 +0,0 @@
1
- <%= render 'spree/admin/payments/source_views/vpago_payment_tmpl',
2
- payment: payment,
3
- check_status_url: '',
4
- heal_payment_url: '',
5
- manual_update_payment_url: '' %>
@@ -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 %>
@@ -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