spree_vpago 2.4.0.pre.pre2 → 2.4.0.pre.pre4
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/assets/config/spree_vpago_manifest.js +2 -0
- data/app/assets/javascripts/vpago/wing_mini_app/login.js +75 -0
- data/app/assets/javascripts/vpago/wing_mini_app/web_jsbridge.bundle.js +1507 -0
- data/app/controllers/concerns/spree/mini_app_concern.rb +25 -0
- data/app/controllers/spree/admin/payment_acleda_v2_base_controller.rb +21 -0
- data/app/controllers/spree/admin/payment_acleda_v2_checkers_controller.rb +26 -0
- data/app/controllers/spree/admin/payment_acleda_v2_queriers_controller.rb +38 -0
- data/app/controllers/spree/admin/payment_payway_queriers_controller.rb +7 -0
- data/app/controllers/spree/admin/payment_vattanac_base_controller.rb +21 -0
- data/app/controllers/spree/admin/payment_vattanac_checkers_controller.rb +26 -0
- data/app/controllers/spree/admin/payment_vattanac_queriers_controller.rb +38 -0
- data/app/controllers/spree/{miniapp → mini_app}/acledas_controller.rb +12 -6
- data/app/controllers/spree/mini_app/wings_controller.rb +58 -0
- data/app/controllers/spree/vpago_payments_controller.rb +15 -3
- data/app/helpers/vpago/vpago_payments_helper.rb +6 -0
- data/app/models/spree/gateway/acleda_mini_app.rb +4 -6
- data/app/models/spree/gateway/wing_mini_app.rb +58 -0
- data/app/models/vpago/payment_decorator.rb +4 -0
- data/app/models/vpago/payment_method_decorator.rb +14 -0
- data/app/serializers/spree/v2/storefront/payment_method_serializer_decorator.rb +6 -0
- data/app/services/vpago/acleda_mini_app/session/create.rb +45 -23
- data/app/services/vpago/payment_finder.rb +11 -0
- data/app/services/vpago/wing_mini_app/session/create.rb +55 -0
- data/app/views/layouts/wing_mini_app.html.erb +20 -0
- data/app/views/spree/admin/payments/source_views/_acleda_v2.html.erb +2 -2
- data/app/views/spree/admin/payments/source_views/_vattanac.html.erb +3 -3
- data/app/views/spree/admin/payments/source_views/_wing_mini_app.html.erb +6 -0
- data/app/views/spree/mini_app/wings/show.html.erb +44 -0
- data/app/views/spree/vpago_payments/_success_script.html.erb +7 -0
- data/app/views/spree/vpago_payments/forms/spree/gateway/_acleda_mini_app.html.erb +80 -79
- data/app/views/spree/vpago_payments/forms/spree/gateway/_wing_mini_app.html.erb +88 -0
- data/app/views/spree/vpago_payments/processing_scripts/spree/gateway/_wing_mini_app.html.erb +15 -0
- data/app/views/spree/vpago_payments/success.html.erb +2 -0
- data/config/locales/en.yml +1 -0
- data/config/locales/km.yml +1 -0
- data/config/routes.rb +17 -4
- data/lib/spree_vpago/engine.rb +1 -0
- data/lib/spree_vpago/version.rb +1 -1
- data/lib/vpago/acleda_mini_app/checkout.rb +5 -9
- data/lib/vpago/acleda_mini_app/session_hash_validator.rb +35 -0
- data/lib/vpago/acleda_v2/payment_request_updater.rb +54 -0
- data/lib/vpago/http_timeouts.rb +6 -0
- data/lib/vpago/payway_v2/payment_request_updater.rb +5 -0
- data/lib/vpago/payway_v2/transaction_status.rb +2 -0
- data/lib/vpago/payway_v2/transaction_status_v2.rb +2 -0
- data/lib/vpago/true_money/base.rb +1 -1
- data/lib/vpago/true_money/transaction_status.rb +1 -1
- data/lib/vpago/vattanac/base.rb +3 -2
- data/lib/vpago/vattanac/payment_request_updater.rb +54 -0
- data/lib/vpago/vattanac/transaction_status.rb +17 -1
- data/lib/vpago/wing_mini_app/checkout.rb +60 -0
- data/lib/vpago/wing_mini_app/webhook_verifier.rb +78 -0
- metadata +27 -3
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# Shared by every mini app controller. What lives here is whatever a mini app needs from the
|
|
3
|
+
# request or gives back in the response, rather than anything one partner does on its own —
|
|
4
|
+
# today the gate its session service runs the caller through, built from the two things only
|
|
5
|
+
# the request knows, and the status a refusal answers with.
|
|
6
|
+
module MiniAppConcern
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def session_gate(provider)
|
|
12
|
+
SpreeCmCommissioner::MiniAppProvider::SessionGate.new(
|
|
13
|
+
provider: provider,
|
|
14
|
+
api_key: request.headers['X-Api-Key'],
|
|
15
|
+
ip: request.remote_ip
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# A cap or a blocked address is not the payload being wrong, so each answers with its
|
|
20
|
+
# own status and the partner can tell them apart.
|
|
21
|
+
def failure_status(result)
|
|
22
|
+
(result.value.is_a?(Hash) && result.value[:status]) || :unprocessable_entity
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
class PaymentAcledaV2BaseController < Spree::Admin::BaseController
|
|
4
|
+
include Spree::Backend::Callbacks
|
|
5
|
+
|
|
6
|
+
before_action :load_payment, only: %i[show update]
|
|
7
|
+
before_action :validate_order, only: [:update]
|
|
8
|
+
|
|
9
|
+
def validate_order
|
|
10
|
+
return unless @payment.order.completed?
|
|
11
|
+
|
|
12
|
+
flash[:error] = Spree.t('vpago.payments.not_allow_for_order_completed')
|
|
13
|
+
redirect_to admin_order_payment_path(order_id: @payment.order.number, id: @payment.number)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def load_payment
|
|
17
|
+
@payment = Payment.find_by!(number: params[:id])
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
class PaymentAcledaV2CheckersController < PaymentAcledaV2BaseController
|
|
4
|
+
include Spree::Backend::Callbacks
|
|
5
|
+
|
|
6
|
+
def update
|
|
7
|
+
options = {
|
|
8
|
+
updated_by_user_id: try_spree_current_user.id,
|
|
9
|
+
updated_reason: Spree.t('vpago.payments.checker_updated_by_description')
|
|
10
|
+
}
|
|
11
|
+
spree_updater = Vpago::AcledaV2::PaymentRequestUpdater.new(@payment, options)
|
|
12
|
+
spree_updater.call
|
|
13
|
+
|
|
14
|
+
@payment.reload
|
|
15
|
+
|
|
16
|
+
if @payment.order.completed?
|
|
17
|
+
flash[:success] = Spree.t(:successfully_updated, resource: Spree.t(:payments))
|
|
18
|
+
else
|
|
19
|
+
flash[:error] = Spree.t(:unsuccessfully_updated, resource: Spree.t(:payments))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
redirect_to admin_order_payment_path(order_id: @payment.order.number, id: @payment.number)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
class PaymentAcledaV2QueriersController < PaymentAcledaV2BaseController
|
|
4
|
+
include Spree::Backend::Callbacks
|
|
5
|
+
|
|
6
|
+
around_action :set_writing_role, only: %i[show]
|
|
7
|
+
|
|
8
|
+
def show
|
|
9
|
+
tran_status = @payment.payment_method.check_transaction(@payment)
|
|
10
|
+
|
|
11
|
+
if tran_status.success?
|
|
12
|
+
@payment.update_column(:gateway_status, true)
|
|
13
|
+
flash[:success] =
|
|
14
|
+
Spree.t('vpago.payments.payment_found_with_result', result: tran_status.json_response)
|
|
15
|
+
else
|
|
16
|
+
flash[:error] = Spree.t('vpago.payments.payment_not_found_with_error', error: tran_status.error_message)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
redirect_to admin_order_payment_path(order_id: @payment.order.number, id: @payment.number)
|
|
20
|
+
rescue Faraday::TimeoutError, Faraday::ConnectionFailed => e
|
|
21
|
+
VpagoLogger.error(
|
|
22
|
+
label: 'Spree::Admin::PaymentAcledaV2QueriersController#show gateway_timeout',
|
|
23
|
+
data: { payment_number: @payment.number, error_class: e.class.name, error_message: e.message }
|
|
24
|
+
)
|
|
25
|
+
flash[:error] = Spree.t('vpago.payments.gateway_timeout')
|
|
26
|
+
redirect_to admin_order_payment_path(order_id: @payment.order.number, id: @payment.number)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def set_writing_role
|
|
32
|
+
ActiveRecord::Base.connected_to(role: :writing) do
|
|
33
|
+
yield if block_given?
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -16,6 +16,13 @@ module Spree
|
|
|
16
16
|
flash[:error] = Spree.t('vpago.payments.payment_not_found_with_error', error: tran_status.error_message)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
redirect_to admin_order_payment_path(order_id: @payment.order.number, id: @payment.number)
|
|
20
|
+
rescue Faraday::TimeoutError, Faraday::ConnectionFailed => e
|
|
21
|
+
VpagoLogger.error(
|
|
22
|
+
label: 'Spree::Admin::PaymentPaywayQueriersController#show gateway_timeout',
|
|
23
|
+
data: { payment_number: @payment.number, error_class: e.class.name, error_message: e.message }
|
|
24
|
+
)
|
|
25
|
+
flash[:error] = Spree.t('vpago.payments.gateway_timeout')
|
|
19
26
|
redirect_to admin_order_payment_path(order_id: @payment.order.number, id: @payment.number)
|
|
20
27
|
end
|
|
21
28
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
class PaymentVattanacBaseController < Spree::Admin::BaseController
|
|
4
|
+
include Spree::Backend::Callbacks
|
|
5
|
+
|
|
6
|
+
before_action :load_payment, only: %i[show update]
|
|
7
|
+
before_action :validate_order, only: [:update]
|
|
8
|
+
|
|
9
|
+
def validate_order
|
|
10
|
+
return unless @payment.order.completed?
|
|
11
|
+
|
|
12
|
+
flash[:error] = Spree.t('vpago.payments.not_allow_for_order_completed')
|
|
13
|
+
redirect_to admin_order_payment_path(order_id: @payment.order.number, id: @payment.number)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def load_payment
|
|
17
|
+
@payment = Payment.find_by!(number: params[:id])
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
class PaymentVattanacCheckersController < PaymentVattanacBaseController
|
|
4
|
+
include Spree::Backend::Callbacks
|
|
5
|
+
|
|
6
|
+
def update
|
|
7
|
+
options = {
|
|
8
|
+
updated_by_user_id: try_spree_current_user.id,
|
|
9
|
+
updated_reason: Spree.t('vpago.payments.checker_updated_by_description')
|
|
10
|
+
}
|
|
11
|
+
spree_updater = Vpago::Vattanac::PaymentRequestUpdater.new(@payment, options)
|
|
12
|
+
spree_updater.call
|
|
13
|
+
|
|
14
|
+
@payment.reload
|
|
15
|
+
|
|
16
|
+
if @payment.order.completed?
|
|
17
|
+
flash[:success] = Spree.t(:successfully_updated, resource: Spree.t(:payments))
|
|
18
|
+
else
|
|
19
|
+
flash[:error] = Spree.t(:unsuccessfully_updated, resource: Spree.t(:payments))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
redirect_to admin_order_payment_path(order_id: @payment.order.number, id: @payment.number)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
class PaymentVattanacQueriersController < PaymentVattanacBaseController
|
|
4
|
+
include Spree::Backend::Callbacks
|
|
5
|
+
|
|
6
|
+
around_action :set_writing_role, only: %i[show]
|
|
7
|
+
|
|
8
|
+
def show
|
|
9
|
+
tran_status = @payment.payment_method.check_transaction(@payment)
|
|
10
|
+
|
|
11
|
+
if tran_status.success?
|
|
12
|
+
@payment.update_column(:gateway_status, true)
|
|
13
|
+
flash[:success] =
|
|
14
|
+
Spree.t('vpago.payments.payment_found_with_result', result: tran_status.json_response)
|
|
15
|
+
else
|
|
16
|
+
flash[:error] = Spree.t('vpago.payments.payment_not_found_with_error', error: tran_status.error_message)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
redirect_to admin_order_payment_path(order_id: @payment.order.number, id: @payment.number)
|
|
20
|
+
rescue Faraday::TimeoutError, Faraday::ConnectionFailed => e
|
|
21
|
+
VpagoLogger.error(
|
|
22
|
+
label: 'Spree::Admin::PaymentVattanacQueriersController#show gateway_timeout',
|
|
23
|
+
data: { payment_number: @payment.number, error_class: e.class.name, error_message: e.message }
|
|
24
|
+
)
|
|
25
|
+
flash[:error] = Spree.t('vpago.payments.gateway_timeout')
|
|
26
|
+
redirect_to admin_order_payment_path(order_id: @payment.order.number, id: @payment.number)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def set_writing_role
|
|
32
|
+
ActiveRecord::Base.connected_to(role: :writing) do
|
|
33
|
+
yield if block_given?
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
module Spree
|
|
2
|
-
module
|
|
2
|
+
module MiniApp
|
|
3
3
|
# Partner Session Initialization API (ACLEDA Mini App Integration Spec, Step 01).
|
|
4
|
-
# ACLEDA
|
|
5
|
-
# session-based miniAppUrl
|
|
4
|
+
# ACLEDA POSTs { phone, first_name, last_name } signed with X-Acleda-Signature, and
|
|
5
|
+
# gets back a session-based miniAppUrl to open in the WebView.
|
|
6
6
|
class AcledasController < Spree::Api::V2::BaseController
|
|
7
|
-
|
|
7
|
+
include Spree::MiniAppConcern
|
|
8
|
+
|
|
9
|
+
PROVIDER = 'acleda_bank'.freeze
|
|
10
|
+
|
|
11
|
+
# POST /mini_app/acleda
|
|
8
12
|
def create
|
|
9
13
|
result = ::Vpago::AcledaMiniApp::Session::Create.call(
|
|
10
14
|
phone: session_params[:phone],
|
|
11
15
|
first_name: session_params[:first_name],
|
|
12
|
-
last_name: session_params[:last_name]
|
|
16
|
+
last_name: session_params[:last_name],
|
|
17
|
+
signature: request.headers['X-Acleda-Signature'],
|
|
18
|
+
gate: session_gate(PROVIDER)
|
|
13
19
|
)
|
|
14
20
|
|
|
15
21
|
if result.success?
|
|
16
22
|
render json: { message: 'SUCCESS', miniAppUrl: result.value[:mini_app_url] }
|
|
17
23
|
else
|
|
18
|
-
render json: { message: 'FAILED', error: result.error.to_s }, status:
|
|
24
|
+
render json: { message: 'FAILED', error: result.error.to_s }, status: failure_status(result)
|
|
19
25
|
end
|
|
20
26
|
end
|
|
21
27
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module MiniApp
|
|
3
|
+
# Wing Mini App WebView entry. Wing opens this page cold — no server-to-server
|
|
4
|
+
# pre-step like ACLEDA/Vattanac — so the page itself calls getProfile and posts
|
|
5
|
+
# the signed result to #create.
|
|
6
|
+
class WingsController < ApplicationController
|
|
7
|
+
include Spree::MiniAppConcern
|
|
8
|
+
|
|
9
|
+
layout 'wing_mini_app'
|
|
10
|
+
|
|
11
|
+
PROVIDER = 'wing_bank'.freeze
|
|
12
|
+
|
|
13
|
+
# Both actions carry their own HMAC (ProfileVerifier / WebhookVerifier), and
|
|
14
|
+
# neither has an authenticated session for a CSRF token to protect.
|
|
15
|
+
skip_before_action :verify_authenticity_token, only: %i[create callback]
|
|
16
|
+
|
|
17
|
+
def show; end
|
|
18
|
+
|
|
19
|
+
def create
|
|
20
|
+
result = ::Vpago::WingMiniApp::Session::Create.call(
|
|
21
|
+
profile: profile_params.to_h,
|
|
22
|
+
gate: session_gate(PROVIDER)
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
if result.success?
|
|
26
|
+
render json: { message: 'SUCCESS', miniAppUrl: result.value[:mini_app_url] }
|
|
27
|
+
else
|
|
28
|
+
render json: { message: 'FAILED', error: result.error.to_s }, status: failure_status(result)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Wing POSTs its settlement here. Its own action rather than
|
|
33
|
+
# VpagoPaymentsController#process_payment, which is shaped around browser
|
|
34
|
+
# returns from other gateways — return_params merging, the ABA reviewing-mode
|
|
35
|
+
# branch, the internal_client flag — none of which a server-to-server webhook
|
|
36
|
+
# sends.
|
|
37
|
+
def callback
|
|
38
|
+
payment = ::Vpago::PaymentFinder.new(params.permit!.to_h).find_and_verify
|
|
39
|
+
|
|
40
|
+
return render json: { status: 'not_found' }, status: :not_found if payment.nil?
|
|
41
|
+
|
|
42
|
+
::Vpago::PaymentProcessorJob.perform_later(payment_number: payment.number) unless payment.order.paid?
|
|
43
|
+
|
|
44
|
+
render json: { status: 'ok' }, status: :ok
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def profile_params
|
|
50
|
+
params.require(:profile).permit(
|
|
51
|
+
:id, :appId, :firstName, :lastName, :middleName, :fullName, :sex, :age, :nationality,
|
|
52
|
+
:phone, :email, :city, :country, :address, :occupation, :addrCode, :dobFull, :nidNumber,
|
|
53
|
+
:nidType, :nidExpirationDate, :lang, :appVersion, :osVersion, :secret_key
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -24,6 +24,12 @@ module Spree
|
|
|
24
24
|
def processing
|
|
25
25
|
@order = @payment.order
|
|
26
26
|
|
|
27
|
+
# Common, not an edge case: the gateway callback or the checkout page's transaction
|
|
28
|
+
# checker often completes the order before the browser lands here. Skipping this page
|
|
29
|
+
# also skips its Firebase listener, so success must post order_is_completed itself
|
|
30
|
+
# (render_success_script) or an embedding iframe modal never closes.
|
|
31
|
+
return redirect_to @payment.success_url, allow_other_host: true if @order.completed?
|
|
32
|
+
|
|
27
33
|
VpagoLogger.log(label: 'Spree::VpagoPaymentsController#processing', data: vpago_log_context)
|
|
28
34
|
end
|
|
29
35
|
|
|
@@ -60,6 +66,12 @@ module Spree
|
|
|
60
66
|
else
|
|
61
67
|
render json: { status: :pending }, status: :ok
|
|
62
68
|
end
|
|
69
|
+
rescue Faraday::TimeoutError, Faraday::ConnectionFailed => e
|
|
70
|
+
VpagoLogger.error(
|
|
71
|
+
label: 'Spree::VpagoPaymentsController#check_transaction gateway_timeout',
|
|
72
|
+
data: vpago_log_context(error_class: e.class.name, error_message: e.message)
|
|
73
|
+
)
|
|
74
|
+
render json: { status: :pending }, status: :ok
|
|
63
75
|
end
|
|
64
76
|
|
|
65
77
|
# POST
|
|
@@ -146,9 +158,9 @@ module Spree
|
|
|
146
158
|
raise ActiveRecord::RecordNotFound unless @payment.present?
|
|
147
159
|
end
|
|
148
160
|
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
#
|
|
161
|
+
# These pages are embedded in iframes on many partner sites we don't
|
|
162
|
+
# enumerate, so framing is left unrestricted: no frame-ancestors policy, and
|
|
163
|
+
# Rails' default SAMEORIGIN header is dropped for the iframe actions.
|
|
152
164
|
def allow_iframe_embedding
|
|
153
165
|
response.headers.delete('X-Frame-Options')
|
|
154
166
|
end
|
|
@@ -13,6 +13,12 @@ module Vpago
|
|
|
13
13
|
render partial: 'spree/vpago_payments/transaction_checker'
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
# Host apps override success.html.erb; call this from every override so an
|
|
17
|
+
# embedding iframe modal still learns the order completed.
|
|
18
|
+
def render_success_script
|
|
19
|
+
render partial: 'spree/vpago_payments/success_script'
|
|
20
|
+
end
|
|
21
|
+
|
|
16
22
|
# Each payment method may have their own additional processing script,
|
|
17
23
|
# so we will look for the partial based on the payment method class name.
|
|
18
24
|
#
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
# ACLEDA Mini App payment method.
|
|
3
3
|
#
|
|
4
|
-
# Reuses the ACLEDA V2 integration in deeplink mode:
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# verification reuses AcledaV2's #check_transaction (getTxnStatus).
|
|
4
|
+
# Reuses the ACLEDA V2 integration in deeplink mode: openSessionV2 returns a deeplink the
|
|
5
|
+
# payment page opens as a top-frame navigation, which ACLEDA's own WebView turns into an app
|
|
6
|
+
# launch. Server-side verification reuses AcledaV2's #check_transaction (getTxnStatus).
|
|
8
7
|
class Gateway::AcledaMiniApp < Gateway::AcledaV2
|
|
9
8
|
# override: partial to render in admin / checkout form lookup
|
|
10
9
|
def method_type
|
|
11
10
|
'acleda_mini_app'
|
|
12
11
|
end
|
|
13
12
|
|
|
14
|
-
# The mini app always
|
|
15
|
-
# set the ACLEDA V2 mode.
|
|
13
|
+
# The mini app always pays by deeplink, so admins don't need to set the ACLEDA V2 mode.
|
|
16
14
|
def deeplink?
|
|
17
15
|
true
|
|
18
16
|
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# Wing Mini App payment method (doPayment). Wing pushes settlement to our webhook,
|
|
3
|
+
# where Vpago::PaymentFinder checks the secretKey and the amount/currency match
|
|
4
|
+
# before storing transaction_response — the only place that is verified.
|
|
5
|
+
class Gateway::WingMiniApp < PaymentMethod
|
|
6
|
+
def method_type
|
|
7
|
+
'wing_mini_app'
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Read from Wing's mini app row (WingMiniApp::Config), not a payment method preference:
|
|
11
|
+
# login verifies against the same pair, and a second source could sign payments as a
|
|
12
|
+
# different partner — a mismatch that first surfaces at the webhook, after the debit.
|
|
13
|
+
def app_id
|
|
14
|
+
SpreeCmCommissioner::WingMiniApp::Config.app_id
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def secret_api_key
|
|
18
|
+
SpreeCmCommissioner::WingMiniApp::Config.secret_api_key
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def payment_source_class
|
|
22
|
+
Spree::VpagoPaymentSource
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def auto_capture?
|
|
26
|
+
true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# override
|
|
30
|
+
def purchase(_amount, _source, gateway_options = {})
|
|
31
|
+
_, payment_number = gateway_options[:order_id].split('-')
|
|
32
|
+
payment = Spree::Payment.find_by(number: payment_number)
|
|
33
|
+
response = payment&.transaction_response || {}
|
|
34
|
+
|
|
35
|
+
params = { payment_response: response }
|
|
36
|
+
|
|
37
|
+
# A present transaction_response is PaymentFinder having already verified this
|
|
38
|
+
# settlement, so there is nothing left to check.
|
|
39
|
+
if response.present?
|
|
40
|
+
ActiveMerchant::Billing::Response.new(true, 'Wing Mini App: Purchased', params)
|
|
41
|
+
else
|
|
42
|
+
ActiveMerchant::Billing::Response.new(false, 'Wing Mini App: Purchasing Failed', params)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# override
|
|
47
|
+
def void(_response_code, _gateway_options)
|
|
48
|
+
ActiveMerchant::Billing::Response.new(true, 'Wing Mini App order has been voided.')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# override
|
|
52
|
+
# Wing documents no refund API, so this fails rather than returning a no-op success:
|
|
53
|
+
# refunds stay manual with Wing until they publish one.
|
|
54
|
+
def cancel(_response_code, _payment)
|
|
55
|
+
ActiveMerchant::Billing::Response.new(false, 'Wing Mini App: cancel is not supported — no Wing refund API is documented yet.')
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -10,6 +10,7 @@ module Vpago
|
|
|
10
10
|
TYPE_TRUE_MONEY = 'Spree::Gateway::TrueMoney'.freeze
|
|
11
11
|
TYPE_VATTANAC = 'Spree::Gateway::Vattanac'.freeze
|
|
12
12
|
TYPE_VATTANAC_MINI_APP = 'Spree::Gateway::VattanacMiniApp'.freeze
|
|
13
|
+
TYPE_WING_MINI_APP = 'Spree::Gateway::WingMiniApp'.freeze
|
|
13
14
|
TYPE_CASH_ON = 'Spree::PaymentMethod::CashOn'.freeze
|
|
14
15
|
|
|
15
16
|
def self.prepended(base)
|
|
@@ -28,6 +29,7 @@ module Vpago
|
|
|
28
29
|
Spree::PaymentMethod::TYPE_ACLEDA_MINI_APP,
|
|
29
30
|
Spree::PaymentMethod::TYPE_VATTANAC,
|
|
30
31
|
Spree::PaymentMethod::TYPE_VATTANAC_MINI_APP,
|
|
32
|
+
Spree::PaymentMethod::TYPE_WING_MINI_APP,
|
|
31
33
|
Spree::PaymentMethod::TYPE_TRUE_MONEY,
|
|
32
34
|
Spree::PaymentMethod::TYPE_CASH_ON
|
|
33
35
|
]
|
|
@@ -82,6 +84,10 @@ module Vpago
|
|
|
82
84
|
::Vpago::Acleda::PaymentRequestUpdater
|
|
83
85
|
elsif type_acleda_mobile?
|
|
84
86
|
::Vpago::AcledaMobile::PaymentRequestUpdater
|
|
87
|
+
elsif type_acleda_v2?
|
|
88
|
+
::Vpago::AcledaV2::PaymentRequestUpdater
|
|
89
|
+
elsif type_vattanac?
|
|
90
|
+
::Vpago::Vattanac::PaymentRequestUpdater
|
|
85
91
|
end
|
|
86
92
|
end
|
|
87
93
|
|
|
@@ -113,10 +119,18 @@ module Vpago
|
|
|
113
119
|
type == Spree::PaymentMethod::TYPE_WINGSDK
|
|
114
120
|
end
|
|
115
121
|
|
|
122
|
+
def type_vattanac?
|
|
123
|
+
type == Spree::PaymentMethod::TYPE_VATTANAC
|
|
124
|
+
end
|
|
125
|
+
|
|
116
126
|
def type_vattanac_mini_app?
|
|
117
127
|
type == Spree::PaymentMethod::TYPE_VATTANAC_MINI_APP
|
|
118
128
|
end
|
|
119
129
|
|
|
130
|
+
def type_wing_mini_app?
|
|
131
|
+
type == Spree::PaymentMethod::TYPE_WING_MINI_APP
|
|
132
|
+
end
|
|
133
|
+
|
|
120
134
|
def type_true_money?
|
|
121
135
|
type == Spree::PaymentMethod::TYPE_TRUE_MONEY
|
|
122
136
|
end
|
|
@@ -14,6 +14,12 @@ module Spree
|
|
|
14
14
|
|
|
15
15
|
pref.blank? || pref[:payment_option].blank? ? payment_method.method_type : pref[:payment_option]
|
|
16
16
|
end
|
|
17
|
+
|
|
18
|
+
# Overried the payment method type to show all payment methods as one list on App.
|
|
19
|
+
# Controlled by ENV["PAYMENT_METHOD_VIEW"] = "split" | "join" (default: "join").
|
|
20
|
+
base.attribute :type do |payment_method|
|
|
21
|
+
ENV.fetch('PAYMENT_METHOD_VIEW', 'join') == 'split' ? payment_method.type : 'payment_method'
|
|
22
|
+
end
|
|
17
23
|
end
|
|
18
24
|
end
|
|
19
25
|
end
|
|
@@ -3,20 +3,32 @@ module Vpago
|
|
|
3
3
|
module Session
|
|
4
4
|
# Partner Session Initialization (ACLEDA Mini App Integration Spec, Step 01).
|
|
5
5
|
#
|
|
6
|
-
# ACLEDA
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# when the WebView later authenticates.
|
|
6
|
+
# ACLEDA POSTs { phone, first_name, last_name } with an X-Acleda-Signature HMAC
|
|
7
|
+
# (see SessionHashValidator), checked before we touch the user so nobody can mint
|
|
8
|
+
# a session for an arbitrary phone. We then find or create the customer, tag them
|
|
9
|
+
# with the acleda_bank identity, and hand back a miniAppUrl carrying a session JWT.
|
|
11
10
|
class Create
|
|
12
11
|
prepend Spree::ServiceModule::Base
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
# `signature`, not `hash`: an attr_reader named `hash` overrides Object#hash and
|
|
14
|
+
# silently breaks this object as a Hash key.
|
|
15
|
+
#
|
|
16
|
+
# `gate` answers who is calling, from where and how often. The controller builds it:
|
|
17
|
+
# the key header and the address exist only on the request.
|
|
18
|
+
def call(gate:, phone: nil, first_name: nil, last_name: nil, signature: nil)
|
|
15
19
|
return failure(nil, 'phone is required') if phone.blank?
|
|
16
20
|
|
|
17
21
|
@phone = phone
|
|
18
22
|
@first_name = first_name
|
|
19
23
|
@last_name = last_name
|
|
24
|
+
@signature = signature
|
|
25
|
+
@gate = gate
|
|
26
|
+
|
|
27
|
+
return failure({ status: :unauthorized }, 'missing or unknown api key') unless gate.api_key_accepted?
|
|
28
|
+
return failure({ status: :forbidden }, 'address not allowed') unless gate.address_allowed?
|
|
29
|
+
return failure(nil, 'invalid signature') unless valid_signature?
|
|
30
|
+
return failure(nil, 'phone is invalid') if intel_phone.blank?
|
|
31
|
+
return failure({ status: :too_many_requests }, 'too many requests') if gate.session_exceeded?(phone: intel_phone)
|
|
20
32
|
|
|
21
33
|
user = find_or_create_user
|
|
22
34
|
return failure(user, 'user creation failed') if user.nil? || !user.persisted?
|
|
@@ -26,7 +38,20 @@ module Vpago
|
|
|
26
38
|
|
|
27
39
|
private
|
|
28
40
|
|
|
29
|
-
attr_reader :phone, :first_name, :last_name
|
|
41
|
+
attr_reader :phone, :first_name, :last_name, :signature, :gate
|
|
42
|
+
|
|
43
|
+
# Only asked where this call would mint an account — the damage a leaked key does.
|
|
44
|
+
# Returning customers never spend that quota.
|
|
45
|
+
def new_user_throttled?
|
|
46
|
+
!acleda_identity.persisted? && find_existing_user.nil? && gate.new_user_exceeded?(phone: intel_phone)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def valid_signature?
|
|
50
|
+
::Vpago::AcledaMiniApp::SessionHashValidator.new(
|
|
51
|
+
phone: phone, first_name: first_name, last_name: last_name, hash: signature,
|
|
52
|
+
secret_key: gate.mini_app&.preferred_secret_key
|
|
53
|
+
).call
|
|
54
|
+
end
|
|
30
55
|
|
|
31
56
|
def find_or_create_user
|
|
32
57
|
identity = acleda_identity
|
|
@@ -34,9 +59,10 @@ module Vpago
|
|
|
34
59
|
# Returning customer: the ACLEDA identity already points at a user.
|
|
35
60
|
return identity.user if identity.persisted?
|
|
36
61
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
#
|
|
62
|
+
return nil if new_user_throttled?
|
|
63
|
+
|
|
64
|
+
# New to ACLEDA: reuse a customer matched by phone, otherwise build one, then
|
|
65
|
+
# tag them so the lookup above hits next time.
|
|
40
66
|
user = find_existing_user || build_user
|
|
41
67
|
identity.name = full_name
|
|
42
68
|
user.user_identity_providers << identity
|
|
@@ -45,14 +71,17 @@ module Vpago
|
|
|
45
71
|
user
|
|
46
72
|
end
|
|
47
73
|
|
|
74
|
+
# Memoized: #new_user_throttled? asks the same questions, and this row is the one
|
|
75
|
+
# that gets tagged onto the user.
|
|
48
76
|
def acleda_identity
|
|
49
|
-
SpreeCmCommissioner::UserIdentityProvider.acleda_bank.find_or_initialize_by(sub: phone)
|
|
77
|
+
@acleda_identity ||= SpreeCmCommissioner::UserIdentityProvider.acleda_bank.find_or_initialize_by(sub: phone)
|
|
50
78
|
end
|
|
51
79
|
|
|
52
80
|
def find_existing_user
|
|
53
|
-
return if
|
|
81
|
+
return @find_existing_user if defined?(@find_existing_user)
|
|
82
|
+
return @find_existing_user = nil if intel_phone.blank?
|
|
54
83
|
|
|
55
|
-
Spree::User.by_non_tenant.find_by(intel_phone_number: intel_phone)
|
|
84
|
+
@find_existing_user = Spree::User.by_non_tenant.find_by(intel_phone_number: intel_phone)
|
|
56
85
|
end
|
|
57
86
|
|
|
58
87
|
def build_user
|
|
@@ -67,15 +96,14 @@ module Vpago
|
|
|
67
96
|
end
|
|
68
97
|
|
|
69
98
|
def mini_app_url(user)
|
|
70
|
-
# A JWT signed with the user's secure_token.
|
|
71
|
-
#
|
|
72
|
-
# which enforces the signature.
|
|
99
|
+
# A JWT signed with the user's secure_token. `session_key`, not `session_id`, so
|
|
100
|
+
# the OAuth grant routes it to the ACLEDA authenticator.
|
|
73
101
|
session_key = SpreeCmCommissioner::UserSessionJwtToken.encode(
|
|
74
102
|
{ user_id: user.id },
|
|
75
103
|
user.reload.secure_token
|
|
76
104
|
)
|
|
77
105
|
|
|
78
|
-
"#{Spree::Store.default.formatted_url}/mini_app/acleda?session_key=#{session_key}
|
|
106
|
+
"#{Spree::Store.default.formatted_url}/mini_app/acleda?session_key=#{session_key}"
|
|
79
107
|
end
|
|
80
108
|
|
|
81
109
|
def full_name
|
|
@@ -87,12 +115,6 @@ module Vpago
|
|
|
87
115
|
|
|
88
116
|
@intel_phone = SpreeCmCommissioner::PhoneNumberParser.call(phone_number: phone).intel_phone_number
|
|
89
117
|
end
|
|
90
|
-
|
|
91
|
-
# Services to disable in the mini app storefront. Configurable per ACLEDA's
|
|
92
|
-
# requirement; defaults to none.
|
|
93
|
-
def disable_services
|
|
94
|
-
ENV.fetch('ACLEDA_MINI_APP_DISABLE_SERVICES', '')
|
|
95
|
-
end
|
|
96
118
|
end
|
|
97
119
|
end
|
|
98
120
|
end
|