mirror of
https://github.com/valitydev/hellgate.git
synced 2024-11-06 02:45:20 +00:00
TD-820: Fixes route candidates on no-route failure rollback after limit overflow (#115)
* TD-820: Fixes route candidates on no-route failure rollback after limit overflow * Adds 'ignore_not_found' flag support for limit rollback * Don't stash empty candidates list * Adds ui cascade test cases
This commit is contained in:
parent
4321821953
commit
bdeefbf2d2
@ -1976,9 +1976,12 @@ produce_routing_events(Ctx = #{error := Error}, _Revision, St) when Error =/= un
|
||||
%% rejected because of provider gone critical, then use subcode to highlight
|
||||
%% the offender. Like 'provider_dead' or 'conversion_lacking'.
|
||||
Failure = genlib:define(St#st.failure, construct_routing_failure(Error)),
|
||||
InitialCandidates = [hg_route:to_payment_route(R) || R <- hg_routing_ctx:initial_candidates(Ctx)],
|
||||
Route = hd(InitialCandidates),
|
||||
Candidates = ordsets:from_list(InitialCandidates),
|
||||
%% NOTE Not all initial candidates have their according limits held. And so
|
||||
%% we must account only for those that can be rolled back.
|
||||
RollbackableCandidates = hg_routing_ctx:accounted_candidates(Ctx),
|
||||
Route = hg_route:to_payment_route(hd(RollbackableCandidates)),
|
||||
Candidates =
|
||||
ordsets:from_list([hg_route:to_payment_route(R) || R <- RollbackableCandidates]),
|
||||
%% For protocol compatability we set choosen route in route_changed event.
|
||||
%% It doesn't influence cash_flow building because this step will be
|
||||
%% skipped. And all limit's 'hold' operations will be rolled back.
|
||||
@ -2201,7 +2204,7 @@ finish_session_processing(Activity, {Events0, Action}, Session, St0) ->
|
||||
%% Previously used routes are supposed to have their limits already rolled back.
|
||||
Route = get_route(St0),
|
||||
Routes = [Route],
|
||||
_ = rollback_payment_limits(Routes, get_iter(St0), St0),
|
||||
_ = rollback_payment_limits(Routes, get_iter(St0), St0, []),
|
||||
_ = rollback_payment_cashflow(St0);
|
||||
_ ->
|
||||
ok
|
||||
@ -2285,7 +2288,7 @@ process_result({payment, processing_accounter}, Action, St) ->
|
||||
process_result({payment, routing_failure}, Action, St = #st{failure = Failure}) ->
|
||||
NewAction = hg_machine_action:set_timeout(0, Action),
|
||||
Routes = get_candidate_routes(St),
|
||||
_ = rollback_payment_limits(Routes, get_iter(St), St, [ignore_business_error]),
|
||||
_ = rollback_payment_limits(Routes, get_iter(St), St, [ignore_business_error, ignore_not_found]),
|
||||
{done, {[?payment_status_changed(?failed(Failure))], NewAction}};
|
||||
process_result({payment, processing_failure}, Action, St = #st{failure = Failure}) ->
|
||||
NewAction = hg_machine_action:set_timeout(0, Action),
|
||||
@ -2293,7 +2296,7 @@ process_result({payment, processing_failure}, Action, St = #st{failure = Failure
|
||||
%% Previously used routes are supposed to have their limits already rolled back.
|
||||
Route = get_route(St),
|
||||
Routes = [Route],
|
||||
_ = rollback_payment_limits(Routes, get_iter(St), St),
|
||||
_ = rollback_payment_limits(Routes, get_iter(St), St, []),
|
||||
_ = rollback_payment_cashflow(St),
|
||||
Revision = get_payment_revision(St),
|
||||
Behaviour = get_route_cascade_behaviour(Route, Revision),
|
||||
@ -2310,7 +2313,7 @@ process_result({payment, finalizing_accounter}, Action, St) ->
|
||||
commit_payment_cashflow(St);
|
||||
?cancelled() ->
|
||||
Route = get_route(St),
|
||||
_ = rollback_payment_limits([Route], get_iter(St), St),
|
||||
_ = rollback_payment_limits([Route], get_iter(St), St, []),
|
||||
rollback_payment_cashflow(St)
|
||||
end,
|
||||
check_recurrent_token(St),
|
||||
@ -2548,9 +2551,6 @@ do_reject_route(LimiterError, Route, TurnoverLimits, {LimitHeldRoutes, RejectedR
|
||||
RejectedRoute = hg_route:to_rejected_route(Route, {'LimitHoldError', LimitsIDs, LimiterError}),
|
||||
{LimitHeldRoutes, [RejectedRoute | RejectedRoutes]}.
|
||||
|
||||
rollback_payment_limits(Routes, Iter, St) ->
|
||||
rollback_payment_limits(Routes, Iter, St, []).
|
||||
|
||||
rollback_payment_limits(Routes, Iter, St, Flags) ->
|
||||
Opts = get_opts(St),
|
||||
Revision = get_payment_revision(St),
|
||||
@ -2597,7 +2597,7 @@ rollback_unused_payment_limits(St) ->
|
||||
Route = get_route(St),
|
||||
Routes = get_candidate_routes(St),
|
||||
UnUsedRoutes = Routes -- [Route],
|
||||
rollback_payment_limits(UnUsedRoutes, get_iter(St), St, [ignore_business_error]).
|
||||
rollback_payment_limits(UnUsedRoutes, get_iter(St), St, [ignore_business_error, ignore_not_found]).
|
||||
|
||||
get_turnover_limits(ProviderTerms) ->
|
||||
TurnoverLimitSelector = ProviderTerms#domain_PaymentsProvisionTerms.turnover_limits,
|
||||
|
@ -14,7 +14,7 @@
|
||||
-type route() :: hg_route:payment_route().
|
||||
-type refund() :: hg_invoice_payment:domain_refund().
|
||||
-type cash() :: dmsl_domain_thrift:'Cash'().
|
||||
-type handling_flag() :: ignore_business_error.
|
||||
-type handling_flag() :: ignore_business_error | ignore_not_found.
|
||||
-type turnover_limit_value() :: dmsl_payproc_thrift:'TurnoverLimitValue'().
|
||||
|
||||
-type change_queue() :: [hg_limiter_client:limit_change()].
|
||||
@ -129,6 +129,14 @@ commit_refund_limits(TurnoverLimits, Invoice, Payment, Refund, Route) ->
|
||||
ok = commit(LimitChanges, Clock, Context),
|
||||
ok = log_limit_changes(TurnoverLimits, Clock, Context).
|
||||
|
||||
%% @doc This function supports flags that can change reaction behaviour to
|
||||
%% limiter response:
|
||||
%%
|
||||
%% - `ignore_business_error` -- prevents error raise upon misconfiguration
|
||||
%% failures in limiter config
|
||||
%%
|
||||
%% - `ignore_not_found` -- does not raise error if limiter won't be able to
|
||||
%% find according posting plan in accountant service
|
||||
-spec rollback_payment_limits([turnover_limit()], route(), pos_integer(), invoice(), payment(), [handling_flag()]) ->
|
||||
ok.
|
||||
rollback_payment_limits(TurnoverLimits, Route, Iter, Invoice, Payment, Flags) ->
|
||||
@ -167,16 +175,26 @@ process_changes(LimitChangesQueues, WithFun, Clock, Context, Flags) ->
|
||||
LimitChangesQueues
|
||||
).
|
||||
|
||||
process_changes_try_wrap([LimitChange], WithFun, Clock, Context, _Flags) ->
|
||||
WithFun(LimitChange, Clock, Context);
|
||||
%% Very specific error to crutch around
|
||||
-define(POSTING_PLAN_NOT_FOUND(ID), #base_InvalidRequest{errors = [<<"Posting plan not found: ", ID/binary>>]}).
|
||||
|
||||
process_changes_try_wrap([LimitChange], WithFun, Clock, Context, Flags) ->
|
||||
IgnoreNotFound = lists:member(ignore_not_found, Flags),
|
||||
#limiter_LimitChange{change_id = ChangeID} = LimitChange,
|
||||
try
|
||||
WithFun(LimitChange, Clock, Context)
|
||||
catch
|
||||
error:(?POSTING_PLAN_NOT_FOUND(ChangeID)) when IgnoreNotFound =:= true ->
|
||||
%% See `limproto_limiter_thrift:'Clock'/0`
|
||||
{latest, #limiter_LatestClock{}}
|
||||
end;
|
||||
process_changes_try_wrap([LimitChange | OtherLimitChanges], WithFun, Clock, Context, Flags) ->
|
||||
IgnoreBusinessError = lists:member(ignore_business_error, Flags),
|
||||
#limiter_LimitChange{change_id = ChangeID} = LimitChange,
|
||||
try
|
||||
WithFun(LimitChange, Clock, Context)
|
||||
catch
|
||||
%% Very specific error to crutch around
|
||||
error:#base_InvalidRequest{errors = [<<"Posting plan not found: ", ChangeID/binary>>]} ->
|
||||
error:(?POSTING_PLAN_NOT_FOUND(ChangeID)) ->
|
||||
process_changes_try_wrap(OtherLimitChanges, WithFun, Clock, Context, Flags);
|
||||
Class:Reason:Stacktrace ->
|
||||
handle_caught_exception(Class, Reason, Stacktrace, IgnoreBusinessError)
|
||||
|
@ -13,6 +13,7 @@
|
||||
-export([initial_candidates/1]).
|
||||
-export([stash_current_candidates/1]).
|
||||
-export([considered_candidates/1]).
|
||||
-export([accounted_candidates/1]).
|
||||
-export([choosen_route/1]).
|
||||
-export([process/2]).
|
||||
-export([with_guard/1]).
|
||||
@ -127,7 +128,17 @@ initial_candidates(#{initial_candidates := InitialCandidates}) ->
|
||||
considered_candidates(Ctx) ->
|
||||
maps:get(stashed_candidates, Ctx, candidates(Ctx)).
|
||||
|
||||
%% @doc Same as 'considered_candidates/1' except for it fallbacks to initial
|
||||
%% candidates if no were stashed.
|
||||
%%
|
||||
%% Its use-case is simillar to 'considered_candidates/1' as well.
|
||||
-spec accounted_candidates(t()) -> [hg_route:t()].
|
||||
accounted_candidates(Ctx) ->
|
||||
maps:get(stashed_candidates, Ctx, initial_candidates(Ctx)).
|
||||
|
||||
-spec stash_current_candidates(t()) -> t().
|
||||
stash_current_candidates(Ctx = #{candidates := []}) ->
|
||||
Ctx;
|
||||
stash_current_candidates(Ctx) ->
|
||||
Ctx#{stashed_candidates => candidates(Ctx)}.
|
||||
|
||||
|
@ -263,24 +263,16 @@ token_respond(Response, CallbackResult) ->
|
||||
%
|
||||
% Payments
|
||||
%
|
||||
process_payment(
|
||||
?processed(),
|
||||
undefined,
|
||||
PaymentInfo,
|
||||
#{<<"always_fail">> := FailureCode, <<"override">> := ProviderCode} = CtxOpts,
|
||||
_
|
||||
) ->
|
||||
_ = maybe_sleep(CtxOpts),
|
||||
Reason = <<"sub failure by ", ProviderCode/binary>>,
|
||||
Failure = payproc_errors:from_notation(FailureCode, <<"sub failure by ", ProviderCode/binary>>),
|
||||
TrxID = hg_utils:construct_complex_id([get_payment_id(PaymentInfo), get_ctx_opts_override(CtxOpts)]),
|
||||
result(?finish({failure, Failure}), <<"state: ", Reason/binary>>, mk_trx(TrxID, PaymentInfo));
|
||||
process_payment(?processed(), undefined, PaymentInfo, CtxOpts, _) ->
|
||||
case get_payment_info_scenario(PaymentInfo) of
|
||||
{preauth_3ds, Timeout} ->
|
||||
Tag = generate_tag(<<"payment">>),
|
||||
Uri = get_callback_url(),
|
||||
result(?suspend(Tag, Timeout, ?redirect(Uri, #{<<"tag">> => Tag})), <<"suspended">>);
|
||||
SuspendWithUI = result(?suspend(Tag, Timeout, ?redirect(Uri, #{<<"tag">> => Tag})), <<"suspended">>),
|
||||
case CtxOpts of
|
||||
#{<<"allow_ui">> := _} -> SuspendWithUI;
|
||||
_ -> maybe_fail(PaymentInfo, CtxOpts, SuspendWithUI)
|
||||
end;
|
||||
change_cash_increase ->
|
||||
%% simple workflow without 3DS
|
||||
result(?sleep(0), <<"sleeping">>);
|
||||
@ -289,7 +281,7 @@ process_payment(?processed(), undefined, PaymentInfo, CtxOpts, _) ->
|
||||
result(?sleep(0), <<"sleeping">>);
|
||||
no_preauth ->
|
||||
%% simple workflow without 3DS
|
||||
result(?sleep(0), <<"sleeping">>);
|
||||
maybe_fail(PaymentInfo, CtxOpts, result(?sleep(0), <<"sleeping">>));
|
||||
empty_cvv ->
|
||||
%% simple workflow without 3DS
|
||||
result(?sleep(0), <<"sleeping">>);
|
||||
@ -352,7 +344,7 @@ process_payment(?processed(), <<"sleeping">>, PaymentInfo, CtxOpts, _) ->
|
||||
{temporary_unavailability, Scenario} ->
|
||||
process_failure_scenario(PaymentInfo, Scenario, TrxID);
|
||||
_ ->
|
||||
finish(success(PaymentInfo), mk_trx(TrxID, PaymentInfo))
|
||||
maybe_fail(PaymentInfo, CtxOpts, finish(success(PaymentInfo), mk_trx(TrxID, PaymentInfo)))
|
||||
end;
|
||||
process_payment(?processed(), <<"sleeping_with_user_interaction">>, PaymentInfo, CtxOpts, _) ->
|
||||
Key = {get_invoice_id(PaymentInfo), get_payment_id(PaymentInfo)},
|
||||
@ -546,6 +538,15 @@ result(Intent, NextState, Trx) ->
|
||||
trx = Trx
|
||||
}.
|
||||
|
||||
maybe_fail(PaymentInfo, #{<<"always_fail">> := FailureCode, <<"override">> := ProviderCode} = CtxOpts, _OrElse) ->
|
||||
_ = maybe_sleep(CtxOpts),
|
||||
Reason = <<"sub failure by ", ProviderCode/binary>>,
|
||||
Failure = payproc_errors:from_notation(FailureCode, <<"sub failure by ", ProviderCode/binary>>),
|
||||
TrxID = hg_utils:construct_complex_id([get_payment_id(PaymentInfo), get_ctx_opts_override(CtxOpts)]),
|
||||
result(?finish({failure, Failure}), <<"state: ", Reason/binary>>, mk_trx(TrxID, PaymentInfo));
|
||||
maybe_fail(_PaymentInfo, _CtxOpts, OrElse) ->
|
||||
OrElse.
|
||||
|
||||
-spec mk_trx(binary()) -> dmsl_domain_thrift:'TransactionInfo'().
|
||||
mk_trx(TrxID) ->
|
||||
mk_trx_w_extra(TrxID, #{}).
|
||||
|
@ -195,6 +195,7 @@
|
||||
-export([payment_cascade_fail_wo_route_candidates/1]).
|
||||
-export([payment_cascade_success_w_refund/1]).
|
||||
-export([payment_big_cascade_success/1]).
|
||||
-export([payment_cascade_limit_overflow/1]).
|
||||
-export([payment_cascade_fail_wo_available_attempt_limit/1]).
|
||||
-export([payment_cascade_failures/1]).
|
||||
-export([payment_cascade_deadline_failures/1]).
|
||||
@ -482,11 +483,12 @@ groups() ->
|
||||
payment_cascade_fail_wo_route_candidates,
|
||||
payment_cascade_success_w_refund,
|
||||
payment_big_cascade_success,
|
||||
payment_cascade_limit_overflow,
|
||||
payment_cascade_fail_wo_available_attempt_limit,
|
||||
payment_cascade_failures,
|
||||
payment_cascade_deadline_failures,
|
||||
payment_cascade_fail_provider_error
|
||||
%% payment_cascade_fail_ui
|
||||
payment_cascade_fail_provider_error,
|
||||
payment_cascade_fail_ui
|
||||
]}
|
||||
].
|
||||
|
||||
@ -5931,9 +5933,12 @@ consistent_account_balances(C) ->
|
||||
-define(PAYMENT_CASCADE_DEADLINE_FAILURES_ID, 700).
|
||||
-define(PAYMENT_CASCADE_FAIL_PROVIDER_ERROR_ID, 800).
|
||||
-define(PAYMENT_CASCADE_FAIL_UI_ID, 900).
|
||||
-define(PAYMENT_CASCADE_LIMIT_OVERFLOW_ID, 1000).
|
||||
|
||||
cascade_fixture_pre_shop_create(Revision, C) ->
|
||||
payment_big_cascade_success_fixture_pre(Revision, C) ++
|
||||
payment_cascade_limit_overflow_fixture_pre(Revision, C) ++
|
||||
payment_cascade_fail_ui_fixture_pre(Revision, C) ++
|
||||
payment_cascade_fail_wo_route_candidates_fixture_pre(Revision, C) ++
|
||||
payment_cascade_fail_wo_available_attempt_limit_fixture_pre(Revision, C) ++
|
||||
payment_cascade_fail_provider_error_fixture_pre(Revision, C).
|
||||
@ -5944,7 +5949,7 @@ cascade_fixture(Revision, C) ->
|
||||
[
|
||||
hg_ct_fixture:construct_payment_routing_ruleset(
|
||||
?ruleset(2),
|
||||
<<"2 routes with failing providers">>,
|
||||
<<"Multiple routes with failing providers">>,
|
||||
{delegates, [
|
||||
?delegate(
|
||||
?partycond(PartyID, {shop_is, cfg({shop_id, ?PAYMENT_CASCADE_SUCCESS_ID}, C)}),
|
||||
@ -5980,14 +5985,21 @@ cascade_fixture(Revision, C) ->
|
||||
?delegate(
|
||||
?partycond(PartyID, {shop_is, cfg({shop_id, ?PAYMENT_CASCADE_FAIL_PROVIDER_ERROR_ID}, C)}),
|
||||
?ruleset(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_FAIL_PROVIDER_ERROR_ID))
|
||||
),
|
||||
?delegate(
|
||||
?partycond(PartyID, {shop_is, cfg({shop_id, ?PAYMENT_CASCADE_LIMIT_OVERFLOW_ID}, C)}),
|
||||
?ruleset(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_LIMIT_OVERFLOW_ID))
|
||||
),
|
||||
?delegate(
|
||||
?partycond(PartyID, {shop_is, cfg({shop_id, ?PAYMENT_CASCADE_FAIL_UI_ID}, C)}),
|
||||
?ruleset(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_FAIL_UI_ID))
|
||||
)
|
||||
%% ?delegate(
|
||||
%% ?partycond(PartyID, {shop_is, cfg({shop_id, ?PAYMENT_CASCADE_FAIL_UI_ID}, C)}),
|
||||
%% ?ruleset(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_FAIL_UI_ID)))
|
||||
]}
|
||||
)
|
||||
] ++
|
||||
payment_cascade_success_fixture(Revision, C) ++
|
||||
payment_cascade_limit_overflow_fixture(Revision, C) ++
|
||||
payment_cascade_fail_ui_fixture(Revision, C) ++
|
||||
payment_cascade_fail_wo_route_candidates_fixture(Revision, C) ++
|
||||
payment_cascade_success_w_refund_fixture(Revision, C) ++
|
||||
payment_big_cascade_success_fixture(Revision, C) ++
|
||||
@ -6061,9 +6073,27 @@ init_route_cascading_group(C1) ->
|
||||
PartyClient
|
||||
)
|
||||
},
|
||||
{
|
||||
{shop_id, ?PAYMENT_CASCADE_LIMIT_OVERFLOW_ID},
|
||||
hg_ct_helper:create_shop(
|
||||
PartyID,
|
||||
?cat(1),
|
||||
<<"RUB">>,
|
||||
?tmpl(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_LIMIT_OVERFLOW_ID)),
|
||||
?pinst(1),
|
||||
PartyClient
|
||||
)
|
||||
},
|
||||
{
|
||||
{shop_id, ?PAYMENT_CASCADE_FAIL_UI_ID},
|
||||
hg_ct_helper:create_shop(PartyID, ?cat(1), <<"RUB">>, ?tmpl(1), ?pinst(1), PartyClient)
|
||||
hg_ct_helper:create_shop(
|
||||
PartyID,
|
||||
?cat(1),
|
||||
<<"RUB">>,
|
||||
?tmpl(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_FAIL_UI_ID)),
|
||||
?pinst(1),
|
||||
PartyClient
|
||||
)
|
||||
}
|
||||
| C1
|
||||
],
|
||||
@ -6081,6 +6111,9 @@ init_per_cascade_case(payment_cascade_success_w_refund, C) ->
|
||||
init_per_cascade_case(payment_big_cascade_success, C) ->
|
||||
ShopID = cfg({shop_id, ?PAYMENT_BIG_CASCADE_SUCCESS_ID}, C),
|
||||
[{shop_id, ShopID} | C];
|
||||
init_per_cascade_case(payment_cascade_limit_overflow, C) ->
|
||||
ShopID = cfg({shop_id, ?PAYMENT_CASCADE_LIMIT_OVERFLOW_ID}, C),
|
||||
[{shop_id, ShopID} | C];
|
||||
init_per_cascade_case(payment_cascade_fail_wo_available_attempt_limit, C) ->
|
||||
ShopID = cfg({shop_id, ?PAYMENT_CASCADE_FAIL_WO_AVAILABLE_ATTEMPT_LIMIT_ID}, C),
|
||||
[{shop_id, ShopID} | C];
|
||||
@ -6456,6 +6489,140 @@ payment_big_cascade_success_fixture(Revision, _C) ->
|
||||
)
|
||||
]).
|
||||
|
||||
payment_cascade_limit_overflow_fixture_pre(Revision, _C) ->
|
||||
lists:flatten([
|
||||
hg_ct_fixture:construct_contract_template(
|
||||
?tmpl(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_LIMIT_OVERFLOW_ID)),
|
||||
?trms(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_LIMIT_OVERFLOW_ID))
|
||||
),
|
||||
new_merchant_terms_attempt_limit(
|
||||
?trms(1),
|
||||
?trms(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_LIMIT_OVERFLOW_ID)),
|
||||
10,
|
||||
Revision
|
||||
)
|
||||
]).
|
||||
|
||||
payment_cascade_limit_overflow_fixture(Revision, _C) ->
|
||||
Brovider =
|
||||
#domain_Provider{abs_account = AbsAccount, accounts = Accounts, terms = Terms} =
|
||||
hg_domain:get(Revision, {provider, ?prv(1)}),
|
||||
Terms1 =
|
||||
Terms#domain_ProvisionTermSet{
|
||||
payments = Terms#domain_ProvisionTermSet.payments#domain_PaymentsProvisionTerms{
|
||||
turnover_limits =
|
||||
{value, [
|
||||
#domain_TurnoverLimit{
|
||||
id = ?LIMIT_ID4,
|
||||
upper_boundary = ?LIMIT_UPPER_BOUNDARY,
|
||||
domain_revision = Revision
|
||||
}
|
||||
]}
|
||||
}
|
||||
},
|
||||
[
|
||||
{provider, #domain_ProviderObject{
|
||||
ref = ?prv(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_LIMIT_OVERFLOW_ID + 1)),
|
||||
data = Brovider#domain_Provider{terms = Terms1}
|
||||
}},
|
||||
{provider, #domain_ProviderObject{
|
||||
ref = ?prv(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_LIMIT_OVERFLOW_ID + 2)),
|
||||
data = #domain_Provider{
|
||||
name = <<"Duck Blocker">>,
|
||||
description = <<"No rubber ducks for you!">>,
|
||||
proxy = #domain_Proxy{
|
||||
ref = ?prx(1),
|
||||
additional = #{
|
||||
<<"always_fail">> => <<"authorization_failed:unknown">>,
|
||||
<<"override">> => <<"duckblocker">>
|
||||
}
|
||||
},
|
||||
abs_account = AbsAccount,
|
||||
accounts = Accounts,
|
||||
%% No limit boundaries configured
|
||||
terms = Terms
|
||||
}
|
||||
}},
|
||||
{terminal, #domain_TerminalObject{
|
||||
ref = ?trm(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_LIMIT_OVERFLOW_ID + 1)),
|
||||
data = #domain_Terminal{
|
||||
name = <<"Brominal 1">>,
|
||||
description = <<"Brominal 1">>,
|
||||
provider_ref = ?prv(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_LIMIT_OVERFLOW_ID + 1))
|
||||
}
|
||||
}},
|
||||
{terminal, #domain_TerminalObject{
|
||||
ref = ?trm(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_LIMIT_OVERFLOW_ID + 2)),
|
||||
data = #domain_Terminal{
|
||||
name = <<"Not-Brominal">>,
|
||||
description = <<"Not-Brominal">>,
|
||||
provider_ref = ?prv(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_LIMIT_OVERFLOW_ID + 2))
|
||||
}
|
||||
}},
|
||||
hg_ct_fixture:construct_payment_routing_ruleset(
|
||||
?ruleset(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_LIMIT_OVERFLOW_ID)),
|
||||
<<"Main with cascading">>,
|
||||
{candidates, [
|
||||
?candidate({constant, true}, ?trm(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_LIMIT_OVERFLOW_ID + 2))),
|
||||
?candidate({constant, true}, ?trm(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_LIMIT_OVERFLOW_ID + 1)))
|
||||
]}
|
||||
)
|
||||
].
|
||||
|
||||
-spec payment_cascade_limit_overflow(config()) -> test_return().
|
||||
payment_cascade_limit_overflow(C) ->
|
||||
Client = cfg(client, C),
|
||||
Amount = 42000 + ?LIMIT_UPPER_BOUNDARY,
|
||||
InvoiceParams = make_invoice_params(
|
||||
cfg(party_id, C),
|
||||
cfg(shop_id, C),
|
||||
<<"rubberduck">>,
|
||||
make_due_date(10),
|
||||
make_cash(Amount)
|
||||
),
|
||||
?invoice_state(Invoice = ?invoice(InvoiceID)) = hg_client_invoicing:create(InvoiceParams, Client),
|
||||
{PaymentTool, Session} = hg_dummy_provider:make_payment_tool(no_preauth, ?pmt_sys(<<"visa-ref">>)),
|
||||
Context = #base_Content{
|
||||
type = <<"application/x-erlang-binary">>,
|
||||
data = erlang:term_to_binary({you, 643, "not", [<<"welcome">>, here]})
|
||||
},
|
||||
PayerSessionInfo = #domain_PayerSessionInfo{
|
||||
redirect_url = <<"https://redirectly.io/merchant">>
|
||||
},
|
||||
PaymentParams = (make_payment_params(PaymentTool, Session, instant))#payproc_InvoicePaymentParams{
|
||||
payer_session_info = PayerSessionInfo,
|
||||
context = Context
|
||||
},
|
||||
#payproc_InvoicePayment{payment = Payment} = hg_client_invoicing:start_payment(InvoiceID, PaymentParams, Client),
|
||||
?invoice_created(?invoice_w_status(?invoice_unpaid())) = next_change(InvoiceID, Client),
|
||||
{ok, Limit} = hg_limiter_helper:get_payment_limit_amount(?LIMIT_ID4, hg_domain:head(), Payment, Invoice),
|
||||
InitialAccountedAmount = hg_limiter_helper:get_amount(Limit),
|
||||
?payment_ev(PaymentID, ?payment_started(?payment_w_status(?pending()))) =
|
||||
next_change(InvoiceID, Client),
|
||||
?payment_ev(PaymentID, ?risk_score_changed(_)) =
|
||||
next_change(InvoiceID, Client),
|
||||
{Route1, _CashFlow1, _TrxID1, Failure1} =
|
||||
await_cascade_triggering(InvoiceID, PaymentID, Client),
|
||||
ok = payproc_errors:match('PaymentFailure', Failure1, fun({authorization_failed, {unknown, _}}) -> ok end),
|
||||
%% And again but no route found
|
||||
[
|
||||
?payment_ev(PaymentID, ?route_changed(Route2, Candidates2)),
|
||||
?payment_ev(PaymentID, ?payment_rollback_started({failure, Failure2})),
|
||||
?payment_ev(PaymentID, ?payment_status_changed(?failed({failure, Failure2})))
|
||||
] =
|
||||
next_changes(InvoiceID, 3, Client),
|
||||
?assertNotEqual(Route1, Route2),
|
||||
?assertNot(lists:member(Route1, Candidates2)),
|
||||
%% No route found and so we pass original failure from previous attempt
|
||||
ok = payproc_errors:match('PaymentFailure', Failure2, fun({authorization_failed, {unknown, _}}) -> ok end),
|
||||
%% Assert payment status IS failed
|
||||
?invoice_state(?invoice_w_status(_), [?payment_state(FinalPayment)]) =
|
||||
hg_client_invoicing:get(InvoiceID, Client),
|
||||
?assertMatch(#domain_InvoicePayment{status = {failed, _}}, FinalPayment),
|
||||
?invoice_status_changed(?invoice_cancelled(<<"overdue">>)) = next_change(InvoiceID, Client),
|
||||
%% At the end of this scenario limit must not be changed.
|
||||
hg_limiter_helper:assert_payment_limit_amount(?LIMIT_ID4, InitialAccountedAmount, FinalPayment, Invoice).
|
||||
|
||||
-spec payment_big_cascade_success(config()) -> test_return().
|
||||
payment_big_cascade_success(C) ->
|
||||
Client = cfg(client, C),
|
||||
@ -6656,10 +6823,126 @@ payment_cascade_fail_provider_error(C) ->
|
||||
?assertMatch(#domain_InvoicePayment{status = {failed, _}}, Payment),
|
||||
?invoice_status_changed(?invoice_cancelled(<<"overdue">>)) = next_change(InvoiceID, Client).
|
||||
|
||||
payment_cascade_fail_ui_fixture_pre(Revision, _C) ->
|
||||
lists:flatten([
|
||||
hg_ct_fixture:construct_contract_template(
|
||||
?tmpl(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_FAIL_UI_ID)),
|
||||
?trms(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_FAIL_UI_ID))
|
||||
),
|
||||
new_merchant_terms_attempt_limit(
|
||||
?trms(1),
|
||||
?trms(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_FAIL_UI_ID)),
|
||||
10,
|
||||
Revision
|
||||
)
|
||||
]).
|
||||
|
||||
payment_cascade_fail_ui_fixture(Revision, _C) ->
|
||||
Brovider =
|
||||
#domain_Provider{abs_account = AbsAccount, accounts = Accounts, terms = Terms} =
|
||||
hg_domain:get(Revision, {provider, ?prv(1)}),
|
||||
lists:flatten([
|
||||
{provider, #domain_ProviderObject{
|
||||
ref = ?prv(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_FAIL_UI_ID + 1)),
|
||||
data = Brovider#domain_Provider{terms = Terms}
|
||||
}},
|
||||
{provider, #domain_ProviderObject{
|
||||
ref = ?prv(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_FAIL_UI_ID + 2)),
|
||||
data = #domain_Provider{
|
||||
name = <<"Rubber GUI">>,
|
||||
description = <<"( ͡° ͜ʖ ͡° )">>,
|
||||
proxy = #domain_Proxy{
|
||||
ref = ?prx(1),
|
||||
additional = #{
|
||||
<<"allow_ui">> => <<"true">>,
|
||||
<<"always_fail">> => <<"preauthorization_failed:unknown">>,
|
||||
<<"override">> => <<"rubber_gui">>
|
||||
}
|
||||
},
|
||||
abs_account = AbsAccount,
|
||||
accounts = Accounts,
|
||||
terms = Terms
|
||||
}
|
||||
}},
|
||||
{provider, #domain_ProviderObject{
|
||||
ref = ?prv(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_FAIL_UI_ID + 3)),
|
||||
data = #domain_Provider{
|
||||
name = <<"Duck Blocker">>,
|
||||
description = <<"No rubber ducks for you!">>,
|
||||
proxy = #domain_Proxy{
|
||||
ref = ?prx(1),
|
||||
additional = #{
|
||||
<<"always_fail">> => <<"authorization_failed:unknown">>,
|
||||
<<"override">> => <<"duckblocker">>
|
||||
}
|
||||
},
|
||||
abs_account = AbsAccount,
|
||||
accounts = Accounts,
|
||||
terms = Terms
|
||||
}
|
||||
}},
|
||||
[
|
||||
{terminal, #domain_TerminalObject{
|
||||
ref = ?trm(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_FAIL_UI_ID + I)),
|
||||
data = #domain_Terminal{
|
||||
name = <<"Brominal ", (integer_to_binary(I))/binary>>,
|
||||
description = <<"Brominal ", (integer_to_binary(I))/binary>>,
|
||||
provider_ref = ?prv(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_FAIL_UI_ID + I))
|
||||
}
|
||||
}}
|
||||
|| I <- lists:seq(1, 3)
|
||||
],
|
||||
hg_ct_fixture:construct_payment_routing_ruleset(
|
||||
?ruleset(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_FAIL_UI_ID)),
|
||||
<<"1 fail, 2 with UI, 3 never reached">>,
|
||||
{candidates, [
|
||||
?candidate({constant, true}, ?trm(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_FAIL_UI_ID + I)))
|
||||
|| I <- lists:reverse(lists:seq(1, 3))
|
||||
]}
|
||||
)
|
||||
]).
|
||||
|
||||
-spec payment_cascade_fail_ui(config()) -> test_return().
|
||||
payment_cascade_fail_ui(_C) ->
|
||||
%% TODO teach hg_dummy_provider how to fail after receiving user_interaction
|
||||
ok.
|
||||
payment_cascade_fail_ui(C) ->
|
||||
Client = cfg(client, C),
|
||||
Amount = 42000,
|
||||
InvoiceID = start_invoice(<<"rubberduck">>, make_due_date(10), Amount, C),
|
||||
{PaymentTool, Session} = hg_dummy_provider:make_payment_tool(preauth_3ds, ?pmt_sys(<<"visa-ref">>)),
|
||||
PaymentParams = make_payment_params(PaymentTool, Session, instant),
|
||||
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(_)) =
|
||||
next_change(InvoiceID, Client),
|
||||
{_Route1, _CashFlow1, _TrxID1, Failure1} =
|
||||
await_cascade_triggering(InvoiceID, PaymentID, Client),
|
||||
ok = payproc_errors:match('PaymentFailure', Failure1, fun({authorization_failed, {unknown, _}}) -> ok end),
|
||||
%% And again with UI
|
||||
[
|
||||
?payment_ev(PaymentID, ?route_changed(_Route2)),
|
||||
?payment_ev(PaymentID, ?cash_flow_changed(_CashFlow2))
|
||||
] = next_changes(InvoiceID, 2, Client),
|
||||
UserInteraction = await_payment_process_interaction(InvoiceID, PaymentID, Client),
|
||||
{URL, Form} = get_post_request(UserInteraction),
|
||||
_ = assert_success_post_request({URL, Form}),
|
||||
ok = await_payment_process_interaction_completion(InvoiceID, PaymentID, UserInteraction, Client),
|
||||
[
|
||||
?payment_ev(PaymentID, ?session_ev(?processed(), ?trx_bound(?trx_info(_TrxID2)))),
|
||||
?payment_ev(
|
||||
PaymentID,
|
||||
?session_ev(?processed(), ?session_finished(?session_failed({failure, Failure2})))
|
||||
),
|
||||
?payment_ev(PaymentID, ?payment_rollback_started({failure, Failure2}))
|
||||
] =
|
||||
next_changes(InvoiceID, 3, Client),
|
||||
?payment_ev(PaymentID, ?payment_status_changed(?failed({failure, Failure2}))) =
|
||||
next_change(InvoiceID, Client),
|
||||
ok = payproc_errors:match('PaymentFailure', Failure2, fun({preauthorization_failed, {unknown, _}}) -> ok end),
|
||||
%% Assert payment status IS failed
|
||||
?invoice_state(?invoice_w_status(_), [?payment_state(Payment)]) =
|
||||
hg_client_invoicing:get(InvoiceID, Client),
|
||||
?assertMatch(#domain_InvoicePayment{status = {failed, _}}, Payment),
|
||||
?invoice_status_changed(?invoice_cancelled(<<"overdue">>)) = next_change(InvoiceID, Client).
|
||||
|
||||
payment_cascade_fail_wo_route_candidates_fixture_pre(Revision, _C) ->
|
||||
lists:flatten([
|
||||
|
Loading…
Reference in New Issue
Block a user