Skip to content
Merged
26 changes: 23 additions & 3 deletions apps/ff_transfer/src/ff_withdrawal.erl
Original file line number Diff line number Diff line change
Expand Up @@ -1929,10 +1929,11 @@ build_failure({route_not_found, []}, _Withdrawal) ->
code => <<"no_route_found">>
};
build_failure({route_not_found, RejectedRoutes}, _Withdrawal) ->
#{
genlib_map:compact(#{
code => <<"no_route_found">>,
reason => genlib:format({rejected_routes, RejectedRoutes})
};
reason => genlib:format({rejected_routes, RejectedRoutes}),
sub => build_route_not_found_sub_failure(RejectedRoutes)
});
build_failure({inconsistent_quote_route, {Type, FoundID}}, Withdrawal) ->
Details =
{inconsistent_quote_route, #{
Expand All @@ -1948,6 +1949,25 @@ build_failure(session, Withdrawal) ->
{failed, Failure} = Result,
Failure.

-define(rejected_route(Reason), {_PrvRef, _TrmRef, Reason}).

build_route_not_found_sub_failure([]) ->
undefined;
%% NOTE If limit overflow reason present in any rejected route See reason-tuple
%% construction in `ff_withdrawal_routing:validate_turnover_limits/4` and
%% `ff_limiter:check_limits/4`.
build_route_not_found_sub_failure([
?rejected_route({terms_violation, {overflow, OverflowedLimits}}) | _
]) ->
#{
code => <<"limit_overflow">>,
sub => #{
code => genlib_string:join($,, [LimitID || {LimitID, _Amount, _Boundary} <- OverflowedLimits])
}
};
build_route_not_found_sub_failure([_H | Rest]) ->
build_route_not_found_sub_failure(Rest).

get_quote_field(provider_id, #{route := Route}) ->
ff_withdrawal_routing:get_provider(Route);
get_quote_field(terminal_id, #{route := Route}) ->
Expand Down
14 changes: 13 additions & 1 deletion apps/ff_transfer/test/ff_withdrawal_limits_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,19 @@ limit_overflow(C) ->
PreviousAmount = get_limit_amount(Cash, WalletID, DestinationID, ?LIMIT_TURNOVER_NUM_PAYTOOL_ID2, C),
ok = ff_withdrawal_machine:create(WithdrawalParams, ff_entity_context:new()),
Result = await_final_withdrawal_status(WithdrawalID),
?assertMatch({failed, #{code := <<"no_route_found">>}}, Result),
?assertMatch(
{failed, #{
code := <<"no_route_found">>,
sub := #{
code := <<"limit_overflow">>,
sub := #{
code := LimitID
}
}
}} when
is_binary(LimitID),
Result
),
%% we get final withdrawal status before we rollback limits so wait for it some amount of time
ok = timer:sleep(500),
Withdrawal = get_withdrawal(WithdrawalID),
Expand Down
9 changes: 2 additions & 7 deletions apps/hellgate/include/domain.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,8 @@
terminal = TerminalRef
}).

-define(failure(Code),
?failure(Code, undefined)
).

