IMP-331: Implements provider host's session change handler (#145)
Some checks failed
Build and publish Docker image / build-push (push) Has been cancelled
Erlang CI Checks / Load .env (push) Has been cancelled
Erlang CI Checks / Run checks (push) Has been cancelled

* IMP-331: Implements provider host's session change handler

* Implements session' failure change

* Adds session change testcase

* Fixes helper' spec

* Refactors testcase w/ more asserts and fixes session finalizing on status change

* Bumps damsel
This commit is contained in:
Aleksey Kashapov 2024-10-14 12:56:00 +03:00 committed by GitHub
parent 24f9f6bd63
commit fa7cd47a79
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 152 additions and 41 deletions

View File

@ -24,6 +24,7 @@
-define(NS, <<"invoice">>).
-export([process_callback/2]).
-export([process_session_change_by_tag/2]).
-export_type([activity/0]).
-export_type([invoice/0]).
@ -211,14 +212,14 @@ get_payment_state(PaymentSession) ->
%%
-type tag() :: dmsl_base_thrift:'Tag'().
-type session_change() :: hg_session:change().
-type callback() :: {provider, dmsl_proxy_provider_thrift:'Callback'()}.
-type callback_response() :: dmsl_proxy_provider_thrift:'CallbackResponse'().
-spec process_callback(tag(), callback()) ->
{ok, callback_response()} | {error, invalid_callback | notfound | failed} | no_return().
process_callback(Tag, Callback) ->
case hg_machine_tag:get_binding(namespace(), Tag) of
{ok, _EntityID, MachineID} ->
process_with_tag(Tag, fun(MachineID) ->
case hg_machine:call(?NS, MachineID, {callback, Tag, Callback}) of
{ok, _} = Ok ->
Ok;
@ -226,7 +227,27 @@ process_callback(Tag, Callback) ->
{error, invalid_callback};
{error, _} = Error ->
Error
end;
end
end).
-spec process_session_change_by_tag(tag(), session_change()) ->
ok | {error, notfound | failed} | no_return().
process_session_change_by_tag(Tag, SessionChange) ->
process_with_tag(Tag, fun(MachineID) ->
case hg_machine:call(?NS, MachineID, {session_change, Tag, SessionChange}) of
ok ->
ok;
{exception, invalid_callback} ->
{error, notfound};
{error, _} = Error ->
Error
end
end).
process_with_tag(Tag, F) ->
case hg_machine_tag:get_binding(namespace(), Tag) of
{ok, _EntityID, MachineID} ->
F(MachineID);
{error, _} = Error ->
Error
end.
@ -339,7 +360,8 @@ handle_expiration(St) ->
-type thrift_call() :: hg_machine:thrift_call().
-type callback_call() :: {callback, tag(), callback()}.
-type call() :: thrift_call() | callback_call().
-type session_change_call() :: {session_change, tag(), session_change()}.
-type call() :: thrift_call() | callback_call() | session_change_call().
-type call_result() :: #{
changes => [invoice_change()],
action => hg_machine_action:t(),
@ -455,14 +477,20 @@ handle_call({{'Invoicing', 'CreatePaymentAdjustment'}, {_InvoiceID, PaymentID, P
hg_invoice_payment:create_adjustment(Timestamp, Params, PaymentSession, Opts),
St
);
handle_call({callback, Tag, Callback}, St) ->
dispatch_callback(Tag, Callback, St).
handle_call({callback, _Tag, _Callback} = Call, St) ->
dispatch_to_session(Call, St);
handle_call({session_change, _Tag, _SessionChange} = Call, St) ->
dispatch_to_session(Call, St).
-spec dispatch_callback(tag(), callback(), st()) -> call_result().
dispatch_callback(Tag, {provider, Payload}, St = #st{activity = {payment, PaymentID}}) ->
-spec dispatch_to_session({callback, tag(), callback()} | {session_change, tag(), session_change()}, st()) ->
call_result().
dispatch_to_session({callback, Tag, {provider, Payload}}, St = #st{activity = {payment, PaymentID}}) ->
PaymentSession = get_payment_session(PaymentID, St),
process_payment_call({callback, Tag, Payload}, PaymentID, PaymentSession, St);
dispatch_callback(_Tag, _Callback, _St) ->
dispatch_to_session({session_change, _Tag, _SessionChange} = Call, St = #st{activity = {payment, PaymentID}}) ->
PaymentSession = get_payment_session(PaymentID, St),
process_payment_call(Call, PaymentID, PaymentSession, St);
dispatch_to_session(_Call, _St) ->
throw(invalid_callback).
assert_no_pending_payment(#st{activity = {payment, PaymentID}}) ->

View File

@ -200,6 +200,7 @@
-type trx_info() :: dmsl_domain_thrift:'TransactionInfo'().
-type tag() :: dmsl_proxy_provider_thrift:'CallbackTag'().
-type callback() :: dmsl_proxy_provider_thrift:'Callback'().
-type session_change() :: hg_session:change().
-type callback_response() :: dmsl_proxy_provider_thrift:'CallbackResponse'().
-type make_recurrent() :: true | false.
-type retry_strategy() :: hg_retry:strategy().
@ -1924,12 +1925,20 @@ repair_process_timeout(Activity, Action, St = #st{repair_scenario = Scenario}) -
process_timeout(Activity, Action, St)
end.
-spec process_call({callback, tag(), callback()}, st(), opts()) -> {callback_response(), machine_result()}.
-spec process_call
({callback, tag(), callback()}, st(), opts()) -> {callback_response(), machine_result()};
({session_change, tag(), session_change()}, st(), opts()) -> {ok, machine_result()}.
process_call({callback, Tag, Payload}, St, Options) ->
scoper:scope(
payment,
get_st_meta(St),
fun() -> process_callback(Tag, Payload, St#st{opts = Options}) end
);
process_call({session_change, Tag, SessionChange}, St, Options) ->
scoper:scope(
payment,
get_st_meta(St),
fun() -> process_session_change(Tag, SessionChange, St#st{opts = Options}) end
).
-spec process_callback(tag(), callback(), st()) -> {callback_response(), machine_result()}.
@ -1937,6 +1946,11 @@ process_callback(Tag, Payload, St) ->
Session = get_activity_session(St),
process_callback(Tag, Payload, Session, St).
-spec process_session_change(tag(), session_change(), st()) -> {ok, machine_result()}.
process_session_change(Tag, SessionChange, St) ->
Session = get_activity_session(St),
process_session_change(Tag, SessionChange, Session, St).
process_callback(Tag, Payload, Session, St) when Session /= undefined ->
case {hg_session:status(Session), hg_session:tags(Session)} of
{suspended, [Tag | _]} ->
@ -1947,6 +1961,19 @@ process_callback(Tag, Payload, Session, St) when Session /= undefined ->
process_callback(_Tag, _Payload, undefined, _St) ->
throw(invalid_callback).
process_session_change(Tag, SessionChange, Session0, St) when Session0 /= undefined ->
%% NOTE Change allowed only for suspended session. Not suspended
%% session does not have registered callback with tag.
case {hg_session:status(Session0), hg_session:tags(Session0)} of
{suspended, [Tag | _]} ->
{Result, Session1} = hg_session:process_change(SessionChange, Session0),
{ok, finish_session_processing(get_activity(St), Result, Session1, St)};
_ ->
throw(invalid_callback)
end;
process_session_change(_Tag, _Payload, undefined, _St) ->
throw(invalid_callback).
%%
-spec process_shop_limit_initialization(action(), st()) -> machine_result().

View File

@ -49,11 +49,16 @@ handle_function('GetPayment', {Tag}, _) ->
end;
{error, notfound} ->
hg_woody_service_wrapper:raise(#proxy_provider_PaymentNotFound{})
end.
end;
handle_function('ChangePaymentSession', {Tag, SessionChange}, _) ->
handle_callback_result(hg_invoice:process_session_change_by_tag(Tag, SessionChange)).
-spec handle_callback_result
(ok) -> ok;
({ok, callback_response()}) -> callback_response();
({error, any()}) -> no_return().
handle_callback_result(ok) ->
ok;
handle_callback_result({ok, Response}) ->
Response;
handle_callback_result({error, invalid_callback}) ->

View File

@ -39,6 +39,7 @@
-type repair_scenario() :: {result, proxy_result()}.
-export_type([t/0]).
-export_type([change/0]).
-export_type([event_context/0]).
-export_type([process_result/0]).
@ -73,6 +74,7 @@
-export([process/1]).
-export([process_callback/2]).
-export([process_change/2]).
%% Internal types
@ -88,6 +90,7 @@
-type interaction() :: dmsl_user_interaction_thrift:'UserInteraction'().
-type payment_info() :: dmsl_proxy_provider_thrift:'PaymentInfo'().
-type timings() :: hg_timings:t().
-type change() :: dmsl_proxy_provider_thrift:'PaymentSessionChange'().
-type wrapped_event() :: dmsl_payproc_thrift:'InvoicePaymentChangePayload'().
-type wrapped_events() :: [wrapped_event()].
@ -201,6 +204,18 @@ process_callback(Payload, Session) ->
{Response, Result} = handle_callback_result(CallbackResult, Session),
{Response, apply_result(Result, Session)}.
-spec process_change(change(), t()) -> process_result().
process_change(#proxy_provider_PaymentSessionChange{status = {failure, Failure}}, Session) ->
SessionEvents = [
?session_activated(),
?session_finished(?session_failed({failure, Failure}))
],
Result = {SessionEvents, hg_machine_action:instant()},
apply_result(Result, Session);
process_change(_Change, _Session) ->
%% NOTE For now there is no other applicable change defined in protocol.
throw(unknown_change).
-spec deduce_activity(t()) -> activity().
deduce_activity(#{repair_scenario := Scenario}) when Scenario =/= undefined ->
repair;

View File

@ -431,7 +431,6 @@ create_shop_(
ShopAccountParams = #payproc_ShopAccountParams{currency = ?cur(Currency)},
ContractParams = make_contract_params(TemplateRef, PaymentInstRef),
PayoutToolParams = make_payout_tool_params(),
TurnoverLimits1 = genlib:define(TurnoverLimits0, ordsets:new()),
@ -440,14 +439,6 @@ create_shop_(
id = ContractID,
modification = {creation, ContractParams}
}},
{contract_modification, #payproc_ContractModificationUnit{
id = ContractID,
modification =
{payout_tool_modification, #payproc_PayoutToolModificationUnit{
payout_tool_id = PayoutToolID,
modification = {creation, PayoutToolParams}
}}
}},
?shop_modification(ShopID, {creation, ShopParams}),
?shop_modification(ShopID, {shop_account_creation, ShopAccountParams}),
?shop_modification(ShopID, {turnover_limits_modification, TurnoverLimits1})
@ -559,19 +550,6 @@ make_contractor() ->
russian_bank_account = BankAccount
}}}.
-spec make_payout_tool_params() -> dmsl_payproc_thrift:'PayoutToolParams'().
make_payout_tool_params() ->
#payproc_PayoutToolParams{
currency = ?cur(<<"RUB">>),
tool_info =
{russian_bank_account, #domain_RussianBankAccount{
account = <<"4276300010908312893">>,
bank_name = <<"SomeBank">>,
bank_post_account = <<"123129876">>,
bank_bik = <<"66642666">>
}}
}.
-spec make_invoice_params(party_id(), shop_id(), binary(), cash()) -> invoice_params().
make_invoice_params(PartyID, ShopID, Product, Cost) ->
make_invoice_params(PartyID, ShopID, Product, make_due_date(), Cost).

View File

@ -16,6 +16,8 @@
-export([get_callback_url/0]).
-export([construct_silent_callback/1]).
-export([change_payment_session/2]).
-export([make_payment_tool/2]).
-export([mk_trx/1]).
@ -733,6 +735,9 @@ get_payment_tool_scenario(
| {preauth_3ds, integer()}
| {preauth_3ds_sleep, integer()}.
-type tag() :: dmsl_proxy_provider_thrift:'CallbackTag'().
-type session_change() :: dmsl_proxy_provider_thrift:'PaymentSessionChange'().
-spec make_payment_tool(payment_tool_code(), payment_system()) -> payment_tool().
make_payment_tool(Code, PSys) when
Code =:= no_preauth orelse
@ -864,6 +869,15 @@ terminate(_Reason, _Req, _State) ->
get_callback_url() ->
genlib:to_binary("http://127.0.0.1:" ++ integer_to_list(?COWBOY_PORT)).
-spec change_payment_session(tag(), session_change()) -> ok | {exception, _Reason} | {error, _Reason}.
change_payment_session(Tag, Change) ->
Client = hg_client_api:new(hg_ct_helper:get_hellgate_url()),
case hg_client_api:call(proxy_host_provider, 'ChangePaymentSession', [Tag, Change], Client) of
{{ok, ok}, _} -> ok;
{{exception, _Reason} = Exception, _} -> Exception;
{{error, _Reason} = Error, _} -> Error
end.
handle_user_interaction_response(<<"POST">>, Req) ->
{ok, Body, Req2} = cowboy_req:read_body(Req),
Form = maps:from_list(cow_qs:parse_qs(Body)),

View File

@ -7,6 +7,7 @@
-include("hg_ct_domain.hrl").
-include("hg_ct_invoice.hrl").
-include_lib("damsel/include/dmsl_repair_thrift.hrl").
-include_lib("damsel/include/dmsl_proxy_provider_thrift.hrl").
-include_lib("hellgate/include/allocation.hrl").
-include_lib("fault_detector_proto/include/fd_proto_fault_detector_thrift.hrl").
@ -69,6 +70,7 @@
-export([payment_success_with_decreased_cost/1]).
-export([refund_payment_with_decreased_cost/1]).
-export([payment_fail_after_silent_callback/1]).
-export([payment_session_changed_to_fail/1]).
-export([invoice_success_on_third_payment/1]).
-export([party_revision_check/1]).
-export([payment_customer_risk_score_check/1]).
@ -331,6 +333,8 @@ groups() ->
payment_success_with_decreased_cost,
refund_payment_with_decreased_cost,
payment_fail_after_silent_callback,
payment_session_changed_to_fail,
payment_temporary_unavailability_retry_success,
payment_temporary_unavailability_too_many_retries,
invoice_success_on_third_payment,
@ -2337,6 +2341,39 @@ payment_fail_after_silent_callback(C) ->
_ = assert_success_post_request({URL, hg_dummy_provider:construct_silent_callback(Form)}),
PaymentID = await_payment_process_timeout(InvoiceID, PaymentID, Client).
-spec payment_session_changed_to_fail(config()) -> _ | no_return().
payment_session_changed_to_fail(C) ->
Client = cfg(client, C),
InvoiceID = start_invoice(<<"rubberdick">>, make_due_date(20), 42000, C),
%% Payment w/ preauth for suspend w/ user interaction occurrence.
PaymentID = start_payment(InvoiceID, make_tds_payment_params(instant, ?pmt_sys(<<"visa-ref">>)), Client),
UserInteraction = await_payment_process_interaction(InvoiceID, PaymentID, Client),
Failure = payproc_errors:construct(
'PaymentFailure',
{authorization_failed, {operation_blocked, ?err_gen_failure()}},
genlib:unique()
),
Change = #proxy_provider_PaymentSessionChange{status = {failure, Failure}},
%% Unknown session callback tag
?assertMatch(
{exception, #base_InvalidRequest{errors = [<<"Not found">>]}},
hg_dummy_provider:change_payment_session(<<"unknown tag">>, Change)
),
%% Since we expect UI to be a redirect, then parse tag value from
%% from request parameter.
Tag = user_interaction_callback_tag(UserInteraction),
ok = hg_dummy_provider:change_payment_session(Tag, Change),
{failed, PaymentID, {failure, Failure}} = await_payment_process_failure(InvoiceID, PaymentID, Client),
%% Bad session callback tag must not be found again
?assertMatch(
{exception, #base_InvalidRequest{errors = [<<"Not found">>]}},
hg_dummy_provider:change_payment_session(Tag, Change)
).
-spec payments_w_bank_card_issuer_conditions(config()) -> test_return().
payments_w_bank_card_issuer_conditions(C) ->
PmtSys = ?pmt_sys(<<"visa-ref">>),
@ -8308,6 +8345,13 @@ assert_success_post_request(Req) ->
assert_invalid_post_request(Req) ->
{ok, 400, _RespHeaders, _RespBody} = post_request(Req).
user_interaction_callback_tag(
{redirect, {post_request, #user_interaction_BrowserPostRequest{form = #{<<"tag">> := Tag}}}}
) ->
Tag;
user_interaction_callback_tag(_UserInteraction) ->
undefined.
post_request({URL, Form}) ->
Method = post,
Headers = [],

View File

@ -27,7 +27,7 @@ services:
command: /sbin/init
dominant:
image: ghcr.io/valitydev/dominant:sha-2150eea
image: ghcr.io/valitydev/dominant:sha-fae8726
command: /opt/dominant/bin/dominant foreground
depends_on:
machinegun:
@ -97,7 +97,7 @@ services:
disable: true
party-management:
image: ghcr.io/valitydev/party-management:sha-9af7d71
image: ghcr.io/valitydev/party-management:sha-b78d0f5
command: /opt/party-management/bin/party-management foreground
depends_on:
machinegun:

View File

@ -21,7 +21,7 @@
{<<"ctx">>,{pkg,<<"ctx">>,<<"0.6.0">>},2},
{<<"damsel">>,
{git,"https://github.com/valitydev/damsel.git",
{ref,"9d4aa513fcbc1cc7ba5eedd9f96d8bc8590a6ac2"}},
{ref,"b149379e9f706dafcace7d36b124145d71fb1bc6"}},
0},
{<<"dmt_client">>,
{git,"https://github.com/valitydev/dmt-client.git",