diff --git a/apps/hellgate/src/hg_invoice_payment.erl b/apps/hellgate/src/hg_invoice_payment.erl index 74b2db3..8d4db6d 100644 --- a/apps/hellgate/src/hg_invoice_payment.erl +++ b/apps/hellgate/src/hg_invoice_payment.erl @@ -1901,6 +1901,12 @@ process_refund_result(Changes, Refund0, St) -> repair_process_timeout(Activity, Action, St = #st{repair_scenario = Scenario}) -> case hg_invoice_repair:check_for_action(fail_pre_processing, Scenario) of + {result, Result} when + Activity =:= {payment, routing} orelse + Activity =:= {payment, cash_flow_building} + -> + rollback_broken_payment_limits(St), + Result; {result, Result} -> Result; call -> @@ -2496,6 +2502,33 @@ rollback_payment_limits(Routes, Iter, St, Flags) -> Routes ). +rollback_broken_payment_limits(St) -> + Opts = get_opts(St), + Payment = get_payment(St), + Invoice = get_invoice(Opts), + LimitValues = get_limit_values(St), + Iter = maps:size(LimitValues), + maps:fold( + fun + (_Route, [], Acc) -> + Acc; + (Route, Values, _Acc) -> + TurnoverLimits = + lists:foldl( + fun(#payproc_TurnoverLimitValue{limit = TurnoverLimit}, Acc1) -> + [TurnoverLimit | Acc1] + end, + [], + Values + ), + ok = hg_limiter:rollback_payment_limits(TurnoverLimits, Route, Iter, Invoice, Payment, [ + ignore_business_error + ]) + end, + ok, + LimitValues + ). + rollback_unused_payment_limits(St) -> Route = get_route(St), Routes = get_candidate_routes(St), @@ -2902,6 +2935,7 @@ merge_change(Change = ?payment_status_changed({failed, _} = Status), #st{payment || S <- [ risk_scoring, routing, + cash_flow_building, routing_failure, processing_failure ] diff --git a/apps/hellgate/src/hg_invoice_repair.erl b/apps/hellgate/src/hg_invoice_repair.erl index d091fc3..3a644f0 100644 --- a/apps/hellgate/src/hg_invoice_repair.erl +++ b/apps/hellgate/src/hg_invoice_repair.erl @@ -81,15 +81,11 @@ get_repair_state(Activity, Scenario, St) -> ok = check_activity_compatibility(Scenario, Activity), hg_invoice_payment:set_repair_scenario(Scenario, St). --define(SCENARIO_COMPLEX(Scenarios), {complex, #payproc_InvoiceRepairComplex{scenarios = Scenarios}}). -define(SCENARIO_FAIL_PRE_PROCESSING, {fail_pre_processing, #payproc_InvoiceRepairFailPreProcessing{}}). -define(SCENARIO_SKIP_INSPECTOR, {skip_inspector, #payproc_InvoiceRepairSkipInspector{}}). -define(SCENARIO_FAIL_SESSION, {fail_session, #payproc_InvoiceRepairFailSession{}}). -define(SCENARIO_FULFILL_SESSION, {fulfill_session, #payproc_InvoiceRepairFulfillSession{}}). -% TODO(TD-221): This case is not used anywhere? hg_invoice:repair_complex applies scenarios one by one earlier. -check_activity_compatibility(?SCENARIO_COMPLEX(Scenarios), Activity) -> - lists:foreach(fun(Sc) -> check_activity_compatibility(Sc, Activity) end, Scenarios); -% TODO(TD-221): {payment, new}, routing and cash_flow_building are untested +% TODO(TD-221): {payment, new} is untested check_activity_compatibility(?SCENARIO_FAIL_PRE_PROCESSING, Activity) when Activity =:= {payment, new} orelse Activity =:= {payment, risk_scoring} orelse diff --git a/apps/hellgate/test/hg_invoice_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_tests_SUITE.erl index a3678d7..01cd1c0 100644 --- a/apps/hellgate/test/hg_invoice_tests_SUITE.erl +++ b/apps/hellgate/test/hg_invoice_tests_SUITE.erl @@ -190,6 +190,9 @@ -export([repair_fulfill_session_on_refund_succeeded/1]). -export([repair_fulfill_session_on_captured_succeeded/1]). +-export([repair_fail_routing_succeeded/1]). +-export([repair_fail_cash_flow_building_succeeded/1]). + -export([consistent_account_balances/1]). -export([allocation_create_invoice/1]). @@ -254,6 +257,7 @@ all() -> {group, chargebacks}, rounding_cashflow_volume, terms_retrieval, + {group, repair_preproc_w_limits}, consistent_account_balances ]. @@ -470,6 +474,10 @@ groups() -> repair_fulfill_session_on_refund_succeeded, repair_fulfill_session_on_captured_succeeded ]}, + {repair_preproc_w_limits, [], [ + repair_fail_routing_succeeded, + repair_fail_cash_flow_building_succeeded + ]}, {allocation, [parallel], [ allocation_create_invoice, allocation_capture_payment, @@ -668,6 +676,8 @@ init_per_group(route_cascading, C) -> init_operation_limits_group(C); init_per_group(operation_limits, C) -> init_operation_limits_group(C); +init_per_group(repair_preproc_w_limits, C) -> + init_operation_limits_group(C); init_per_group(allocation, C) -> init_allocation_group(C); init_per_group(_, C) -> @@ -727,9 +737,29 @@ init_per_testcase(limit_hold_payment_tool_not_supported, C) -> override_domain_fixture(fun patch_with_unsupported_payment_tool/1, C); init_per_testcase(limit_hold_two_routes_failure, C) -> override_domain_fixture(fun patch_providers_limits_to_fail_and_overflow/1, C); +init_per_testcase(repair_fail_routing_succeeded, C) -> + meck:expect( + hg_limiter, + check_limits, + fun override_check_limits/4 + ), + init_per_testcase(C); +init_per_testcase(repair_fail_cash_flow_building_succeeded, C) -> + meck:expect( + hg_cashflow_utils, + collect_cashflow, + fun override_collect_cashflow/1 + ), + init_per_testcase(C); init_per_testcase(_Name, C) -> init_per_testcase(C). +override_check_limits(_, _, _, _) -> throw(unknown). +-dialyzer({nowarn_function, override_check_limits/4}). + +override_collect_cashflow(_) -> throw(unknown). +-dialyzer({nowarn_function, override_collect_cashflow/1}). + override_domain_fixture(Fixture, C) -> Revision = hg_domain:head(), _ = hg_domain:upsert(Fixture(Revision)), @@ -743,6 +773,12 @@ init_per_testcase(C) -> [{client, Client}, {client_tpl, ClientTpl} | C]. -spec end_per_testcase(test_case_name(), config()) -> _. +end_per_testcase(repair_fail_routing_succeeded, C) -> + meck:unload(hg_limiter), + end_per_testcase(default, C); +end_per_testcase(repair_fail_cash_flow_building_succeeded, C) -> + meck:unload(hg_cashflow_utils), + end_per_testcase(default, C); end_per_testcase(_Name, C) -> ok = hg_context:cleanup(), _ = @@ -5431,6 +5467,108 @@ payment_with_tokenized_bank_card(C) -> [?payment_state(?payment_w_status(PaymentID, ?captured()))] ) = hg_client_invoicing:get(InvoiceID, Client). +-spec repair_fail_routing_succeeded(config()) -> test_return(). +repair_fail_routing_succeeded(C) -> + RootUrl = cfg(root_url, C), + Client = hg_client_invoicing:start_link(hg_ct_helper:create_client(RootUrl)), + PartyClient = cfg(party_client, C), + #{party_id := PartyID} = cfg(limits, C), + ShopID = hg_ct_helper:create_shop(PartyID, ?cat(8), <<"RUB">>, ?tmpl(1), ?pinst(1), PartyClient), + + %% Invoice + InvoiceParams = make_invoice_params(PartyID, ShopID, <<"rubberduck">>, make_due_date(10), make_cash(10000)), + InvoiceID = create_invoice(InvoiceParams, Client), + ?invoice_created(?invoice_w_status(?invoice_unpaid())) = next_change(InvoiceID, Client), + + %% Payment + PaymentParams = make_payment_params(?pmt_sys(<<"visa-ref">>)), + ?payment_state(?payment(PaymentID)) = hg_client_invoicing:start_payment(InvoiceID, PaymentParams, Client), + ?payment_ev(PaymentID, ?payment_started(?payment_w_status(?pending()))) = next_change(InvoiceID, Client), + ?payment_ev(PaymentID, ?risk_score_changed(_RS)) = next_change(InvoiceID, Client), + %% routing broken + timeout = next_change(InvoiceID, 2000, Client), + + %% Limits hold + Route = ?route(?prv(5), ?trm(12)), + #{ + Route := [ + #payproc_TurnoverLimitValue{ + limit = #domain_TurnoverLimit{id = ?LIMIT_ID, upper_boundary = ?LIMIT_UPPER_BOUNDARY}, + value = 10000 + } + ] + } = hg_client_invoicing:get_limit_values(InvoiceID, PaymentID, Client), + + %% Repair with rollback limits + ok = repair_invoice_with_scenario(InvoiceID, fail_pre_processing, Client), + + %% Check final status + ?payment_ev(PaymentID, ?payment_status_changed(?failed({failure, _Failure}))) = next_change(InvoiceID, Client), + + %% Check limits rolled back + #{ + Route := [ + #payproc_TurnoverLimitValue{ + limit = #domain_TurnoverLimit{id = ?LIMIT_ID, upper_boundary = ?LIMIT_UPPER_BOUNDARY}, + value = 0 + } + ] + } = hg_client_invoicing:get_limit_values(InvoiceID, PaymentID, Client), + + %% Check duplicate repair + {exception, {base_InvalidRequest, [<<"No need to repair">>]}} = repair_invoice_with_scenario( + InvoiceID, fail_pre_processing, Client + ). + +%% fail cash_flow_building before accounting hold +-spec repair_fail_cash_flow_building_succeeded(config()) -> test_return(). +repair_fail_cash_flow_building_succeeded(C) -> + RootUrl = cfg(root_url, C), + Client = hg_client_invoicing:start_link(hg_ct_helper:create_client(RootUrl)), + PartyClient = cfg(party_client, C), + #{party_id := PartyID} = cfg(limits, C), + ShopID = hg_ct_helper:create_shop(PartyID, ?cat(8), <<"RUB">>, ?tmpl(1), ?pinst(1), PartyClient), + + %% Invoice + InvoiceParams = make_invoice_params(PartyID, ShopID, <<"rubberduck">>, make_due_date(10), make_cash(10000)), + InvoiceID = create_invoice(InvoiceParams, Client), + ?invoice_created(?invoice_w_status(?invoice_unpaid())) = next_change(InvoiceID, Client), + + %% Payment + PaymentParams = make_payment_params(?pmt_sys(<<"visa-ref">>)), + ?payment_state(?payment(PaymentID)) = hg_client_invoicing:start_payment(InvoiceID, PaymentParams, Client), + ?payment_ev(PaymentID, ?payment_started(?payment_w_status(?pending()))) = next_change(InvoiceID, Client), + ?payment_ev(PaymentID, ?risk_score_changed(_RS)) = next_change(InvoiceID, Client), + ?payment_ev(PaymentID, ?route_changed(Route)) = next_change(InvoiceID, Client), + %% cash_flow_building broken + timeout = next_change(InvoiceID, 2000, Client), + + %% Limits hold + #{ + Route := [ + #payproc_TurnoverLimitValue{ + limit = #domain_TurnoverLimit{id = ?LIMIT_ID, upper_boundary = ?LIMIT_UPPER_BOUNDARY}, + value = 10000 + } + ] + } = hg_client_invoicing:get_limit_values(InvoiceID, PaymentID, Client), + + %% Repair + ok = repair_invoice_with_scenario(InvoiceID, fail_pre_processing, Client), + + %% Check final status + ?payment_ev(PaymentID, ?payment_status_changed(?failed({failure, _Failure}))) = next_change(InvoiceID, Client), + + %% Check limits rolled back + #{ + Route := [ + #payproc_TurnoverLimitValue{ + limit = #domain_TurnoverLimit{id = ?LIMIT_ID, upper_boundary = ?LIMIT_UPPER_BOUNDARY}, + value = 0 + } + ] + } = hg_client_invoicing:get_limit_values(InvoiceID, PaymentID, Client). + -spec repair_fail_pre_processing_succeeded(config()) -> test_return(). repair_fail_pre_processing_succeeded(C) -> Client = cfg(client, C), diff --git a/rebar.config b/rebar.config index d6cf076..0087afb 100644 --- a/rebar.config +++ b/rebar.config @@ -98,7 +98,10 @@ ]} ]}, {test, [ - {dialyzer, [{plt_extra_apps, [eunit, common_test, runtime_tools, damsel]}]} + {deps, [ + {meck, "0.9.2"} + ]}, + {dialyzer, [{plt_extra_apps, [eunit, common_test, runtime_tools, damsel, meck]}]} ]} ]}.