From fa7cd47a79266ddca3984196875bfed5dff92333 Mon Sep 17 00:00:00 2001 From: Aleksey Kashapov Date: Mon, 14 Oct 2024 12:56:00 +0300 Subject: [PATCH] IMP-331: Implements provider host's session change handler (#145) * 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 --- apps/hellgate/src/hg_invoice.erl | 56 ++++++++++++++----- apps/hellgate/src/hg_invoice_payment.erl | 29 +++++++++- apps/hellgate/src/hg_proxy_host_provider.erl | 7 ++- apps/hellgate/src/hg_session.erl | 15 +++++ apps/hellgate/test/hg_ct_helper.erl | 22 -------- apps/hellgate/test/hg_dummy_provider.erl | 14 +++++ apps/hellgate/test/hg_invoice_tests_SUITE.erl | 44 +++++++++++++++ compose.yaml | 4 +- rebar.lock | 2 +- 9 files changed, 152 insertions(+), 41 deletions(-) diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index da82a9c..0e0d423 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -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,22 +212,42 @@ 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) -> + process_with_tag(Tag, fun(MachineID) -> + case hg_machine:call(?NS, MachineID, {callback, Tag, Callback}) of + {ok, _} = Ok -> + Ok; + {exception, invalid_callback} -> + {error, invalid_callback}; + {error, _} = Error -> + Error + 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} -> - case hg_machine:call(?NS, MachineID, {callback, Tag, Callback}) of - {ok, _} = Ok -> - Ok; - {exception, invalid_callback} -> - {error, invalid_callback}; - {error, _} = Error -> - Error - end; + 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}}) -> diff --git a/apps/hellgate/src/hg_invoice_payment.erl b/apps/hellgate/src/hg_invoice_payment.erl index 6bb1866..0670b4d 100644 --- a/apps/hellgate/src/hg_invoice_payment.erl +++ b/apps/hellgate/src/hg_invoice_payment.erl @@ -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(). diff --git a/apps/hellgate/src/hg_proxy_host_provider.erl b/apps/hellgate/src/hg_proxy_host_provider.erl index 197c4a8..44c89da 100644 --- a/apps/hellgate/src/hg_proxy_host_provider.erl +++ b/apps/hellgate/src/hg_proxy_host_provider.erl @@ -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}) -> diff --git a/apps/hellgate/src/hg_session.erl b/apps/hellgate/src/hg_session.erl index b64c047..312c413 100644 --- a/apps/hellgate/src/hg_session.erl +++ b/apps/hellgate/src/hg_session.erl @@ -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; diff --git a/apps/hellgate/test/hg_ct_helper.erl b/apps/hellgate/test/hg_ct_helper.erl index 7e01722..716d7a7 100644 --- a/apps/hellgate/test/hg_ct_helper.erl +++ b/apps/hellgate/test/hg_ct_helper.erl @@ -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). diff --git a/apps/hellgate/test/hg_dummy_provider.erl b/apps/hellgate/test/hg_dummy_provider.erl index 60002a0..4a88b6d 100644 --- a/apps/hellgate/test/hg_dummy_provider.erl +++ b/apps/hellgate/test/hg_dummy_provider.erl @@ -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)), diff --git a/apps/hellgate/test/hg_invoice_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_tests_SUITE.erl index b737a4f..33ce7fe 100644 --- a/apps/hellgate/test/hg_invoice_tests_SUITE.erl +++ b/apps/hellgate/test/hg_invoice_tests_SUITE.erl @@ -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 = [], diff --git a/compose.yaml b/compose.yaml index 2f6be76..ebb7c10 100644 --- a/compose.yaml +++ b/compose.yaml @@ -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: diff --git a/rebar.lock b/rebar.lock index 82a23b1..81c22a0 100644 --- a/rebar.lock +++ b/rebar.lock @@ -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",