From 6b99225a16e60c33d5d137029d9d8db9657f687e Mon Sep 17 00:00:00 2001 From: Aleksey Kashapov Date: Tue, 28 Apr 2026 16:21:13 +0300 Subject: [PATCH 1/3] TECH-329: Re-implements `ListWallets` --- rebar.config | 2 +- rebar.lock | 2 +- src/wapi_domain_backend.erl | 32 +++++++++++++++++++++++++++++++ src/wapi_wallet_backend.erl | 32 +++++++++++++++++++++++++++++++ src/wapi_wallet_handler.erl | 20 +++++++++++++++++++ test/wapi_ct_helper.erl | 15 +++++++++++++++ test/wapi_wallet_tests_SUITE.erl | 33 ++++++++++++++++++++++++++++++++ 7 files changed, 134 insertions(+), 2 deletions(-) diff --git a/rebar.config b/rebar.config index b94a6c0..e6b1012 100644 --- a/rebar.config +++ b/rebar.config @@ -32,7 +32,7 @@ {thrift, {git, "https://github.com/valitydev/thrift-erlang.git", {tag, "v1.0.0"}}}, {woody, {git, "https://github.com/valitydev/woody_erlang.git", {tag, "v1.1.0"}}}, {dmt_client, {git, "https://github.com/valitydev/dmt-client.git", {tag, "v2.0.3"}}}, - {damsel, {git, "https://github.com/valitydev/damsel.git", {tag, "v2.2.17"}}}, + {damsel, {git, "https://github.com/valitydev/damsel.git", {tag, "v2.2.32"}}}, {identdocstore_proto, {git, "https://github.com/valitydev/identdocstore-proto.git", {branch, "master"}}}, {fistful_proto, {git, "https://github.com/valitydev/fistful-proto.git", {tag, "v2.0.1"}}}, {fistful_reporter_proto, {git, "https://github.com/valitydev/fistful-reporter-proto.git", {branch, "master"}}}, diff --git a/rebar.lock b/rebar.lock index 40819da..b5388c7 100644 --- a/rebar.lock +++ b/rebar.lock @@ -21,7 +21,7 @@ {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.11.0">>},2}, {<<"damsel">>, {git,"https://github.com/valitydev/damsel.git", - {ref,"f831d3aa5fdfd0338b41af44d1eeffe810ca9708"}}, + {ref,"31495ce9d95c5d1b627b349c01d9937a5ef0231c"}}, 0}, {<<"dmt_client">>, {git,"https://github.com/valitydev/dmt-client.git", diff --git a/src/wapi_domain_backend.erl b/src/wapi_domain_backend.erl index d50e9ee..e664fbc 100644 --- a/src/wapi_domain_backend.erl +++ b/src/wapi_domain_backend.erl @@ -10,6 +10,7 @@ -export([get_party_config/1]). -export([get_object/1]). -export([get_object/2]). +-export([get_with_related/3]). %% @@ -58,3 +59,34 @@ get_object(Ref, {Type, ObjectRef}) -> #domain_conf_v2_ObjectNotFound{} -> {error, notfound} end. + +-spec get_with_related( + dmt_client:object_ref(), dmt_client:version(), wapi_handler_utils:handler_context() | undefined +) -> + {ok, T, ReferencedBy :: [T], ReferencesTo :: [T]} | {error, not_found} +when + T :: dmt_client:domain_object(). +get_with_related(Ref, Revision, Context) -> + try + Opts = make_opts(Context), + #domain_conf_v2_VersionedObjectWithReferences{ + object = #domain_conf_v2_VersionedObject{object = Object}, + referenced_by = ReferencedBy, + references_to = ReferencesTo + } = + dmt_client:checkout_object_with_references(Revision, Ref, Opts), + Unwrapper = fun(#domain_conf_v2_VersionedObject{object = O}) -> O end, + {ok, Object, lists:map(Unwrapper, ReferencedBy), lists:map(Unwrapper, ReferencesTo)} + catch + error:version_not_found -> + {error, not_found}; + throw:#domain_conf_v2_ObjectNotFound{} -> + {error, not_found} + end. + +%% + +make_opts(#{woody_context := WoodyContext}) -> + #{woody_context => WoodyContext}; +make_opts(_) -> + #{}. diff --git a/src/wapi_wallet_backend.erl b/src/wapi_wallet_backend.erl index 1865c27..d591aa0 100644 --- a/src/wapi_wallet_backend.erl +++ b/src/wapi_wallet_backend.erl @@ -1,15 +1,47 @@ -module(wapi_wallet_backend). -type handler_context() :: wapi_handler_utils:handler_context(). +-type request_data() :: #{ + 'partyID' := binary() | undefined, + %% TODO Other fields are obsolete, yet mentioned in API spec. Refactor + %% request_data away after solving swag specification inconsistency. + 'currencyID' := binary() | undefined, + 'limit' := pos_integer() | undefined, + 'continuationToken' := binary() | undefined, + _ => _ +}. -type response_data() :: wapi_handler_utils:response_data(). -type id() :: binary(). +-export([list_wallets/2]). -export([get/2]). -export([get_account/2]). -include_lib("damsel/include/dmsl_domain_thrift.hrl"). -include_lib("damsel/include/dmsl_payproc_thrift.hrl"). +-spec list_wallets(request_data(), handler_context()) -> {ok, response_data()}. +list_wallets(#{'partyID' := undefined}, _Context) -> + {ok, []}; +list_wallets(#{'partyID' := PartyID}, Context) -> + PartyRef = #domain_PartyConfigRef{id = PartyID}, + case wapi_domain_backend:get_with_related({party_config, PartyRef}, wapi_domain_backend:head(), Context) of + {ok, _, ReferencedBy, _} -> + F = fun + ( + {wallet_config, #domain_WalletConfigObject{ + ref = #domain_WalletConfigRef{id = WalletID}, data = WalletConfig + }} + ) -> + {true, unmarshal(wallet, {WalletID, WalletConfig})}; + (_) -> + false + end, + {ok, lists:filtermap(F, ReferencedBy)}; + {error, not_found} -> + {ok, []} + end. + -spec get(id(), handler_context()) -> {ok, response_data(), id()} | {error, {wallet, notfound}}. get(WalletID, _HandlerContext) -> case get_wallet_config(WalletID) of diff --git a/src/wapi_wallet_handler.erl b/src/wapi_wallet_handler.erl index f750f7d..3574f74 100644 --- a/src/wapi_wallet_handler.erl +++ b/src/wapi_wallet_handler.erl @@ -73,6 +73,26 @@ mask_notfound(Resolution) -> -spec prepare(operation_id(), request_data(), handler_context(), handler_opts()) -> {ok, request_state()}. %% Wallets +prepare('ListWallets' = OperationID, Req0, Context, _Opts) -> + AuthContext = build_auth_context( + [wapi_handler_utils:maybe_with('identityID', Req0, fun(IdentityID) -> {identity, IdentityID} end)], + [], + Context + ), + {Req, PartyID} = patch_party_req(Context, Req0), + Authorize = fun() -> + Prototypes = [ + {operation, build_prototype_for(operation, #{party => PartyID, id => OperationID}, AuthContext)}, + {wallet, build_prototype_for(wallet, [], AuthContext)} + ], + Resolution = wapi_auth:authorize_operation(Prototypes, Context), + {ok, Resolution} + end, + Process = fun() -> + {ok, List} = wapi_wallet_backend:list_wallets(Req, Context), + wapi_handler_utils:reply_ok(200, #{<<"result">> => List}) + end, + {ok, #{authorize => Authorize, process => Process}}; prepare('GetWallet' = OperationID, #{'walletID' := WalletID}, Context, _Opts) -> {ResultWallet, ResultWalletOwner} = case wapi_wallet_backend:get(WalletID, Context) of diff --git a/test/wapi_ct_helper.erl b/test/wapi_ct_helper.erl index a54781e..bb5b438 100644 --- a/test/wapi_ct_helper.erl +++ b/test/wapi_ct_helper.erl @@ -194,6 +194,21 @@ start_app({dmt_client = AppName, SupPid}) -> PiObject = mk_pi_object(1, 100, 101), DomainConfigClient = fun + ('CheckoutObjectWithReferences', {{version, V}, {party_config, #domain_PartyConfigRef{id = ?STRING}}}) -> + {ok, #domain_conf_v2_VersionedObjectWithReferences{ + object = mk_versioned_object(party_config, PartyConfigObject, V), + referenced_by = ordsets:from_list([ + mk_versioned_object(wallet_config, WalletConfigObject, V), + mk_versioned_object(wallet_config, WalletConfigLimitsOk, V) + ]), + references_to = [] + }}; + ('CheckoutObjectWithReferences', {{version, V}, {party_config, #domain_PartyConfigRef{id = _} = Ref}}) -> + {ok, #domain_conf_v2_VersionedObjectWithReferences{ + object = mk_versioned_object(party_config, PartyConfigObject#domain_PartyConfigObject{ref = Ref}, V), + referenced_by = [], + references_to = [] + }}; ('CheckoutObject', {{version, V}, {wallet_config, #domain_WalletConfigRef{id = ?STRING}}}) -> {ok, mk_versioned_object(wallet_config, WalletConfigObject, V)}; ('CheckoutObject', {{version, V}, {wallet_config, #domain_WalletConfigRef{id = ?WALLET_ID_OK}}}) -> diff --git a/test/wapi_wallet_tests_SUITE.erl b/test/wapi_wallet_tests_SUITE.erl index 6b2ddf4..655e55e 100644 --- a/test/wapi_wallet_tests_SUITE.erl +++ b/test/wapi_wallet_tests_SUITE.erl @@ -21,6 +21,8 @@ -export([init/1]). -export([ + list_wallets/1, + list_wallets_party_id_passed/1, get_ok/1, get_fail_wallet_notfound/1, get_account_ok/1, @@ -53,6 +55,8 @@ all() -> groups() -> [ {base, [], [ + list_wallets, + list_wallets_party_id_passed, get_ok, get_fail_wallet_notfound, get_account_ok, @@ -104,6 +108,35 @@ end_per_testcase(_Name, C) -> %%% Tests +-spec list_wallets(config()) -> _. +list_wallets(C) -> + PartyID = ?config(party, C), + Params = #{ + qs_val => #{ + <<"limit">> => <<"123">> + } + }, + {ok, #{<<"result">> := [_ | _]}} = assert_list_wallets_party_id(PartyID, Params, C). + +-spec list_wallets_party_id_passed(config()) -> _. +list_wallets_party_id_passed(C) -> + PartyID = genlib:bsuuid(), + Params = #{ + qs_val => #{ + <<"partyID">> => PartyID, + <<"limit">> => <<"123">> + } + }, + {ok, #{<<"result">> := []}} = assert_list_wallets_party_id(PartyID, Params, C). + +assert_list_wallets_party_id(PartyID, Params, C) -> + _ = wapi_ct_helper_bouncer:mock_assert_party_op_ctx(<<"ListWallets">>, PartyID, C), + {ok, _} = call_api( + fun swag_client_wallet_wallets_api:list_wallets/3, + Params, + wapi_ct_helper:cfg(context, C) + ). + -spec get_ok(config()) -> _. get_ok(C) -> PartyID = ?config(party, C), From 06b08277d3befcd17d779006ec3746ac4e0b512b Mon Sep 17 00:00:00 2001 From: Aleksey Kashapov Date: Tue, 28 Apr 2026 16:29:54 +0300 Subject: [PATCH 2/3] Fixes op's auth context building --- src/wapi_wallet_handler.erl | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/wapi_wallet_handler.erl b/src/wapi_wallet_handler.erl index 3574f74..f70b453 100644 --- a/src/wapi_wallet_handler.erl +++ b/src/wapi_wallet_handler.erl @@ -74,12 +74,8 @@ mask_notfound(Resolution) -> %% Wallets prepare('ListWallets' = OperationID, Req0, Context, _Opts) -> - AuthContext = build_auth_context( - [wapi_handler_utils:maybe_with('identityID', Req0, fun(IdentityID) -> {identity, IdentityID} end)], - [], - Context - ), {Req, PartyID} = patch_party_req(Context, Req0), + AuthContext = build_auth_context([{party, PartyID}], [], Context), Authorize = fun() -> Prototypes = [ {operation, build_prototype_for(operation, #{party => PartyID, id => OperationID}, AuthContext)}, From 8c43dc6b5bf27ff0d0a4f51b675815e257bd7f2d Mon Sep 17 00:00:00 2001 From: Aleksey Kashapov Date: Wed, 29 Apr 2026 13:43:31 +0300 Subject: [PATCH 3/3] Refactors `list_wallets` w/ pagination via limit and token --- src/wapi_wallet_backend.erl | 80 +++++++++++++++++++++++++++++++++---- src/wapi_wallet_handler.erl | 4 +- 2 files changed, 75 insertions(+), 9 deletions(-) diff --git a/src/wapi_wallet_backend.erl b/src/wapi_wallet_backend.erl index d591aa0..a9cdd71 100644 --- a/src/wapi_wallet_backend.erl +++ b/src/wapi_wallet_backend.erl @@ -3,11 +3,11 @@ -type handler_context() :: wapi_handler_utils:handler_context(). -type request_data() :: #{ 'partyID' := binary() | undefined, - %% TODO Other fields are obsolete, yet mentioned in API spec. Refactor - %% request_data away after solving swag specification inconsistency. - 'currencyID' := binary() | undefined, 'limit' := pos_integer() | undefined, 'continuationToken' := binary() | undefined, + %% TODO Other field is obsolete, yet mentioned in API spec. Refactor + %% request_data away after solving swag specification inconsistency. + 'currencyID' := binary() | undefined, _ => _ }. -type response_data() :: wapi_handler_utils:response_data(). @@ -20,10 +20,13 @@ -include_lib("damsel/include/dmsl_domain_thrift.hrl"). -include_lib("damsel/include/dmsl_payproc_thrift.hrl"). +-define(LIST_RESULT(L, T), genlib_map:compact(#{<<"result">> => L, <<"continuationToken">> => T})). +-define(EMPTY_RESULT(T), ?LIST_RESULT([], T)). + -spec list_wallets(request_data(), handler_context()) -> {ok, response_data()}. list_wallets(#{'partyID' := undefined}, _Context) -> - {ok, []}; -list_wallets(#{'partyID' := PartyID}, Context) -> + {ok, ?EMPTY_RESULT(undefined)}; +list_wallets(#{'partyID' := PartyID, 'limit' := Limit, 'continuationToken' := ContinuationToken}, Context) -> PartyRef = #domain_PartyConfigRef{id = PartyID}, case wapi_domain_backend:get_with_related({party_config, PartyRef}, wapi_domain_backend:head(), Context) of {ok, _, ReferencedBy, _} -> @@ -37,9 +40,11 @@ list_wallets(#{'partyID' := PartyID}, Context) -> (_) -> false end, - {ok, lists:filtermap(F, ReferencedBy)}; + List = lists:filtermap(F, ReferencedBy), + Result = paginate(List, Limit, ContinuationToken), + {ok, Result}; {error, not_found} -> - {ok, []} + {ok, ?EMPTY_RESULT(ContinuationToken)} end. -spec get(id(), handler_context()) -> {ok, response_data(), id()} | {error, {wallet, notfound}}. @@ -76,6 +81,26 @@ get_wallet_config(WalletID) -> ObjectRef = {wallet_config, #domain_WalletConfigRef{id = WalletID}}, wapi_domain_backend:get_object(ObjectRef). +paginate([], _Limit, Token) -> + ?EMPTY_RESULT(Token); +paginate(List0, Limit, Token) -> + List1 = slice_with_token(List0, Token), + case lists:sublist(List1, Limit) of + [] -> + ?EMPTY_RESULT(Token); + List2 -> + NewToken = maps:get(<<"id">>, lists:last(List2)), + ?LIST_RESULT(List2, NewToken) + end. + +slice_with_token(List, undefined) -> + List; +slice_with_token(List, Token) -> + case lists:dropwhile(fun(#{<<"id">> := ID}) -> ID =/= Token end, List) of + [] -> []; + [_ | T] -> T + end. + %% Marshaling unmarshal( @@ -115,3 +140,44 @@ unmarshal(account_state, #payproc_AccountState{ }; unmarshal(T, V) -> wapi_codec:unmarshal(T, V). + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +-spec test() -> _. + +-spec paginate_test_() -> _. +paginate_test_() -> + Items = [#{<<"id">> => integer_to_binary(I)} || I <- lists:seq(1, 10)], + [ + ?_assertMatch( + #{<<"result">> := L, <<"continuationToken">> := <<"10">>} when length(L) =:= 10, + paginate(Items, 10, undefined) + ), + ?_assertMatch( + #{<<"result">> := L, <<"continuationToken">> := <<"10">>} when length(L) =:= 10, + paginate(Items, 999, undefined) + ), + ?_assertMatch( + #{<<"result">> := L, <<"continuationToken">> := <<"5">>} when length(L) =:= 5, + paginate(Items, 5, undefined) + ), + ?_assertMatch( + #{<<"result">> := L, <<"continuationToken">> := <<"10">>} when length(L) =:= 5, + paginate(Items, 5, <<"5">>) + ), + ?_assertMatch( + #{<<"result">> := [], <<"continuationToken">> := <<"10">>}, + paginate(Items, 5, <<"10">>) + ), + ?_assertMatch( + #{<<"result">> := [], <<"continuationToken">> := <<"999">>}, + paginate(Items, 5, <<"999">>) + ), + ?_assertMatch( + #{<<"result">> := []}, + paginate([], 5, undefined) + ) + ]. + +-endif. diff --git a/src/wapi_wallet_handler.erl b/src/wapi_wallet_handler.erl index f70b453..43eb2de 100644 --- a/src/wapi_wallet_handler.erl +++ b/src/wapi_wallet_handler.erl @@ -85,8 +85,8 @@ prepare('ListWallets' = OperationID, Req0, Context, _Opts) -> {ok, Resolution} end, Process = fun() -> - {ok, List} = wapi_wallet_backend:list_wallets(Req, Context), - wapi_handler_utils:reply_ok(200, #{<<"result">> => List}) + {ok, Result} = wapi_wallet_backend:list_wallets(Req, Context), + wapi_handler_utils:reply_ok(200, Result) end, {ok, #{authorize => Authorize, process => Process}}; prepare('GetWallet' = OperationID, #{'walletID' := WalletID}, Context, _Opts) ->