-define(failure(Code, Reason),
{failure, #domain_Failure{code = Code, reason = Reason}}
).
-define(failure(Code, Reason, Sub), #domain_Failure{code = Code, sub = Sub, reason = Reason}).
-define(subfailure(Code, Sub), #domain_SubFailure{code = Code, sub = Sub}).

-define(operation_timeout(),
{operation_timeout, #domain_OperationTimeout{}}
Expand Down
1 change: 0 additions & 1 deletion apps/hellgate/src/hellgate.app.src
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
dmt_client,
party_client,
bender_client,
payproc_errors,
erl_health,
limiter_proto,
opentelemetry_api,
Expand Down
8 changes: 4 additions & 4 deletions apps/hellgate/src/hg_cascade.erl
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ is_mapped_errors_triggered(#domain_CascadeOnMappedErrors{}, {operation_timeout,
failure_matches_any_transient(Failure, TransientErrorsList) ->
lists:any(
fun(ExpectNotation) ->
payproc_errors:match_notation(Failure, fun
(Notation) when binary_part(Notation, {0, byte_size(ExpectNotation)}) =:= ExpectNotation -> true;
(_) -> false
end)
case iolist_to_binary(hg_invoice_utils:format_failure(Failure)) of
Notation when binary_part(Notation, {0, byte_size(ExpectNotation)}) =:= ExpectNotation -> true;
_ -> false
end
end,
TransientErrorsList
).
Expand Down
117 changes: 66 additions & 51 deletions apps/hellgate/src/hg_invoice_payment.erl
Original file line number Diff line number Diff line change
Expand Up @@ -1048,11 +1048,9 @@ validate_processing_deadline(#domain_InvoicePayment{processing_deadline = Deadli
ok ->
ok;
{error, deadline_reached} ->
{failure,
payproc_errors:construct(
'PaymentFailure',
{authorization_failed, {processing_deadline_reached, #payproc_error_GeneralFailure{}}}
)}
{failure, #domain_Failure{
code = <<"authorization_failed">>, sub = #domain_SubFailure{code = <<"processing_deadline_reached">>}
}}
end;
validate_processing_deadline(_, _TargetType) ->
ok.
Expand Down Expand Up @@ -1969,19 +1967,27 @@ process_shop_limit_initialization(_Action, St) ->
case check_shop_limits(Opts, St) of
ok ->
{next, {[?shop_limit_initiated()], timeout}};
{error, {limit_overflow = Error, IDs}} ->
Failure = construct_shop_limit_failure(Error, IDs),
{error, {limit_overflow = Error, LimitIDs}} ->
Failure = construct_shop_limit_failure(Error, LimitIDs),
Events = [
?shop_limit_initiated(),
?payment_rollback_started(Failure)
],
{next, {Events, timeout}}
end.

construct_shop_limit_failure(limit_overflow, IDs) ->
Error = mk_static_error([authorization_failed, shop_limit_exceeded, unknown]),
Reason = genlib:format("Limits with following IDs overflowed: ~p", [IDs]),
{failure, payproc_errors:construct('PaymentFailure', Error, Reason)}.
construct_shop_limit_failure(limit_overflow, LimitIDs) ->
{failure, #domain_Failure{
reason = genlib:format("Limits ~p overflowed", [LimitIDs]),
code = <<"authorization_failed">>,
sub = #domain_SubFailure{
code = <<"shop_limit_exceeded">>,
sub = #domain_SubFailure{
code = <<"unknown">>,
sub = #domain_SubFailure{code = genlib_string:join($,, LimitIDs)}
}
}
}}.

process_shop_limit_failure(_Action, #st{failure = Failure} = St) ->
Opts = get_opts(St),
Expand Down Expand Up @@ -2190,19 +2196,44 @@ log_rejected_route_groups(Result, VS) ->

construct_routing_failure({rejected_routes, {SubCode, RejectedRoutes}}) when
SubCode =:= limit_misconfiguration orelse
SubCode =:= limit_overflow orelse
SubCode =:= adapter_unavailable orelse
SubCode =:= provider_conversion_is_too_low
->
construct_routing_failure([rejected, SubCode], genlib:format(normalize_rejected_routes(RejectedRoutes)));
construct_routing_failure(
#domain_SubFailure{code = <<"rejected">>, sub = #domain_SubFailure{code = atom_to_binary(SubCode)}},
genlib:format(normalize_rejected_routes(RejectedRoutes))
);
construct_routing_failure({rejected_routes, {limit_overflow, RejectedRoutes}}) ->
%% NOTE See reason-tuple construction in `get_limit_overflow_routes/4`.
LimitIDs = ordsets:from_list(
lists:flatten([
LimitIDs
|| {_PrvRef, _TrmRef, {'LimitOverflow', LimitIDs}} <- normalize_rejected_routes(RejectedRoutes)
])
),
construct_routing_failure(
#domain_SubFailure{
code = <<"rejected">>,
sub = #domain_SubFailure{
code = <<"limit_overflow">>,
sub = #domain_SubFailure{code = genlib_string:join($,, LimitIDs)}
}
},
genlib:format(normalize_rejected_routes(RejectedRoutes))
);
construct_routing_failure({rejected_routes, {_SubCode, RejectedRoutes}}) ->
construct_routing_failure([forbidden], genlib:format(normalize_rejected_routes(RejectedRoutes)));
construct_routing_failure(
#domain_SubFailure{code = <<"forbidden">>}, genlib:format(normalize_rejected_routes(RejectedRoutes))
);
construct_routing_failure({misconfiguration = Code, Details}) ->
construct_routing_failure([unknown, {unknown_error, atom_to_binary(Code)}], genlib:format(Details));
construct_routing_failure(
#domain_SubFailure{code = <<"unknown">>, sub = #domain_SubFailure{code = atom_to_binary(Code)}},
genlib:format(Details)
);
construct_routing_failure(risk_score_is_too_high = Code) ->
construct_routing_failure([Code], undefined);
construct_routing_failure(#domain_SubFailure{code = atom_to_binary(Code)}, undefined);
construct_routing_failure(Error) when is_atom(Error) ->
construct_routing_failure([{unknown_error, Error}], undefined).
construct_routing_failure(#domain_SubFailure{code = atom_to_binary(Error)}, undefined).

normalize_rejected_routes(RejectedRoutes) ->
[normalize_rejected_route(Route) || Route <- RejectedRoutes].
Expand All @@ -2214,12 +2245,8 @@ normalize_rejected_route(#{provider_ref := _, terminal_ref := _, rejection_reaso
normalize_rejected_route(Route) ->
Route.

construct_routing_failure(Codes, Reason) ->
{failure, payproc_errors:construct('PaymentFailure', mk_static_error([no_route_found | Codes]), Reason)}.

mk_static_error([_ | _] = Codes) -> mk_static_error_(#payproc_error_GeneralFailure{}, lists:reverse(Codes)).
mk_static_error_(T, []) -> T;
mk_static_error_(Sub, [Code | Codes]) -> mk_static_error_({Code, Sub}, Codes).
construct_routing_failure(SubFailure, Reason) ->
{failure, #domain_Failure{reason = Reason, code = <<"no_route_found">>, sub = SubFailure}}.

-spec process_cash_flow_building(action(), st()) -> machine_result().
process_cash_flow_building(_Action, St) ->
Expand Down Expand Up @@ -2626,12 +2653,7 @@ get_bank_card_token(?recurrent_payer({bank_card, #domain_BankCard{token = Token}
get_bank_card_token(_) ->
undefined.

choose_fd_operation_status_for_failure({failure, Failure}) ->
payproc_errors:match('PaymentFailure', Failure, fun do_choose_fd_operation_status_for_failure/1);
choose_fd_operation_status_for_failure(_Failure) ->
finish.

do_choose_fd_operation_status_for_failure({authorization_failed, {FailType, _}}) ->
choose_fd_operation_status_for_failure({failure, ?failure(<<"authorization_failed">>, _, ?subfailure(FailType0, _))}) ->
DefaultBenignFailures = [
insufficient_funds,
rejected_by_issuer,
Expand All @@ -2640,11 +2662,17 @@ do_choose_fd_operation_status_for_failure({authorization_failed, {FailType, _}})
FDConfig = genlib_app:env(hellgate, fault_detector, #{}),
Config = genlib_map:get(conversion, FDConfig, #{}),
BenignFailures = genlib_map:get(benign_failures, Config, DefaultBenignFailures),
case lists:member(FailType, BenignFailures) of
FailType1 =
try
erlang:binary_to_existing_atom(FailType0, utf8)
catch
error:badarg -> undefined
end,
case lists:member(FailType1, BenignFailures) of
false -> error;
true -> finish
end;
do_choose_fd_operation_status_for_failure(_Failure) ->
choose_fd_operation_status_for_failure(_) ->
finish.

maybe_notify_fault_detector({payment, processing_session}, processed, Status, St) ->
Expand Down Expand Up @@ -2686,7 +2714,7 @@ get_initial_retry_strategy(TargetType) ->
St :: st(),
Timeout :: non_neg_integer().
check_retry_possibility(Target, Failure, St) ->
case check_failure_type(Target, Failure) of
case check_failure_type(Failure) of
transient ->
RetryStrategy = get_actual_retry_strategy(Target, St),
case hg_retry:next_step(RetryStrategy) of
Expand All @@ -2701,20 +2729,10 @@ check_retry_possibility(Target, Failure, St) ->
fatal
end.

-spec check_failure_type(target(), failure()) -> transient | fatal.
check_failure_type(Target, {failure, Failure}) ->
payproc_errors:match(get_error_class(Target), Failure, fun do_check_failure_type/1);
check_failure_type(_Target, _Other) ->
fatal.

get_error_class({Target, _}) when Target =:= processed; Target =:= captured; Target =:= cancelled ->
'PaymentFailure';
get_error_class(Target) ->
error({unsupported_target, Target}).

do_check_failure_type({authorization_failed, {temporarily_unavailable, _}}) ->
-spec check_failure_type(failure()) -> transient | fatal.
check_failure_type({failure, ?failure(<<"authorization_failed">>, _, ?subfailure(<<"temporarily_unavailable">>, _))}) ->
transient;
do_check_failure_type(_Failure) ->
check_failure_type(_) ->
fatal.

get_action(?processed(), _Action, St) ->
Expand Down Expand Up @@ -2796,8 +2814,8 @@ get_limit_overflow_routes(Routes, VS, Iter, St) ->
case hg_limiter:check_limits(TurnoverLimits, Invoice, Payment, Session, PaymentRoute, Iter) of
{ok, Limits} ->
{[Route | RoutesNoOverflowIn], RejectedIn, LimitsIn#{PaymentRoute => Limits}};
{error, {limit_overflow, IDs, Limits}} ->
RejectedRoute = hg_route:set_rejection_reason({'LimitOverflow', IDs}, Route),
{error, {limit_overflow, LimitIDs}, Limits} ->
RejectedRoute = hg_route:set_rejection_reason({'LimitOverflow', LimitIDs}, Route),
{RoutesNoOverflowIn, [RejectedRoute | RejectedIn], LimitsIn#{PaymentRoute => Limits}}
end
end,
Expand Down Expand Up @@ -4181,10 +4199,7 @@ format_status_details(_) ->
format_failure({operation_timeout, _}) ->
[<<"timeout">>];
format_failure({failure, Failure}) ->
format_domain_failure(Failure).

format_domain_failure(Failure) ->
payproc_errors:format_raw(Failure).
hg_invoice_utils:format_failure(Failure).

get_account_key({AccountParty, AccountType}) ->
hg_utils:join(AccountParty, $., AccountType).
Expand Down
15 changes: 5 additions & 10 deletions apps/hellgate/src/hg_invoice_payment_refund.erl
Original file line number Diff line number Diff line change
Expand Up @@ -304,11 +304,9 @@ process_refund_cashflow(Refund) ->
{next, {Events, Action}};
_ ->
Failure =
{failure,
payproc_errors:construct(
'RefundFailure',
{terms_violated, {insufficient_merchant_funds, #payproc_error_GeneralFailure{}}}
)},
{failure, #domain_Failure{
code = <<"terms_violated">>, sub = #domain_SubFailure{code = <<"insufficient_merchant_funds">>}
}},
{next, {[?refund_rollback_started(Failure)], Action}}
end.

Expand Down Expand Up @@ -483,12 +481,9 @@ check_retry_possibility(Failure, Refund) ->
fatal
end.

check_failure_type({failure, Failure}) ->
payproc_errors:match('RefundFailure', Failure, fun do_check_failure_type/1).

do_check_failure_type({authorization_failed, {temporarily_unavailable, _}}) ->
check_failure_type({failure, ?failure(<<"authorization_failed">>, _, ?subfailure(<<"temporarily_unavailable">>, _))}) ->
transient;
do_check_failure_type(_Failure) ->
check_failure_type(_) ->
fatal.

get_actual_retry_strategy(Refund) ->
Expand Down
8 changes: 8 additions & 0 deletions apps/hellgate/src/hg_invoice_utils.erl
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
-export([check_deadline/1]).
-export([assert_party_unblocked/1]).
-export([assert_shop_unblocked/1]).
-export([format_failure/1]).

-type account_id() :: dmsl_domain_thrift:'AccountID'().
-type amount() :: dmsl_domain_thrift:'Amount'().
Expand Down Expand Up @@ -180,3 +181,10 @@ check_deadline(Deadline) ->
_ ->
{error, deadline_reached}
end.

-spec format_failure(dmsl_domain_thrift:'Failure'() | undefined) -> iolist().
format_failure(Failure) -> lists:join($:, extract_failure_code(Failure)).

extract_failure_code(undefined) -> [];
Comment thread
nanodirijabl marked this conversation as resolved.
extract_failure_code(#domain_Failure{code = Code, sub = Sub}) -> [Code | extract_failure_code(Sub)];
extract_failure_code(#domain_SubFailure{code = Code, sub = Sub}) -> [Code | extract_failure_code(Sub)].
Loading
Loading