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
|
%% rejected because of provider gone critical, then use subcode to highlight
|
||||||
%% the offender. Like 'provider_dead' or 'conversion_lacking'.
|
%% the offender. Like 'provider_dead' or 'conversion_lacking'.
|
||||||
Failure = genlib:define(St#st.failure, construct_routing_failure(Error)),
|
Failure = genlib:define(St#st.failure, construct_routing_failure(Error)),
|
||||||
InitialCandidates = [hg_route:to_payment_route(R) || R <- hg_routing_ctx:initial_candidates(Ctx)],
|
%% NOTE Not all initial candidates have their according limits held. And so
|
||||||
Route = hd(InitialCandidates),
|
%% we must account only for those that can be rolled back.
|
||||||
Candidates = ordsets:from_list(InitialCandidates),
|
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.
|
%% For protocol compatability we set choosen route in route_changed event.
|
||||||
%% It doesn't influence cash_flow building because this step will be
|
%% It doesn't influence cash_flow building because this step will be
|
||||||
%% skipped. And all limit's 'hold' operations will be rolled back.
|
%% 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.
|
%% Previously used routes are supposed to have their limits already rolled back.
|
||||||
Route = get_route(St0),
|
Route = get_route(St0),
|
||||||
Routes = [Route],
|
Routes = [Route],
|
||||||
_ = rollback_payment_limits(Routes, get_iter(St0), St0),
|
_ = rollback_payment_limits(Routes, get_iter(St0), St0, []),
|
||||||
_ = rollback_payment_cashflow(St0);
|
_ = rollback_payment_cashflow(St0);
|
||||||
_ ->
|
_ ->
|
||||||
ok
|
ok
|
||||||
@ -2285,7 +2288,7 @@ process_result({payment, processing_accounter}, Action, St) ->
|
|||||||
process_result({payment, routing_failure}, Action, St = #st{failure = Failure}) ->
|
process_result({payment, routing_failure}, Action, St = #st{failure = Failure}) ->
|
||||||
NewAction = hg_machine_action:set_timeout(0, Action),
|
NewAction = hg_machine_action:set_timeout(0, Action),
|
||||||
Routes = get_candidate_routes(St),
|
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}};
|
{done, {[?payment_status_changed(?failed(Failure))], NewAction}};
|
||||||
process_result({payment, processing_failure}, Action, St = #st{failure = Failure}) ->
|
process_result({payment, processing_failure}, Action, St = #st{failure = Failure}) ->
|
||||||
NewAction = hg_machine_action:set_timeout(0, Action),
|
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.
|
%% Previously used routes are supposed to have their limits already rolled back.
|
||||||
Route = get_route(St),
|
Route = get_route(St),
|
||||||
Routes = [Route],
|
Routes = [Route],
|
||||||
_ = rollback_payment_limits(Routes, get_iter(St), St),
|
_ = rollback_payment_limits(Routes, get_iter(St), St, []),
|
||||||
_ = rollback_payment_cashflow(St),
|
_ = rollback_payment_cashflow(St),
|
||||||
Revision = get_payment_revision(St),
|
Revision = get_payment_revision(St),
|
||||||
Behaviour = get_route_cascade_behaviour(Route, Revision),
|
Behaviour = get_route_cascade_behaviour(Route, Revision),
|
||||||
@ -2310,7 +2313,7 @@ process_result({payment, finalizing_accounter}, Action, St) ->
|
|||||||
commit_payment_cashflow(St);
|
commit_payment_cashflow(St);
|
||||||
?cancelled() ->
|
?cancelled() ->
|
||||||
Route = get_route(St),
|
Route = get_route(St),
|
||||||
_ = rollback_payment_limits([Route], get_iter(St), St),
|
_ = rollback_payment_limits([Route], get_iter(St), St, []),
|
||||||
rollback_payment_cashflow(St)
|
rollback_payment_cashflow(St)
|
||||||
end,
|
end,
|
||||||
check_recurrent_token(St),
|
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}),
|
RejectedRoute = hg_route:to_rejected_route(Route, {'LimitHoldError', LimitsIDs, LimiterError}),
|
||||||
{LimitHeldRoutes, [RejectedRoute | RejectedRoutes]}.
|
{LimitHeldRoutes, [RejectedRoute | RejectedRoutes]}.
|
||||||
|
|
||||||
rollback_payment_limits(Routes, Iter, St) ->
|
|
||||||
rollback_payment_limits(Routes, Iter, St, []).
|
|
||||||
|
|
||||||
rollback_payment_limits(Routes, Iter, St, Flags) ->
|
rollback_payment_limits(Routes, Iter, St, Flags) ->
|
||||||
Opts = get_opts(St),
|
Opts = get_opts(St),
|
||||||
Revision = get_payment_revision(St),
|
Revision = get_payment_revision(St),
|
||||||
@ -2597,7 +2597,7 @@ rollback_unused_payment_limits(St) ->
|
|||||||
Route = get_route(St),
|
Route = get_route(St),
|
||||||
Routes = get_candidate_routes(St),
|
Routes = get_candidate_routes(St),
|
||||||
UnUsedRoutes = Routes -- [Route],
|
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) ->
|
get_turnover_limits(ProviderTerms) ->
|
||||||
TurnoverLimitSelector = ProviderTerms#domain_PaymentsProvisionTerms.turnover_limits,
|
TurnoverLimitSelector = ProviderTerms#domain_PaymentsProvisionTerms.turnover_limits,
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
-type route() :: hg_route:payment_route().
|
-type route() :: hg_route:payment_route().
|
||||||
-type refund() :: hg_invoice_payment:domain_refund().
|
-type refund() :: hg_invoice_payment:domain_refund().
|
||||||
-type cash() :: dmsl_domain_thrift:'Cash'().
|
-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 turnover_limit_value() :: dmsl_payproc_thrift:'TurnoverLimitValue'().
|
||||||
|
|
||||||
-type change_queue() :: [hg_limiter_client:limit_change()].
|
-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 = commit(LimitChanges, Clock, Context),
|
||||||
ok = log_limit_changes(TurnoverLimits, 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()]) ->
|
-spec rollback_payment_limits([turnover_limit()], route(), pos_integer(), invoice(), payment(), [handling_flag()]) ->
|
||||||
ok.
|
ok.
|
||||||
rollback_payment_limits(TurnoverLimits, Route, Iter, Invoice, Payment, Flags) ->
|
rollback_payment_limits(TurnoverLimits, Route, Iter, Invoice, Payment, Flags) ->
|
||||||
@ -167,16 +175,26 @@ process_changes(LimitChangesQueues, WithFun, Clock, Context, Flags) ->
|
|||||||
LimitChangesQueues
|
LimitChangesQueues
|
||||||
).
|
).
|
||||||
|
|
||||||
process_changes_try_wrap([LimitChange], WithFun, Clock, Context, _Flags) ->
|
%% Very specific error to crutch around
|
||||||
WithFun(LimitChange, Clock, Context);
|
-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) ->
|
process_changes_try_wrap([LimitChange | OtherLimitChanges], WithFun, Clock, Context, Flags) ->
|
||||||
IgnoreBusinessError = lists:member(ignore_business_error, Flags),
|
IgnoreBusinessError = lists:member(ignore_business_error, Flags),
|
||||||
#limiter_LimitChange{change_id = ChangeID} = LimitChange,
|
#limiter_LimitChange{change_id = ChangeID} = LimitChange,
|
||||||
try
|
try
|
||||||
WithFun(LimitChange, Clock, Context)
|
WithFun(LimitChange, Clock, Context)
|
||||||
catch
|
catch
|
||||||
%% Very specific error to crutch around
|
error:(?POSTING_PLAN_NOT_FOUND(ChangeID)) ->
|
||||||
error:#base_InvalidRequest{errors = [<<"Posting plan not found: ", ChangeID/binary>>]} ->
|
|
||||||
process_changes_try_wrap(OtherLimitChanges, WithFun, Clock, Context, Flags);
|
process_changes_try_wrap(OtherLimitChanges, WithFun, Clock, Context, Flags);
|
||||||
Class:Reason:Stacktrace ->
|
Class:Reason:Stacktrace ->
|
||||||
handle_caught_exception(Class, Reason, Stacktrace, IgnoreBusinessError)
|
handle_caught_exception(Class, Reason, Stacktrace, IgnoreBusinessError)
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
-export([initial_candidates/1]).
|
-export([initial_candidates/1]).
|
||||||
-export([stash_current_candidates/1]).
|
-export([stash_current_candidates/1]).
|
||||||
-export([considered_candidates/1]).
|
-export([considered_candidates/1]).
|
||||||
|
-export([accounted_candidates/1]).
|
||||||
-export([choosen_route/1]).
|
-export([choosen_route/1]).
|
||||||
-export([process/2]).
|
-export([process/2]).
|
||||||
-export([with_guard/1]).
|
-export([with_guard/1]).
|
||||||
@ -127,7 +128,17 @@ initial_candidates(#{initial_candidates := InitialCandidates}) ->
|
|||||||
considered_candidates(Ctx) ->
|
considered_candidates(Ctx) ->
|
||||||
maps:get(stashed_candidates, Ctx, 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().
|
-spec stash_current_candidates(t()) -> t().
|
||||||
|
stash_current_candidates(Ctx = #{candidates := []}) ->
|
||||||
|
Ctx;
|
||||||
stash_current_candidates(Ctx) ->
|
stash_current_candidates(Ctx) ->
|
||||||
Ctx#{stashed_candidates => candidates(Ctx)}.
|
Ctx#{stashed_candidates => candidates(Ctx)}.
|
||||||
|
|
||||||
|
@ -263,24 +263,16 @@ token_respond(Response, CallbackResult) ->
|
|||||||
%
|
%
|
||||||
% Payments
|
% 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, _) ->
|
process_payment(?processed(), undefined, PaymentInfo, CtxOpts, _) ->
|
||||||
case get_payment_info_scenario(PaymentInfo) of
|
case get_payment_info_scenario(PaymentInfo) of
|
||||||
{preauth_3ds, Timeout} ->
|
{preauth_3ds, Timeout} ->
|
||||||
Tag = generate_tag(<<"payment">>),
|
Tag = generate_tag(<<"payment">>),
|
||||||
Uri = get_callback_url(),
|
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 ->
|
change_cash_increase ->
|
||||||
%% simple workflow without 3DS
|
%% simple workflow without 3DS
|
||||||
result(?sleep(0), <<"sleeping">>);
|
result(?sleep(0), <<"sleeping">>);
|
||||||
@ -289,7 +281,7 @@ process_payment(?processed(), undefined, PaymentInfo, CtxOpts, _) ->
|
|||||||
result(?sleep(0), <<"sleeping">>);
|
result(?sleep(0), <<"sleeping">>);
|
||||||
no_preauth ->
|
no_preauth ->
|
||||||
%% simple workflow without 3DS
|
%% simple workflow without 3DS
|
||||||
result(?sleep(0), <<"sleeping">>);
|
maybe_fail(PaymentInfo, CtxOpts, result(?sleep(0), <<"sleeping">>));
|
||||||
empty_cvv ->
|
empty_cvv ->
|
||||||
%% simple workflow without 3DS
|
%% simple workflow without 3DS
|
||||||
result(?sleep(0), <<"sleeping">>);
|
result(?sleep(0), <<"sleeping">>);
|
||||||
@ -352,7 +344,7 @@ process_payment(?processed(), <<"sleeping">>, PaymentInfo, CtxOpts, _) ->
|
|||||||
{temporary_unavailability, Scenario} ->
|
{temporary_unavailability, Scenario} ->
|
||||||
process_failure_scenario(PaymentInfo, Scenario, TrxID);
|
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;
|
end;
|
||||||
process_payment(?processed(), <<"sleeping_with_user_interaction">>, PaymentInfo, CtxOpts, _) ->
|
process_payment(?processed(), <<"sleeping_with_user_interaction">>, PaymentInfo, CtxOpts, _) ->
|
||||||
Key = {get_invoice_id(PaymentInfo), get_payment_id(PaymentInfo)},
|
Key = {get_invoice_id(PaymentInfo), get_payment_id(PaymentInfo)},
|
||||||
@ -546,6 +538,15 @@ result(Intent, NextState, Trx) ->
|
|||||||
trx = 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'().
|
-spec mk_trx(binary()) -> dmsl_domain_thrift:'TransactionInfo'().
|
||||||
mk_trx(TrxID) ->
|
mk_trx(TrxID) ->
|
||||||
mk_trx_w_extra(TrxID, #{}).
|
mk_trx_w_extra(TrxID, #{}).
|
||||||
|
@ -195,6 +195,7 @@
|
|||||||
-export([payment_cascade_fail_wo_route_candidates/1]).
|
-export([payment_cascade_fail_wo_route_candidates/1]).
|
||||||
-export([payment_cascade_success_w_refund/1]).
|
-export([payment_cascade_success_w_refund/1]).
|
||||||
-export([payment_big_cascade_success/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_fail_wo_available_attempt_limit/1]).
|
||||||
-export([payment_cascade_failures/1]).
|
-export([payment_cascade_failures/1]).
|
||||||
-export([payment_cascade_deadline_failures/1]).
|
-export([payment_cascade_deadline_failures/1]).
|
||||||
@ -482,11 +483,12 @@ groups() ->
|
|||||||
payment_cascade_fail_wo_route_candidates,
|
payment_cascade_fail_wo_route_candidates,
|
||||||
payment_cascade_success_w_refund,
|
payment_cascade_success_w_refund,
|
||||||
payment_big_cascade_success,
|
payment_big_cascade_success,
|
||||||
|
payment_cascade_limit_overflow,
|
||||||
payment_cascade_fail_wo_available_attempt_limit,
|
payment_cascade_fail_wo_available_attempt_limit,
|
||||||
payment_cascade_failures,
|
payment_cascade_failures,
|
||||||
payment_cascade_deadline_failures,
|
payment_cascade_deadline_failures,
|
||||||
payment_cascade_fail_provider_error
|
payment_cascade_fail_provider_error,
|
||||||
%% payment_cascade_fail_ui
|
payment_cascade_fail_ui
|
||||||
]}
|
]}
|
||||||
].
|
].
|
||||||
|
|
||||||
@ -5931,9 +5933,12 @@ consistent_account_balances(C) ->
|
|||||||
-define(PAYMENT_CASCADE_DEADLINE_FAILURES_ID, 700).
|
-define(PAYMENT_CASCADE_DEADLINE_FAILURES_ID, 700).
|
||||||
-define(PAYMENT_CASCADE_FAIL_PROVIDER_ERROR_ID, 800).
|
-define(PAYMENT_CASCADE_FAIL_PROVIDER_ERROR_ID, 800).
|
||||||
-define(PAYMENT_CASCADE_FAIL_UI_ID, 900).
|
-define(PAYMENT_CASCADE_FAIL_UI_ID, 900).
|
||||||
|
-define(PAYMENT_CASCADE_LIMIT_OVERFLOW_ID, 1000).
|
||||||
|
|
||||||
cascade_fixture_pre_shop_create(Revision, C) ->
|
cascade_fixture_pre_shop_create(Revision, C) ->
|
||||||
payment_big_cascade_success_fixture_pre(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_route_candidates_fixture_pre(Revision, C) ++
|
||||||
payment_cascade_fail_wo_available_attempt_limit_fixture_pre(Revision, C) ++
|
payment_cascade_fail_wo_available_attempt_limit_fixture_pre(Revision, C) ++
|
||||||
payment_cascade_fail_provider_error_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(
|
hg_ct_fixture:construct_payment_routing_ruleset(
|
||||||
?ruleset(2),
|
?ruleset(2),
|
||||||
<<"2 routes with failing providers">>,
|
<<"Multiple routes with failing providers">>,
|
||||||
{delegates, [
|
{delegates, [
|
||||||
?delegate(
|
?delegate(
|
||||||
?partycond(PartyID, {shop_is, cfg({shop_id, ?PAYMENT_CASCADE_SUCCESS_ID}, C)}),
|
?partycond(PartyID, {shop_is, cfg({shop_id, ?PAYMENT_CASCADE_SUCCESS_ID}, C)}),
|
||||||
@ -5980,14 +5985,21 @@ cascade_fixture(Revision, C) ->
|
|||||||
?delegate(
|
?delegate(
|
||||||
?partycond(PartyID, {shop_is, cfg({shop_id, ?PAYMENT_CASCADE_FAIL_PROVIDER_ERROR_ID}, C)}),
|
?partycond(PartyID, {shop_is, cfg({shop_id, ?PAYMENT_CASCADE_FAIL_PROVIDER_ERROR_ID}, C)}),
|
||||||
?ruleset(?CASCADE_ID_RANGE(?PAYMENT_CASCADE_FAIL_PROVIDER_ERROR_ID))
|
?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_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_fail_wo_route_candidates_fixture(Revision, C) ++
|
||||||
payment_cascade_success_w_refund_fixture(Revision, C) ++
|
payment_cascade_success_w_refund_fixture(Revision, C) ++
|
||||||
payment_big_cascade_success_fixture(Revision, C) ++
|
payment_big_cascade_success_fixture(Revision, C) ++
|
||||||
@ -6061,9 +6073,27 @@ init_route_cascading_group(C1) ->
|
|||||||
PartyClient
|
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},
|
{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
|
| C1
|
||||||
],
|
],
|
||||||
@ -6081,6 +6111,9 @@ init_per_cascade_case(payment_cascade_success_w_refund, C) ->
|
|||||||
init_per_cascade_case(payment_big_cascade_success, C) ->
|
init_per_cascade_case(payment_big_cascade_success, C) ->
|
||||||
ShopID = cfg({shop_id, ?PAYMENT_BIG_CASCADE_SUCCESS_ID}, C),
|
ShopID = cfg({shop_id, ?PAYMENT_BIG_CASCADE_SUCCESS_ID}, C),
|
||||||
[{shop_id, ShopID} | 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) ->
|
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),
|
ShopID = cfg({shop_id, ?PAYMENT_CASCADE_FAIL_WO_AVAILABLE_ATTEMPT_LIMIT_ID}, C),
|
||||||
[{shop_id, ShopID} | 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().
|
-spec payment_big_cascade_success(config()) -> test_return().
|
||||||
payment_big_cascade_success(C) ->
|
payment_big_cascade_success(C) ->
|
||||||
Client = cfg(client, C),
|
Client = cfg(client, C),
|
||||||
@ -6656,10 +6823,126 @@ payment_cascade_fail_provider_error(C) ->
|
|||||||
?assertMatch(#domain_InvoicePayment{status = {failed, _}}, Payment),
|
?assertMatch(#domain_InvoicePayment{status = {failed, _}}, Payment),
|
||||||
?invoice_status_changed(?invoice_cancelled(<<"overdue">>)) = next_change(InvoiceID, Client).
|
?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().
|
-spec payment_cascade_fail_ui(config()) -> test_return().
|
||||||
payment_cascade_fail_ui(_C) ->
|
payment_cascade_fail_ui(C) ->
|
||||||
%% TODO teach hg_dummy_provider how to fail after receiving user_interaction
|
Client = cfg(client, C),
|
||||||
ok.
|
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) ->
|
payment_cascade_fail_wo_route_candidates_fixture_pre(Revision, _C) ->
|
||||||
lists:flatten([
|
lists:flatten([
|
||||||
|
Loading…
Reference in New Issue
Block a user