mirror of
https://github.com/valitydev/hellgate.git
synced 2024-11-06 02:45:20 +00:00
TD-586: Move cascade hook to processing_failure
activity handling (#68)
* Refactors merge_change for payment status change and cascade context log message. * Adds payment success testcase with multiple failing routes
This commit is contained in:
parent
36f3b27ec0
commit
4cc8e5d30d
@ -178,8 +178,7 @@
|
||||
-type payment() :: dmsl_domain_thrift:'InvoicePayment'().
|
||||
-type payment_id() :: dmsl_domain_thrift:'InvoicePaymentID'().
|
||||
-type payment_status() :: dmsl_domain_thrift:'InvoicePaymentStatus'().
|
||||
-type payment_status_type() ::
|
||||
pending_attempt | pending | processed | captured | cancelled | refunded | failed | charged_back.
|
||||
-type payment_status_type() :: pending | processed | captured | cancelled | refunded | failed | charged_back.
|
||||
-type domain_refund() :: dmsl_domain_thrift:'InvoicePaymentRefund'().
|
||||
-type payment_refund() :: dmsl_payproc_thrift:'InvoicePaymentRefund'().
|
||||
-type refund_id() :: dmsl_domain_thrift:'InvoicePaymentRefundID'().
|
||||
@ -2155,13 +2154,12 @@ process_timeout({payment, Step}, _Action, St) when
|
||||
->
|
||||
process_session(St);
|
||||
process_timeout({payment, Step}, Action, St) when
|
||||
Step =:= routing_failure orelse
|
||||
Step =:= processing_failure orelse
|
||||
Step =:= routing_failure orelse
|
||||
Step =:= processing_accounter orelse
|
||||
Step =:= finalizing_accounter
|
||||
->
|
||||
process_result(Action, St);
|
||||
process_timeout({payment, processing_failure}, Action, St) ->
|
||||
process_processing_failure(Action, St);
|
||||
process_timeout({payment, updating_accounter}, Action, St) ->
|
||||
process_accounter_update(Action, St);
|
||||
process_timeout({chargeback, ID, Type}, Action, St) ->
|
||||
@ -2275,10 +2273,11 @@ handle_gathered_route_result({ok, RoutesNoOverflow}, _Routes, CandidateRoutes, R
|
||||
{ChoosenRoute, ChoiceContext} = hg_routing:choose_route(RoutesNoOverflow),
|
||||
_ = log_route_choice_meta(ChoiceContext, Revision),
|
||||
[?route_changed(hg_routing:to_payment_route(ChoosenRoute), ordsets:from_list(CandidateRoutes))];
|
||||
handle_gathered_route_result({error, {not_found, _}}, Routes, CandidateRoutes, _Revision, #st{
|
||||
interim_payment_status = {failed, #domain_InvoicePaymentFailed{failure = InterimFailure}}
|
||||
}) ->
|
||||
handle_gathered_route_result_(Routes, CandidateRoutes, InterimFailure);
|
||||
handle_gathered_route_result({error, _Reason}, Routes, CandidateRoutes, _Revision, #st{failure = Failure}) when
|
||||
Failure =/= undefined
|
||||
->
|
||||
%% Pass original failure if it is set
|
||||
handle_gathered_route_result_(Routes, CandidateRoutes, Failure);
|
||||
handle_gathered_route_result({error, {not_found, RejectedRoutes}}, Routes, CandidateRoutes, _Revision, _St) ->
|
||||
Failure = construct_routing_failure(forbidden, genlib:format({rejected_routes, RejectedRoutes})),
|
||||
handle_gathered_route_result_(Routes, CandidateRoutes, Failure).
|
||||
@ -2291,13 +2290,6 @@ handle_gathered_route_result_(Routes, CandidateRoutes, Failure) ->
|
||||
%% For same purpose in cascade routing we use route form unfiltered list of originally resolved candidates.
|
||||
[?route_changed(Route, ordsets:from_list(CandidateRoutes)), ?payment_rollback_started(Failure)].
|
||||
|
||||
handle_choose_route_error(
|
||||
_Reason,
|
||||
Events,
|
||||
St = #st{interim_payment_status = {failed, #domain_InvoicePaymentFailed{failure = InterimFailure}}},
|
||||
Action
|
||||
) ->
|
||||
process_failure(get_activity(St), Events, Action, InterimFailure, St);
|
||||
handle_choose_route_error({rejected_routes, _RejectedRoutes} = Reason, Events, St, Action) ->
|
||||
do_handle_routing_error(unknown, genlib:format(Reason), Events, St, Action);
|
||||
handle_choose_route_error({misconfiguration, _Details} = Reason, Events, St, Action) ->
|
||||
@ -2542,6 +2534,17 @@ process_result({payment, routing_failure}, Action, St = #st{failure = Failure})
|
||||
Routes = get_candidate_routes(St),
|
||||
_ = rollback_payment_limits(Routes, get_iter(St), St, [ignore_business_error]),
|
||||
{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),
|
||||
%% We need to rollback only current route.
|
||||
%% Previously used routes are supposed to have their limits already rolled back.
|
||||
Routes = [get_route(St)],
|
||||
_ = rollback_payment_limits(Routes, get_iter(St), St),
|
||||
_ = rollback_payment_cashflow(St),
|
||||
case is_route_cascade_available(?failed(Failure), St) of
|
||||
true -> process_routing(NewAction, St);
|
||||
false -> {done, {[?payment_status_changed(?failed(Failure))], NewAction}}
|
||||
end;
|
||||
process_result({payment, finalizing_accounter}, Action, St) ->
|
||||
Target = get_target(St),
|
||||
_PostingPlanLog =
|
||||
@ -2581,22 +2584,14 @@ process_result({refund_accounter, ID}, Action, St) ->
|
||||
end,
|
||||
{done, {[?refund_ev(ID, ?refund_status_changed(?refund_succeeded())) | Events], Action}}.
|
||||
|
||||
process_processing_failure(Action, St = #st{failure = Failure}) ->
|
||||
NewAction = hg_machine_action:set_timeout(0, Action),
|
||||
%% We need to rollback only current route.
|
||||
%% Previously used routes are supposed to have their limits already rolled back.
|
||||
Routes = [get_route(St)],
|
||||
_ = rollback_payment_limits(Routes, get_iter(St), St),
|
||||
_ = rollback_payment_cashflow(St),
|
||||
Result = {[?payment_status_changed(?failed(Failure))], NewAction},
|
||||
case is_failure_cascade_trigger(Failure) of
|
||||
true -> {next, Result};
|
||||
false -> {done, Result}
|
||||
end.
|
||||
|
||||
process_failure(Activity, Events, Action, Failure, St) ->
|
||||
process_failure(Activity, Events, Action, Failure, St, undefined).
|
||||
|
||||
process_failure({payment, processing_failure}, Events, Action, _Failure, #st{failure = Failure}, _RefundSt) when
|
||||
Failure =/= undefined
|
||||
->
|
||||
%% In case of cascade attempt we may catch and handle routing failure during 'processing_failure' activity
|
||||
{done, {Events ++ [?payment_status_changed(?failed(Failure))], Action}};
|
||||
process_failure({payment, Step}, Events, Action, Failure, _St, _RefundSt) when
|
||||
Step =:= risk_scoring orelse
|
||||
Step =:= routing
|
||||
@ -3221,8 +3216,11 @@ merge_change(Change = ?risk_score_changed(RiskScore), #st{} = St, Opts) ->
|
||||
activity = {payment, routing}
|
||||
};
|
||||
merge_change(Change = ?route_changed(Route, Candidates), #st{routes = Routes} = St, Opts) ->
|
||||
_ = validate_transition({payment, routing}, Change, St, Opts),
|
||||
_ = validate_transition([{payment, S} || S <- [routing, processing_failure]], Change, St, Opts),
|
||||
St#st{
|
||||
%% On route change we expect cash flow from previous attempt to be rolled back.
|
||||
%% So on `?payment_rollback_started(_)` event for routing failure we won't try to do it again.
|
||||
cash_flow = undefined,
|
||||
routes = [Route | Routes],
|
||||
candidate_routes = ordsets:to_list(Candidates),
|
||||
activity = {payment, cash_flow_building}
|
||||
@ -3286,11 +3284,7 @@ merge_change(Change = ?payment_rollback_started(Failure), St, Opts) ->
|
||||
activity = Activity,
|
||||
timings = accrue_status_timing(failed, Opts, St)
|
||||
};
|
||||
merge_change(
|
||||
Change = ?payment_status_changed(?failed(_) = OriginalStatus),
|
||||
#st{payment = Payment, interim_payment_status = InterimStatus} = St,
|
||||
Opts
|
||||
) ->
|
||||
merge_change(Change = ?payment_status_changed({failed, _} = Status), #st{payment = Payment} = St, Opts) ->
|
||||
_ = validate_transition(
|
||||
[
|
||||
{payment, S}
|
||||
@ -3305,24 +3299,12 @@ merge_change(
|
||||
St,
|
||||
Opts
|
||||
),
|
||||
ActualStatus = genlib:define(InterimStatus, OriginalStatus),
|
||||
case is_route_cascade_available(OriginalStatus, St) of
|
||||
true ->
|
||||
St#st{
|
||||
interim_payment_status = ActualStatus,
|
||||
activity = {payment, routing},
|
||||
timings = accrue_status_timing(pending_attempt, Opts, St),
|
||||
%% Since next step is routing again, we won't need (previously rollbacked) cash flow
|
||||
cash_flow = undefined
|
||||
};
|
||||
false ->
|
||||
St#st{
|
||||
payment = Payment#domain_InvoicePayment{status = ActualStatus},
|
||||
activity = idle,
|
||||
failure = undefined,
|
||||
timings = accrue_status_timing(failed, Opts, St)
|
||||
}
|
||||
end;
|
||||
St#st{
|
||||
payment = Payment#domain_InvoicePayment{status = Status},
|
||||
activity = idle,
|
||||
failure = undefined,
|
||||
timings = accrue_status_timing(failed, Opts, St)
|
||||
};
|
||||
merge_change(Change = ?payment_status_changed({cancelled, _} = Status), #st{payment = Payment} = St, Opts) ->
|
||||
_ = validate_transition({payment, finalizing_accounter}, Change, St, Opts),
|
||||
St#st{
|
||||
|
@ -252,9 +252,10 @@ process_payment(
|
||||
?processed(),
|
||||
undefined,
|
||||
_PaymentInfo,
|
||||
#{<<"always_fail">> := FailureCode, <<"override">> := ProviderCode},
|
||||
#{<<"always_fail">> := FailureCode, <<"override">> := ProviderCode} = Opts,
|
||||
_
|
||||
) ->
|
||||
_ = maybe_sleep(Opts),
|
||||
Failure = payproc_errors:from_notation(FailureCode, <<"sub failure by ", ProviderCode/binary>>),
|
||||
result(?finish({failure, Failure}));
|
||||
process_payment(?processed(), undefined, PaymentInfo, _Ctx, _) ->
|
||||
@ -835,3 +836,8 @@ set_transaction_state(Key, Value) ->
|
||||
|
||||
get_transaction_state(Key) ->
|
||||
hg_kv_store:get(Key).
|
||||
|
||||
maybe_sleep(#{<<"sleep_ms">> := TimeMs}) when is_binary(TimeMs) ->
|
||||
timer:sleep(binary_to_integer(TimeMs));
|
||||
maybe_sleep(_Opts) ->
|
||||
ok.
|
||||
|
@ -195,9 +195,11 @@
|
||||
-export([allocation_refund_payment/1]).
|
||||
|
||||
-export([payment_cascade_success/1]).
|
||||
-export([payment_big_cascade_success/1]).
|
||||
-export([payment_cascade_fail_wo_available_attempt_limit/1]).
|
||||
-export([payment_cascade_failures/1]).
|
||||
-export([payment_cascade_no_route/1]).
|
||||
-export([payment_cascade_deadline_failures/1]).
|
||||
|
||||
%%
|
||||
|
||||
@ -470,9 +472,11 @@ groups() ->
|
||||
]},
|
||||
{route_cascading, [], [
|
||||
payment_cascade_success,
|
||||
payment_big_cascade_success,
|
||||
payment_cascade_fail_wo_available_attempt_limit,
|
||||
payment_cascade_failures,
|
||||
payment_cascade_no_route
|
||||
payment_cascade_no_route,
|
||||
payment_cascade_deadline_failures
|
||||
]}
|
||||
].
|
||||
|
||||
@ -695,6 +699,8 @@ init_per_testcase(invalid_permit_partial_capture_in_service, C) ->
|
||||
override_domain_fixture(fun construct_term_set_for_partial_capture_service_permit/1, C);
|
||||
init_per_testcase(invalid_permit_partial_capture_in_provider, C) ->
|
||||
override_domain_fixture(fun construct_term_set_for_partial_capture_provider_permit/1, C);
|
||||
init_per_testcase(payment_cascade_deadline_failures, C) ->
|
||||
override_domain_fixture(fun routes_ruleset_w_different_failing_providers_fixture/1, C);
|
||||
init_per_testcase(payment_cascade_no_route, C) ->
|
||||
override_domain_fixture(fun routes_ruleset_w_one_failing_route_fixture/1, C);
|
||||
init_per_testcase(payment_cascade_failures, C) ->
|
||||
@ -703,6 +709,8 @@ init_per_testcase(payment_cascade_fail_wo_available_attempt_limit, C) ->
|
||||
override_domain_fixture(fun merchant_payments_service_terms_wo_attempt_limit/1, C);
|
||||
init_per_testcase(payment_cascade_success, C) ->
|
||||
override_domain_fixture(fun routes_ruleset_w_failing_provider_fixture/1, C);
|
||||
init_per_testcase(payment_big_cascade_success, C) ->
|
||||
override_domain_fixture(fun big_routes_ruleset_w_failing_provider_fixture/1, C);
|
||||
init_per_testcase(limit_hold_currency_error, C) ->
|
||||
override_domain_fixture(fun patch_limit_config_w_invalid_currency/1, C);
|
||||
init_per_testcase(limit_hold_operation_not_supported, C) ->
|
||||
@ -1680,6 +1688,7 @@ routes_ruleset_w_different_failing_providers_fixture(Revision) ->
|
||||
ref = ?prx(1),
|
||||
additional = #{
|
||||
<<"always_fail">> => <<"preauthorization_failed:card_blocked">>,
|
||||
<<"sleep_ms">> => <<"2000">>,
|
||||
<<"override">> => <<"duckblocker">>
|
||||
}
|
||||
},
|
||||
@ -1792,24 +1801,126 @@ routes_ruleset_w_failing_provider_fixture(Revision) ->
|
||||
)
|
||||
].
|
||||
|
||||
big_routes_ruleset_w_failing_provider_fixture(Revision) ->
|
||||
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 = ?BIG_LIMIT_UPPER_BOUNDARY,
|
||||
domain_revision = Revision
|
||||
}
|
||||
]}
|
||||
}
|
||||
},
|
||||
ProviderProto = #domain_Provider{
|
||||
name = <<"Provider Proto">>,
|
||||
proxy = #domain_Proxy{
|
||||
ref = ?prx(1),
|
||||
additional = #{}
|
||||
},
|
||||
description = <<"No rubber ducks for you!">>,
|
||||
abs_account = AbsAccount,
|
||||
accounts = Accounts,
|
||||
terms = Terms1
|
||||
},
|
||||
lists:flatten([
|
||||
set_merchant_terms_attempt_limit(?trms(1), 10, Revision),
|
||||
{provider, #domain_ProviderObject{
|
||||
ref = ?prv(1),
|
||||
data = Brovider#domain_Provider{terms = Terms1}
|
||||
}},
|
||||
mk_provider_w_term(?trm(999), <<"Not-Brominal #999">>, ?prv(999), <<"Duck Blocker #999">>, ProviderProto, #{
|
||||
<<"always_fail">> => <<"preauthorization_failed:card_blocked">>,
|
||||
<<"override">> => <<"duckblocker_999">>
|
||||
}),
|
||||
mk_provider_w_term(?trm(998), <<"Not-Brominal #998">>, ?prv(998), <<"Duck Blocker #998">>, ProviderProto, #{
|
||||
<<"always_fail">> => <<"preauthorization_failed:card_blocked">>,
|
||||
<<"override">> => <<"duckblocker_998">>
|
||||
}),
|
||||
mk_provider_w_term(?trm(997), <<"Not-Brominal #997">>, ?prv(997), <<"Duck Blocker #997">>, ProviderProto, #{
|
||||
<<"always_fail">> => <<"preauthorization_failed:card_blocked">>,
|
||||
<<"override">> => <<"duckblocker_997">>
|
||||
}),
|
||||
mk_provider_w_term(?trm(996), <<"Not-Brominal #996">>, ?prv(996), <<"Duck Blocker #996">>, ProviderProto, #{
|
||||
<<"always_fail">> => <<"preauthorization_failed:card_blocked">>,
|
||||
<<"override">> => <<"duckblocker_996">>
|
||||
}),
|
||||
mk_provider_w_term(?trm(995), <<"Not-Brominal #995">>, ?prv(995), <<"Duck Blocker #995">>, ProviderProto, #{
|
||||
<<"always_fail">> => <<"preauthorization_failed:card_blocked">>,
|
||||
<<"override">> => <<"duckblocker_995">>
|
||||
}),
|
||||
mk_provider_w_term(?trm(994), <<"Not-Brominal #994">>, ?prv(994), <<"Duck Blocker #994">>, ProviderProto, #{
|
||||
<<"always_fail">> => <<"preauthorization_failed:card_blocked">>,
|
||||
<<"override">> => <<"duckblocker_994">>
|
||||
}),
|
||||
hg_ct_fixture:construct_payment_routing_ruleset(
|
||||
?ruleset(2),
|
||||
<<"Big Main with cascading">>,
|
||||
%% 7 route candidates, 6 to fail
|
||||
{candidates, [
|
||||
?candidate({constant, true}, ?trm(999)),
|
||||
?candidate({constant, true}, ?trm(998)),
|
||||
?candidate({constant, true}, ?trm(997)),
|
||||
?candidate({constant, true}, ?trm(996)),
|
||||
?candidate({constant, true}, ?trm(995)),
|
||||
?candidate({constant, true}, ?trm(994)),
|
||||
?candidate({constant, true}, ?trm(1))
|
||||
]}
|
||||
)
|
||||
]).
|
||||
|
||||
mk_provider_w_term(TerminalRef, TerminalName, ProviderRef, ProviderName, Provider0, ProxyAdds) ->
|
||||
Provider1 = Provider0#domain_Provider{
|
||||
name = ProviderName,
|
||||
proxy = #domain_Proxy{
|
||||
ref = ?prx(1),
|
||||
additional = ProxyAdds
|
||||
}
|
||||
},
|
||||
[
|
||||
{provider, #domain_ProviderObject{
|
||||
ref = ProviderRef,
|
||||
data = Provider1
|
||||
}},
|
||||
{terminal, #domain_TerminalObject{
|
||||
ref = TerminalRef,
|
||||
data = #domain_Terminal{
|
||||
name = TerminalName,
|
||||
description = TerminalName,
|
||||
provider_ref = ProviderRef
|
||||
}
|
||||
}}
|
||||
].
|
||||
|
||||
merchant_payments_service_terms_wo_attempt_limit(Revision) ->
|
||||
lists:flatten([
|
||||
set_merchant_terms_attempt_limit(?trms(1), 1, Revision),
|
||||
routes_ruleset_w_failing_provider_fixture(Revision)
|
||||
]).
|
||||
|
||||
set_merchant_terms_attempt_limit(TermSetHierarchyRef, Attempts, Revision) ->
|
||||
#domain_TermSetHierarchy{term_sets = [BaseTermSet0]} =
|
||||
hg_domain:get(Revision, {term_set_hierarchy, ?trms(1)}),
|
||||
hg_domain:get(Revision, {term_set_hierarchy, TermSetHierarchyRef}),
|
||||
#domain_TimedTermSet{terms = TermsSet} = BaseTermSet0,
|
||||
#domain_TermSet{payments = PaymentsTerms0} = TermsSet,
|
||||
PaymentsTerms1 = PaymentsTerms0#domain_PaymentsServiceTerms{
|
||||
attempt_limit = {value, #domain_AttemptLimit{attempts = 1}}
|
||||
attempt_limit = {value, #domain_AttemptLimit{attempts = Attempts}}
|
||||
},
|
||||
BaseTermSet1 = BaseTermSet0#domain_TimedTermSet{
|
||||
terms = TermsSet#domain_TermSet{payments = PaymentsTerms1}
|
||||
},
|
||||
lists:flatten([
|
||||
[
|
||||
{term_set_hierarchy, #domain_TermSetHierarchyObject{
|
||||
ref = ?trms(1),
|
||||
ref = TermSetHierarchyRef,
|
||||
data = #domain_TermSetHierarchy{term_sets = [BaseTermSet1]}
|
||||
}},
|
||||
routes_ruleset_w_failing_provider_fixture(Revision)
|
||||
]).
|
||||
}}
|
||||
].
|
||||
|
||||
patch_limit_config_w_invalid_currency(Revision) ->
|
||||
NewRevision = hg_domain:update({limit_config, hg_limiter_helper:mk_config_object(?LIMIT_ID, <<"KEK">>)}),
|
||||
@ -5976,26 +6087,102 @@ payment_cascade_success(C) ->
|
||||
InitialAccountedAmount = hg_limiter_helper:get_amount(Limit),
|
||||
?payment_ev(PaymentID, ?payment_started(?payment_w_status(?pending()))) =
|
||||
next_change(InvoiceID, Client),
|
||||
_ = await_payment_cash_flow(InvoiceID, PaymentID, Client),
|
||||
PaymentID = await_payment_session_started(InvoiceID, PaymentID, Client, ?processed()),
|
||||
[
|
||||
?payment_ev(
|
||||
PaymentID,
|
||||
?session_ev(?processed(), ?session_finished(?session_failed({failure, Failure})))
|
||||
),
|
||||
?payment_ev(PaymentID, ?payment_rollback_started({failure, Failure})),
|
||||
?payment_ev(PaymentID, ?payment_status_changed(?failed({failure, Failure})))
|
||||
] =
|
||||
next_changes(InvoiceID, 3, Client),
|
||||
ok = payproc_errors:match('PaymentFailure', Failure, fun({preauthorization_failed, {card_blocked, _}}) -> ok end),
|
||||
?payment_ev(PaymentID, ?risk_score_changed(_)) =
|
||||
next_change(InvoiceID, Client),
|
||||
{_Route1, _CashFlow1, Failure1} =
|
||||
await_cascade_triggering(InvoiceID, PaymentID, Client),
|
||||
ok = payproc_errors:match('PaymentFailure', Failure1, fun({preauthorization_failed, {card_blocked, _}}) -> ok end),
|
||||
%% Assert payment status IS NOT failed
|
||||
?invoice_state(?invoice_w_status(_), [?payment_state(PaymentInterim)]) =
|
||||
hg_client_invoicing:get(InvoiceID, Client),
|
||||
?assertNotMatch(#domain_InvoicePayment{status = {failed, _}}, PaymentInterim),
|
||||
?payment_ev(PaymentID, ?route_changed(Route)) = next_change(InvoiceID, Client),
|
||||
?assertMatch(#domain_PaymentRoute{provider = ?prv(1)}, Route),
|
||||
%% And again
|
||||
?payment_ev(PaymentID, ?cash_flow_changed(_CashFlow)) = next_change(InvoiceID, Client),
|
||||
[
|
||||
?payment_ev(PaymentID, ?route_changed(Route2)),
|
||||
?payment_ev(PaymentID, ?cash_flow_changed(_CashFlow2))
|
||||
] =
|
||||
next_changes(InvoiceID, 2, Client),
|
||||
?assertMatch(#domain_PaymentRoute{provider = ?prv(1)}, Route2),
|
||||
PaymentID = await_payment_session_started(InvoiceID, PaymentID, Client, ?processed()),
|
||||
PaymentID = await_payment_process_finish(InvoiceID, PaymentID, Client),
|
||||
PaymentID = await_payment_capture(InvoiceID, PaymentID, Client),
|
||||
?invoice_state(?invoice_w_status(?invoice_paid()), [PaymentSt = ?payment_state(PaymentFinal)]) =
|
||||
hg_client_invoicing:get(InvoiceID, Client),
|
||||
?payment_w_status(PaymentID, ?captured()) = PaymentFinal,
|
||||
?payment_last_trx(Trx) = PaymentSt,
|
||||
?assertMatch(
|
||||
#domain_InvoicePayment{
|
||||
payer_session_info = PayerSessionInfo,
|
||||
context = Context
|
||||
},
|
||||
PaymentFinal
|
||||
),
|
||||
?assertMatch(
|
||||
#domain_TransactionInfo{
|
||||
extra = #{
|
||||
<<"payment.payer_session_info.redirect_url">> := RedirectURL
|
||||
}
|
||||
},
|
||||
Trx
|
||||
),
|
||||
%% At the end of this scenario limit must be accounted only once.
|
||||
hg_limiter_helper:assert_payment_limit_amount(?LIMIT_ID4, InitialAccountedAmount + Amount, PaymentFinal, Invoice).
|
||||
|
||||
-spec payment_big_cascade_success(config()) -> test_return().
|
||||
payment_big_cascade_success(C) ->
|
||||
Client = cfg(client, C),
|
||||
Amount = 42000,
|
||||
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 = RedirectURL = <<"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),
|
||||
[
|
||||
(fun() ->
|
||||
{_Route, _CashFlow, Failure} =
|
||||
await_cascade_triggering(InvoiceID, PaymentID, Client),
|
||||
ok = payproc_errors:match(
|
||||
'PaymentFailure',
|
||||
Failure,
|
||||
fun({preauthorization_failed, {card_blocked, _}}) -> ok end
|
||||
),
|
||||
%% Assert payment status IS NOT failed
|
||||
?invoice_state(?invoice_w_status(_), [?payment_state(PaymentInterim)]) =
|
||||
hg_client_invoicing:get(InvoiceID, Client),
|
||||
?assertNotMatch(#domain_InvoicePayment{status = {failed, _}}, PaymentInterim)
|
||||
end)()
|
||||
|| _I <- lists:seq(1, 6)
|
||||
],
|
||||
%% And again
|
||||
[
|
||||
?payment_ev(PaymentID, ?route_changed(RouteFinal)),
|
||||
?payment_ev(PaymentID, ?cash_flow_changed(_CashFlow2))
|
||||
] =
|
||||
next_changes(InvoiceID, 2, Client),
|
||||
?assertMatch(#domain_PaymentRoute{provider = ?prv(1)}, RouteFinal),
|
||||
PaymentID = await_payment_session_started(InvoiceID, PaymentID, Client, ?processed()),
|
||||
PaymentID = await_payment_process_finish(InvoiceID, PaymentID, Client),
|
||||
PaymentID = await_payment_capture(InvoiceID, PaymentID, Client),
|
||||
@ -6031,17 +6218,12 @@ payment_cascade_fail_wo_available_attempt_limit(C) ->
|
||||
hg_client_invoicing:start_payment(InvoiceID, PaymentParams, Client),
|
||||
?payment_ev(PaymentID, ?payment_started(?payment_w_status(?pending()))) =
|
||||
next_change(InvoiceID, Client),
|
||||
_ = await_payment_cash_flow(InvoiceID, PaymentID, Client),
|
||||
PaymentID = await_payment_session_started(InvoiceID, PaymentID, Client, ?processed()),
|
||||
[
|
||||
?payment_ev(
|
||||
PaymentID,
|
||||
?session_ev(?processed(), ?session_finished(?session_failed({failure, Failure})))
|
||||
),
|
||||
?payment_ev(PaymentID, ?payment_rollback_started({failure, Failure})),
|
||||
?payment_ev(PaymentID, ?payment_status_changed(?failed({failure, Failure})))
|
||||
] =
|
||||
next_changes(InvoiceID, 3, Client),
|
||||
?payment_ev(PaymentID, ?risk_score_changed(_)) =
|
||||
next_change(InvoiceID, Client),
|
||||
{_Route, _CashFlow, Failure} =
|
||||
await_cascade_triggering(InvoiceID, PaymentID, Client),
|
||||
?payment_ev(PaymentID, ?payment_status_changed(?failed({failure, Failure}))) =
|
||||
next_change(InvoiceID, Client),
|
||||
ok = payproc_errors:match('PaymentFailure', Failure, fun({preauthorization_failed, {card_blocked, _}}) -> ok end),
|
||||
%% Assert payment status IS failed
|
||||
?invoice_state(?invoice_w_status(_), [?payment_state(Payment)]) =
|
||||
@ -6059,32 +6241,53 @@ payment_cascade_failures(C) ->
|
||||
hg_client_invoicing:start_payment(InvoiceID, PaymentParams, Client),
|
||||
?payment_ev(PaymentID, ?payment_started(?payment_w_status(?pending()))) =
|
||||
next_change(InvoiceID, Client),
|
||||
_ = await_payment_cash_flow(InvoiceID, PaymentID, Client),
|
||||
PaymentID = await_payment_session_started(InvoiceID, PaymentID, Client, ?processed()),
|
||||
[
|
||||
?payment_ev(
|
||||
PaymentID,
|
||||
?session_ev(?processed(), ?session_finished(?session_failed({failure, Failure1})))
|
||||
),
|
||||
?payment_ev(PaymentID, ?payment_rollback_started({failure, Failure1})),
|
||||
?payment_ev(PaymentID, ?payment_status_changed(?failed({failure, Failure1})))
|
||||
] =
|
||||
next_changes(InvoiceID, 3, Client),
|
||||
?payment_ev(PaymentID, ?risk_score_changed(_)) =
|
||||
next_change(InvoiceID, Client),
|
||||
{_Route1, _CashFlow1, Failure1} =
|
||||
await_cascade_triggering(InvoiceID, PaymentID, Client),
|
||||
ok = payproc_errors:match('PaymentFailure', Failure1, fun({preauthorization_failed, {card_blocked, _}}) -> ok end),
|
||||
%% And again
|
||||
{_Route2, _CashFlow2, Failure2} =
|
||||
await_cascade_triggering(InvoiceID, PaymentID, Client),
|
||||
?payment_ev(PaymentID, ?payment_status_changed(?failed({failure, Failure2}))) =
|
||||
next_change(InvoiceID, Client),
|
||||
ok = payproc_errors:match('PaymentFailure', Failure2, fun({preauthorization_failed, {card_blocked, _}}) -> 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).
|
||||
|
||||
-spec payment_cascade_deadline_failures(config()) -> test_return().
|
||||
payment_cascade_deadline_failures(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(no_preauth, ?pmt_sys(<<"visa-ref">>)),
|
||||
PaymentParams = (make_payment_params(PaymentTool, Session, instant))#payproc_InvoicePaymentParams{
|
||||
processing_deadline = hg_datetime:add_time_span(#base_TimeSpan{seconds = 2}, hg_datetime:format_now())
|
||||
},
|
||||
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, Failure1} =
|
||||
await_cascade_triggering(InvoiceID, PaymentID, Client),
|
||||
ok = payproc_errors:match('PaymentFailure', Failure1, fun({preauthorization_failed, {card_blocked, _}}) -> ok end),
|
||||
?payment_ev(PaymentID, ?route_changed(_Route)) = next_change(InvoiceID, Client),
|
||||
%% And again
|
||||
?payment_ev(PaymentID, ?cash_flow_changed(_CashFlow)) = next_change(InvoiceID, Client),
|
||||
PaymentID = await_payment_session_started(InvoiceID, PaymentID, Client, ?processed()),
|
||||
[
|
||||
?payment_ev(
|
||||
PaymentID,
|
||||
?session_ev(?processed(), ?session_finished(?session_failed({failure, Failure2})))
|
||||
),
|
||||
?payment_ev(PaymentID, ?route_changed(_Route2)),
|
||||
?payment_ev(PaymentID, ?cash_flow_changed(_CashFlow2)),
|
||||
?payment_ev(PaymentID, ?payment_rollback_started({failure, Failure2})),
|
||||
?payment_ev(PaymentID, ?payment_status_changed(?failed({failure, Failure2})))
|
||||
] =
|
||||
next_changes(InvoiceID, 3, Client),
|
||||
ok = payproc_errors:match('PaymentFailure', Failure2, fun({preauthorization_failed, {card_blocked, _}}) -> ok end),
|
||||
next_changes(InvoiceID, 4, Client),
|
||||
ok = payproc_errors:match(
|
||||
'PaymentFailure',
|
||||
Failure2,
|
||||
fun({authorization_failed, {processing_deadline_reached, _}}) -> ok end
|
||||
),
|
||||
%% Assert payment status IS failed
|
||||
?invoice_state(?invoice_w_status(_), [?payment_state(Payment)]) =
|
||||
hg_client_invoicing:get(InvoiceID, Client),
|
||||
@ -6101,26 +6304,19 @@ payment_cascade_no_route(C) ->
|
||||
hg_client_invoicing:start_payment(InvoiceID, PaymentParams, Client),
|
||||
?payment_ev(PaymentID, ?payment_started(?payment_w_status(?pending()))) =
|
||||
next_change(InvoiceID, Client),
|
||||
_ = await_payment_cash_flow(InvoiceID, PaymentID, Client),
|
||||
PaymentID = await_payment_session_started(InvoiceID, PaymentID, Client, ?processed()),
|
||||
[
|
||||
?payment_ev(
|
||||
PaymentID,
|
||||
?session_ev(?processed(), ?session_finished(?session_failed({failure, CardBlockedFailure})))
|
||||
),
|
||||
?payment_ev(PaymentID, ?payment_rollback_started({failure, CardBlockedFailure})),
|
||||
?payment_ev(PaymentID, ?payment_status_changed(?failed({failure, CardBlockedFailure})))
|
||||
] =
|
||||
next_changes(InvoiceID, 3, Client),
|
||||
?payment_ev(PaymentID, ?risk_score_changed(_)) =
|
||||
next_change(InvoiceID, Client),
|
||||
{_Route1, _CashFlow1, CardBlockedFailure} =
|
||||
await_cascade_triggering(InvoiceID, PaymentID, Client),
|
||||
ok = payproc_errors:match(
|
||||
'PaymentFailure',
|
||||
CardBlockedFailure,
|
||||
fun({preauthorization_failed, {card_blocked, _}}) -> ok end
|
||||
),
|
||||
[
|
||||
?payment_ev(PaymentID, ?route_changed(_Route)),
|
||||
?payment_ev(PaymentID, ?route_changed(_Route2)),
|
||||
?payment_ev(PaymentID, ?payment_rollback_started({failure, CardBlockedFailure})),
|
||||
?payment_ev(PaymentID, ?payment_status_changed(?failed({failure, _Failure})))
|
||||
?payment_ev(PaymentID, ?payment_status_changed(?failed({failure, CardBlockedFailure})))
|
||||
] = next_changes(InvoiceID, 3, Client),
|
||||
%% Assert payment status IS failed
|
||||
?invoice_state(?invoice_w_status(_), [?payment_state(Payment)]) =
|
||||
@ -6130,6 +6326,20 @@ payment_cascade_no_route(C) ->
|
||||
|
||||
%%
|
||||
|
||||
await_cascade_triggering(InvoiceID, PaymentID, Client) ->
|
||||
[
|
||||
?payment_ev(PaymentID, ?route_changed(Route)),
|
||||
?payment_ev(PaymentID, ?cash_flow_changed(CashFlow)),
|
||||
?payment_ev(PaymentID, ?session_ev(?processed(), ?session_started())),
|
||||
?payment_ev(
|
||||
PaymentID,
|
||||
?session_ev(?processed(), ?session_finished(?session_failed({failure, Failure})))
|
||||
),
|
||||
?payment_ev(PaymentID, ?payment_rollback_started({failure, Failure}))
|
||||
] =
|
||||
next_changes(InvoiceID, 5, Client),
|
||||
{Route, CashFlow, Failure}.
|
||||
|
||||
next_changes(InvoiceID, Amount, Client) ->
|
||||
next_changes(InvoiceID, Amount, ?DEFAULT_NEXT_CHANGE_TIMEOUT, Client).
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user