EMP-74: Adds scope support for withdrawal's destination field, e.g. phone number (#31)

* Adds scope support for withdrawal's destination phone number

* Implements destination_field limit scope

* Adds hold and commit testcases for destination_field scope

* Moves field path into hashed value

* Adds destination_field support for domain object

* Bumps stable deps
This commit is contained in:
Aleksey Kashapov 2024-10-29 17:54:11 +03:00 committed by GitHub
parent 9c1ee9fb00
commit 0fc3b48a18
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 219 additions and 31 deletions

View File

@ -139,6 +139,8 @@ marshal_scope_type(terminal) ->
{terminal, #config_LimitScopeEmptyDetails{}};
marshal_scope_type(payer_contact_email) ->
{payer_contact_email, #config_LimitScopeEmptyDetails{}};
marshal_scope_type({destination_field, FieldPath}) ->
{destination_field, #config_LimitScopeDestinationFieldDetails{field_path = FieldPath}};
marshal_scope_type(sender) ->
{sender, #config_LimitScopeEmptyDetails{}};
marshal_scope_type(receiver) ->
@ -354,6 +356,12 @@ unmarshal_scope_type({terminal, _}) ->
terminal;
unmarshal_scope_type({payer_contact_email, _}) ->
payer_contact_email;
unmarshal_scope_type({destination_field, #config_LimitScopeDestinationFieldDetails{field_path = FieldPath}}) ->
%% Limiter proto variant clause
{destination_field, FieldPath};
unmarshal_scope_type({destination_field, #limiter_config_LimitScopeDestinationFieldDetails{field_path = FieldPath}}) ->
%% Domain config variant clause
{destination_field, FieldPath};
unmarshal_scope_type({sender, _}) ->
sender;
unmarshal_scope_type({receiver, _}) ->
@ -441,7 +449,7 @@ unmarshal_config_object_test() ->
time_range_type => {calendar, day},
context_type => payment_processing,
type => {turnover, number},
scope => ordsets:from_list([party, shop]),
scope => ordsets:from_list([party, shop, {destination_field, [<<"path">>, <<"to">>, <<"field">>]}]),
description => <<"description">>,
currency_conversion => true
},
@ -457,7 +465,11 @@ unmarshal_config_object_test() ->
type =
{turnover, #limiter_config_LimitTypeTurnover{metric = {number, #limiter_config_LimitTurnoverNumber{}}}},
scopes = ordsets:from_list([
{'party', #limiter_config_LimitScopeEmptyDetails{}}, {'shop', #limiter_config_LimitScopeEmptyDetails{}}
{'party', #limiter_config_LimitScopeEmptyDetails{}},
{'shop', #limiter_config_LimitScopeEmptyDetails{}},
{'destination_field', #limiter_config_LimitScopeDestinationFieldDetails{
field_path = [<<"path">>, <<"to">>, <<"field">>]
}}
]),
description = <<"description">>,
currency_conversion = #limiter_config_CurrencyConversion{}

View File

@ -41,7 +41,16 @@
-type limit_type() :: {turnover, lim_turnover_metric:t()}.
-type limit_scope() :: ordsets:ordset(limit_scope_type()).
-type limit_scope_type() :: party | shop | wallet | identity | payment_tool | provider | terminal | payer_contact_email.
-type limit_scope_type() ::
party
| shop
| wallet
| identity
| payment_tool
| provider
| terminal
| payer_contact_email
| {destination_field, [Field :: binary()]}.
-type shard_size() :: pos_integer().
-type shard_id() :: binary().
-type prefix() :: binary().
@ -579,8 +588,34 @@ append_prefix(Fragment, Acc) ->
-spec enumerate_context_bits(limit_scope()) -> [context_bit()].
enumerate_context_bits(Types) ->
TypesOrder =
[party, shop, identity, wallet, payment_tool, provider, terminal, payer_contact_email, sender, receiver],
SortedTypes = lists:filter(fun(T) -> ordsets:is_element(T, Types) end, TypesOrder),
[
party,
shop,
identity,
wallet,
payment_tool,
provider,
terminal,
payer_contact_email,
%% Scope 'destination_field' differs from other scope
%% types by having an attribute and being represented as a
%% tuple '{destination_field, [Field :: binary()]}'.
destination_field,
sender,
receiver
],
SortedTypes = lists:filtermap(
fun
(destination_field) ->
case lists:keyfind(destination_field, 1, Types) of
{destination_field, _} = Found -> {true, Found};
_ -> false
end;
(T) ->
ordsets:is_element(T, Types)
end,
TypesOrder
),
SquashedTypes = squash_scope_types(SortedTypes),
lists:flatmap(fun get_context_bits/1, SquashedTypes).
@ -620,6 +655,8 @@ get_context_bits(terminal) ->
[{prefix, <<"terminal">>}, {from, provider_id}, {from, terminal_id}];
get_context_bits(payer_contact_email) ->
[{prefix, <<"payer_contact_email">>}, {from, payer_contact_email}];
get_context_bits({destination_field, FieldPath}) ->
[{prefix, <<"destination">>}, {from, {destination_field, FieldPath}}];
get_context_bits(sender) ->
[{prefix, <<"sender">>}, {from, sender}];
get_context_bits(receiver) ->
@ -629,6 +666,15 @@ get_context_bits(receiver) ->
{ok, binary()} | {error, lim_context:context_error()}.
extract_context_bit({prefix, Prefix}, _ContextType, _LimitContext) ->
{ok, Prefix};
extract_context_bit({from, {destination_field, FieldPath}}, ContextType, LimitContext) ->
do(fun() ->
#{type := Type, data := Data} = unwrap(get_generic_payment_tool_resource(ContextType, LimitContext)),
DecodedData = unwrap(lim_context_utils:decode_content(Type, Data)),
FieldValue = unwrap(lim_context_utils:get_field_by_path(FieldPath, DecodedData)),
PrefixedValue = lim_string:join($., FieldPath ++ [FieldValue]),
HashedValue = lim_context_utils:base61_hash(PrefixedValue),
mk_scope_component([HashedValue])
end);
extract_context_bit({from, payment_tool}, ContextType, LimitContext) ->
case lim_context:get_value(ContextType, payment_tool, LimitContext) of
{ok, {bank_card, #{token := Token, exp_date := {Month, Year}}}} ->
@ -637,6 +683,9 @@ extract_context_bit({from, payment_tool}, ContextType, LimitContext) ->
{ok, mk_scope_component([Token, <<"undefined">>])};
{ok, {digital_wallet, #{id := ID, service := Service}}} ->
{ok, mk_scope_component([<<"DW">>, Service, ID])};
%% Generic payment tool is supposed to be not supported
{ok, {generic = Type, _}} ->
{error, {unsupported, {payment_tool, Type}}};
{error, _} = Error ->
Error
end;
@ -646,6 +695,16 @@ extract_context_bit({from, ValueName}, ContextType, LimitContext) ->
mk_scope_component(Fragments) ->
lim_string:join($/, Fragments).
get_generic_payment_tool_resource(ContextType, LimitContext) ->
case lim_context:get_value(ContextType, payment_tool, LimitContext) of
{ok, {generic, #{data := ResourceData}}} ->
{ok, ResourceData};
{ok, {ToolType, _}} ->
{error, {unsupported, ToolType}};
{error, _} = Error ->
Error
end.
%%% Machinery callbacks
-spec init(args([event()]), machine(), handler_args(), handler_opts()) -> result().

View File

@ -5,6 +5,9 @@
-export([route_provider_id/1]).
-export([route_terminal_id/1]).
-export([base61_hash/1]).
-export([decode_content/2]).
-export([get_field_by_path/2]).
-type provider_id() :: binary().
-type terminal_id() :: binary().
@ -20,3 +23,28 @@ route_provider_id(#base_Route{provider = #domain_ProviderRef{id = ID}}) ->
{ok, terminal_id()}.
route_terminal_id(#base_Route{terminal = #domain_TerminalRef{id = ID}}) ->
{ok, genlib:to_binary(ID)}.
-spec base61_hash(iolist() | binary()) -> binary().
base61_hash(IOList) ->
<<I:160/integer>> = crypto:hash(sha, IOList),
genlib_format:format_int_base(I, 61).
-spec decode_content(Type, binary()) ->
{ok, map()} | {error, {unsupported, Type}}
when
Type :: binary().
decode_content(<<"application/schema-instance+json; schema=", _/binary>>, Data) ->
{ok, jsx:decode(Data)};
decode_content(<<"application/json">>, Data) ->
{ok, jsx:decode(Data)};
decode_content(Type, _Data) ->
{error, {unsupported, Type}}.
-spec get_field_by_path([binary()], map()) -> {ok, map() | binary() | number()} | {error, notfound}.
get_field_by_path([], Data) ->
{ok, Data};
get_field_by_path([Key | Path], Data) ->
case maps:get(Key, Data, undefined) of
undefined -> {error, notfound};
Value -> get_field_by_path(Path, Value)
end.

View File

@ -1,6 +1,7 @@
-module(lim_payproc_utils).
-include_lib("damsel/include/dmsl_domain_thrift.hrl").
-include_lib("damsel/include/dmsl_base_thrift.hrl").
-export([cash/1]).
-export([payment_tool/1]).
@ -16,6 +17,13 @@
| {digital_wallet, #{
id := binary(),
service := binary()
}}
| {generic, #{
service := binary(),
data := #{
type := binary(),
data := binary()
}
}}.
%%
@ -39,6 +47,18 @@ payment_tool({digital_wallet, DW}) ->
id => DW#domain_DigitalWallet.id,
service => DW#domain_DigitalWallet.payment_service#domain_PaymentServiceRef.id
}}};
payment_tool({generic, G}) ->
%% TODO Move to codec into marshal/unmarshal clauses
Content = G#domain_GenericPaymentTool.data,
{ok,
{generic, #{
service => G#domain_GenericPaymentTool.payment_service#domain_PaymentServiceRef.id,
data => #{
%% TODO Content decoding
type => Content#base_Content.type,
data => Content#base_Content.data
}
}}};
payment_tool({Type, _}) ->
{error, {unsupported, {payment_tool, Type}}}.

View File

@ -127,15 +127,11 @@ get_terminal_id(_, _CtxWithdrawal) ->
{error, notfound}.
get_destination_sender(withdrawal, ?SENDER_RECEIVER(#wthd_domain_SenderReceiverAuthData{sender = Token})) ->
{ok, hash_token(Token)};
{ok, lim_context_utils:base61_hash(Token)};
get_destination_sender(_, _CtxWithdrawal) ->
{error, notfound}.
get_destination_receiver(withdrawal, ?SENDER_RECEIVER(#wthd_domain_SenderReceiverAuthData{receiver = Token})) ->
{ok, hash_token(Token)};
{ok, lim_context_utils:base61_hash(Token)};
get_destination_receiver(_, _CtxWithdrawal) ->
{error, notfound}.
hash_token(Token) ->
<<I:160/integer>> = crypto:hash(sha, Token),
genlib_format:format_int_base(I, 61).

View File

@ -7,6 +7,7 @@
-include_lib("limiter_proto/include/limproto_limiter_thrift.hrl").
-include_lib("limiter_proto/include/limproto_context_payproc_thrift.hrl").
-include_lib("limiter_proto/include/limproto_context_withdrawal_thrift.hrl").
-include_lib("damsel/include/dmsl_base_thrift.hrl").
-include_lib("damsel/include/dmsl_domain_thrift.hrl").
-include_lib("damsel/include/dmsl_wthd_domain_thrift.hrl").
@ -41,6 +42,9 @@
-define(scope_wallet(), {wallet, #config_LimitScopeEmptyDetails{}}).
-define(scope_sender(), {sender, #config_LimitScopeEmptyDetails{}}).
-define(scope_receiver(), {receiver, #config_LimitScopeEmptyDetails{}}).
-define(scope_destination_field(FieldPath),
{destination_field, #config_LimitScopeDestinationFieldDetails{field_path = FieldPath}}
).
-define(lim_type_turnover(), ?lim_type_turnover(?turnover_metric_number())).
-define(lim_type_turnover(Metric),
@ -116,6 +120,15 @@
}}
).
-define(generic_pt(),
{generic, #domain_GenericPaymentTool{
payment_service = #domain_PaymentServiceRef{id = <<"ID42">>},
data = #base_Content{
type = <<"application/json">>, data = <<"{\"opaque\":{\"payload\":{\"data\":\"value\"}}}">>
}
}}
).
-define(invoice(OwnerID, ShopID, Cost), #domain_Invoice{
id = ?string,
owner_id = OwnerID,
@ -153,15 +166,20 @@
}}
}).
-define(payproc_ctx_invoice(Cost), #limiter_LimitContext{
-define(payproc_ctx(Op, Invoice, InvoicePayment), #limiter_LimitContext{
payment_processing = #context_payproc_Context{
op = ?op_invoice,
op = Op,
invoice = #context_payproc_Invoice{
invoice = ?invoice(?string, ?string, Cost)
invoice = Invoice,
payment = InvoicePayment
}
}
}).
-define(payproc_ctx(Invoice, InvoicePayment), ?payproc_ctx(?op_invoice, Invoice, InvoicePayment)).
-define(payproc_ctx_invoice(Cost), ?payproc_ctx(?invoice(?string, ?string, Cost), undefined)).
-define(payproc_ctx_payment(Cost, CaptureCost),
?payproc_ctx_payment(?string, ?string, Cost, CaptureCost)
).
@ -236,26 +254,21 @@
-define(op_withdrawal, {withdrawal, #context_withdrawal_OperationWithdrawal{}}).
-define(wthdproc_ctx_withdrawal(Cost), #limiter_LimitContext{
-define(wthdproc_ctx(Withdrawal), #limiter_LimitContext{
withdrawal_processing = #context_withdrawal_Context{
op = ?op_withdrawal,
withdrawal = #context_withdrawal_Withdrawal{
withdrawal = ?withdrawal(Cost),
withdrawal = Withdrawal,
route = ?route(),
wallet_id = ?string
}
}
}).
-define(wthdproc_ctx_withdrawal_w_auth_data(Cost, Sender, Receiver), #limiter_LimitContext{
withdrawal_processing = #context_withdrawal_Context{
op = ?op_withdrawal,
withdrawal = #context_withdrawal_Withdrawal{
withdrawal = ?withdrawal(Cost, ?bank_card(), ?string, ?auth_data(Sender, Receiver)),
route = ?route(),
wallet_id = ?string
}
}
}).
-define(wthdproc_ctx_withdrawal(Cost), ?wthdproc_ctx(?withdrawal(Cost))).
-define(wthdproc_ctx_withdrawal_w_auth_data(Cost, Sender, Receiver),
?wthdproc_ctx(?withdrawal(Cost, ?bank_card(), ?string, ?auth_data(Sender, Receiver)))
).
-endif.

View File

@ -59,9 +59,12 @@
-export([commit_with_multi_scope_ok/1]).
-export([hold_with_sender_notfound/1]).
-export([hold_with_receiver_notfound/1]).
-export([hold_with_destination_field_not_found/1]).
-export([hold_with_destination_field_not_supported/1]).
-export([commit_with_sender_scope_ok/1]).
-export([commit_with_receiver_scope_ok/1]).
-export([commit_with_sender_receiver_scope_ok/1]).
-export([commit_with_destination_field_scope_ok/1]).
-type group_name() :: atom().
-type test_case_name() :: atom().
@ -108,7 +111,9 @@ groups() ->
commit_with_email_scope_ok,
commit_with_multi_scope_ok,
hold_with_sender_notfound,
hold_with_receiver_notfound
hold_with_receiver_notfound,
hold_with_destination_field_not_found,
hold_with_destination_field_not_supported
]},
{default, [], [
{group, base},
@ -130,7 +135,9 @@ groups() ->
commit_with_wallet_scope_ok,
commit_with_sender_scope_ok,
commit_with_receiver_scope_ok,
commit_with_sender_receiver_scope_ok
commit_with_sender_receiver_scope_ok,
commit_with_destination_field_scope_ok,
hold_with_destination_field_not_supported
]},
{cashless, [parallel], [
commit_number_ok,
@ -659,12 +666,15 @@ commit_with_terminal_scope_ok(C) ->
_ = commit_with_some_scope(?scope([?scope_terminal()]), C).
commit_with_some_scope(Scope, C) ->
{ID, Version} = configure_limit(?time_range_month(), Scope, C),
Context =
case get_group_name(C) of
withdrawals -> ?wthdproc_ctx_withdrawal_w_auth_data(?cash(10, <<"RUB">>), ?token, ?token);
_Default -> ?payproc_ctx_payment(?cash(10, <<"RUB">>), ?cash(10, <<"RUB">>))
end,
commit_with_some_scope(Scope, Context, C).
commit_with_some_scope(Scope, Context, C) ->
{ID, Version} = configure_limit(?time_range_month(), Scope, C),
{ok, {vector, _}} = hold_and_commit(?LIMIT_CHANGE(ID, ?CHANGE_ID, Version), Context, ?config(client, C)),
{ok, #limiter_Limit{}} = lim_client:get(ID, Version, Context, ?config(client, C)).
@ -728,12 +738,30 @@ hold_with_sender_notfound(C) ->
hold_with_receiver_notfound(C) ->
hold_with_scope_notfound([?scope_receiver()], C).
-spec hold_with_destination_field_not_found(config()) -> _.
hold_with_destination_field_not_found(C) ->
Scopes = [?scope_destination_field([<<"not">>, <<"existing">>, <<"field">>])],
Context =
case get_group_name(C) of
withdrawals -> ?wthdproc_ctx(?withdrawal(?cash(0), ?generic_pt(), ?string));
_Default -> ?payproc_ctx(?invoice(?string, ?string, ?cash(0)), undefined)
end,
hold_with_scope_notfound(Scopes, Context, C).
-spec hold_with_destination_field_not_supported(config()) -> _.
hold_with_destination_field_not_supported(C) ->
Scopes = [?scope_destination_field([<<"opaque">>, <<"payload">>, <<"data">>])],
hold_with_scope_unsupported(Scopes, C).
hold_with_scope_notfound(Scopes, C) ->
Context =
case get_group_name(C) of
withdrawals -> ?wthdproc_ctx_withdrawal(?cash(0));
_Default -> ?payproc_ctx_invoice(?cash(0))
end,
hold_with_scope_notfound(Scopes, Context, C).
hold_with_scope_notfound(Scopes, Context, C) ->
{ID, Version} = configure_limit(?time_range_month(), ?scope(Scopes), C),
?assertException(
error,
@ -742,6 +770,28 @@ hold_with_scope_notfound(Scopes, C) ->
lim_client:hold(?LIMIT_CHANGE(ID, ?CHANGE_ID, Version), Context, ?config(client, C))
).
hold_with_scope_unsupported(Scopes, C) ->
{ID, Version} = configure_limit(?time_range_month(), ?scope(Scopes), C),
Context =
case get_group_name(C) of
withdrawals ->
?wthdproc_ctx(?withdrawal(?cash(10, <<"RUB">>), ?bank_card(), ?string));
_Default ->
?payproc_ctx(
?op_payment, ?invoice(?string, ?string, ?cash(10, <<"RUB">>)), #context_payproc_InvoicePayment{
payment = ?invoice_payment(?cash(10, <<"RUB">>), ?cash(10, <<"RUB">>)),
route = ?route()
}
)
end,
?assertException(
error,
{woody_error,
{external, result_unexpected,
<<"error:{unknown_error,{lim_turnover_processor,{unsupported,bank_card}}}", _/binary>>}},
lim_client:hold(?LIMIT_CHANGE(ID, ?CHANGE_ID, Version), Context, ?config(client, C))
).
-spec commit_with_sender_scope_ok(config()) -> _.
commit_with_sender_scope_ok(C) ->
_ = commit_with_some_scope(?scope([?scope_sender()]), C).
@ -754,6 +804,16 @@ commit_with_receiver_scope_ok(C) ->
commit_with_sender_receiver_scope_ok(C) ->
_ = commit_with_some_scope(?scope([?scope_sender(), ?scope_receiver()]), C).
-spec commit_with_destination_field_scope_ok(config()) -> _.
commit_with_destination_field_scope_ok(C) ->
Scopes = [?scope_destination_field([<<"opaque">>, <<"payload">>, <<"data">>])],
Context =
case get_group_name(C) of
withdrawals -> ?wthdproc_ctx(?withdrawal(?cash(10, <<"RUB">>), ?generic_pt(), ?string));
_Default -> ?payproc_ctx_payment(?cash(10, <<"RUB">>), ?cash(10, <<"RUB">>))
end,
_ = commit_with_some_scope(?scope(Scopes), Context, C).
%%
gen_change_id(LimitID, ChangeID) ->

View File

@ -12,7 +12,7 @@
{<<"ctx">>,{pkg,<<"ctx">>,<<"0.6.0">>},2},
{<<"damsel">>,
{git,"https://github.com/valitydev/damsel.git",
{ref,"500c0a2aab6dc90db0c0b7ea4cd9757fd86368d0"}},
{ref,"7ed2112a6503abe9f65142e43dca6675e939d164"}},
0},
{<<"dmt_client">>,
{git,"https://github.com/valitydev/dmt_client.git",
@ -38,7 +38,7 @@
{<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},1},
{<<"limiter_proto">>,
{git,"https://github.com/valitydev/limiter-proto.git",
{ref,"10328404f1cea68586962ed7fce0405b18d62b28"}},
{ref,"2483f600d6f00193a7e8493bcefd6654d14eaa6a"}},
0},
{<<"machinery">>,
{git,"https://github.com/valitydev/machinery-erlang.git",