FF-219: wapi getP2PTransferEvents via thrift backend (#322) (#336)

This commit is contained in:
dinama 2020-11-19 22:51:23 +03:00 committed by GitHub
parent 619720f0e9
commit c229d491ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 860 additions and 236 deletions

View File

@ -117,7 +117,8 @@ start_app(wapi = AppName) ->
validation_opts => #{
custom_validator => wapi_swagger_validator
}
}}
}},
{events_fetch_limit, 32}
]), #{}};
start_app(wapi_woody_client = AppName) ->
@ -129,10 +130,11 @@ start_app(wapi_woody_client = AppName) ->
fistful_wallet => "http://localhost:8022/v1/wallet",
fistful_identity => "http://localhost:8022/v1/identity",
fistful_destination => "http://localhost:8022/v1/destination",
w2w_transfer => "http://localhost:8022/v1/w2w_transfer",
p2p_transfer => "http://localhost:8022/v1/p2p_transfer",
fistful_withdrawal => "http://localhost:8022/v1/withdrawal",
fistful_p2p_template => "http://localhost:8022/v1/p2p_template"
fistful_w2w_transfer => "http://localhost:8022/v1/w2w_transfer",
fistful_p2p_template => "http://localhost:8022/v1/p2p_template",
fistful_p2p_transfer => "http://localhost:8022/v1/p2p_transfer",
fistful_p2p_session => "http://localhost:8022/v1/p2p_transfer/session"
}},
{service_retries, #{
fistful_stat => #{

View File

@ -8,6 +8,7 @@
-export([marshal_state/2]).
-export([marshal_event/1]).
-export([marshal/2]).
-export([unmarshal/2]).
@ -26,6 +27,16 @@ marshal_state(State, Context) ->
context = marshal(ctx, Context)
}.
-spec marshal_event(p2p_transfer_machine:event()) ->
ff_proto_p2p_session_thrift:'Event'().
marshal_event({EventID, {ev, Timestamp, Change}}) ->
#p2p_session_Event{
event = ff_codec:marshal(event_id, EventID),
occured_at = ff_codec:marshal(timestamp, Timestamp),
change = marshal(change, Change)
}.
-spec marshal(ff_codec:type_name(), ff_codec:decoded_value()) ->
ff_codec:encoded_value().

View File

@ -41,4 +41,13 @@ handle_function_('GetContext', [ID], _Opts) ->
{ok, ff_codec:marshal(context, Context)};
{error, {unknown_p2p_session, _Ref}} ->
woody_error:raise(business, #fistful_P2PSessionNotFound{})
end;
handle_function_('GetEvents', [ID, EventRange], _Opts) ->
ok = scoper:add_meta(#{id => ID}),
case p2p_session_machine:events(ID, ff_codec:unmarshal(event_range, EventRange)) of
{ok, Events} ->
{ok, lists:map(fun ff_p2p_session_codec:marshal_event/1, Events)};
{error, {unknown_p2p_session, _Ref}} ->
woody_error:raise(business, #fistful_P2PSessionNotFound{})
end.

View File

@ -15,6 +15,7 @@
-export([end_per_testcase/2]).
%% Tests
-export([get_p2p_session_events_ok_test/1]).
-export([get_p2p_session_context_ok_test/1]).
-export([get_p2p_session_ok_test/1]).
-export([create_adjustment_ok_test/1]).
@ -40,6 +41,7 @@ all() ->
groups() ->
[
{default, [parallel], [
get_p2p_session_events_ok_test,
get_p2p_session_context_ok_test,
get_p2p_session_ok_test,
create_adjustment_ok_test,
@ -92,6 +94,13 @@ end_per_testcase(_Name, _C) ->
%% Tests
-spec get_p2p_session_events_ok_test(config()) -> test_return().
get_p2p_session_events_ok_test(C) ->
#{
session_id := ID
} = prepare_standard_environment(C),
{ok, [#p2p_session_Event{change = {created, _}} | _Rest]} = call_p2p_session('GetEvents', [ID, #'EventRange'{}]).
-spec get_p2p_session_context_ok_test(config()) -> test_return().
get_p2p_session_context_ok_test(C) ->
#{

View File

@ -88,7 +88,7 @@ get_context_by_id(p2p_template, TemplateID, WoodyCtx) ->
{error, notfound}
end;
get_context_by_id(w2w_transfer, W2WTransferID, WoodyCtx) ->
Request = {w2w_transfer, 'GetContext', [W2WTransferID]},
Request = {fistful_w2w_transfer, 'GetContext', [W2WTransferID]},
case wapi_handler_utils:service_call(Request, WoodyCtx) of
{ok, Context} ->
Context;
@ -96,7 +96,7 @@ get_context_by_id(w2w_transfer, W2WTransferID, WoodyCtx) ->
{error, notfound}
end;
get_context_by_id(p2p_transfer, P2PTransferID, WoodyCtx) ->
Request = {p2p_transfer, 'GetContext', [P2PTransferID]},
Request = {fistful_p2p_transfer, 'GetContext', [P2PTransferID]},
case wapi_handler_utils:service_call(Request, WoodyCtx) of
{ok, Context} ->
Context;

View File

@ -192,19 +192,24 @@ quote_transfer(ID, Params, HandlerContext) ->
{error, {token, _}} |
{error, {external_id_conflict, _}}.
create_transfer(ID, #{quote_token := Token} = Params, HandlerContext) ->
create_transfer(ID, #{<<"quoteToken">> := Token} = Params, HandlerContext) ->
case uac_authorizer_jwt:verify(Token, #{}) of
{ok, {_, _, VerifiedToken}} ->
case decode_and_validate_token_payload(VerifiedToken, ID, HandlerContext) of
{ok, Quote} ->
create_transfer(ID, Params#{<<"quote">> => Quote}, HandlerContext);
do_create_transfer(ID, Params#{<<"quote">> => Quote}, HandlerContext);
{error, token_expired} ->
{error, {token, expired}}
{error, {token, expired}};
{error, Error} ->
{error, {token, {not_verified, Error}}}
end;
{error, Error} ->
{error, {token, {not_verified, Error}}}
end;
create_transfer(ID, Params, HandlerContext) ->
do_create_transfer(ID, Params, HandlerContext).
do_create_transfer(ID, Params, HandlerContext) ->
case wapi_access_backend:check_resource_by_id(p2p_template, ID, HandlerContext) of
ok ->
TransferID = context_transfer_id(HandlerContext),
@ -212,7 +217,7 @@ create_transfer(ID, Params, HandlerContext) ->
ok ->
MarshaledContext = marshal_context(wapi_backend_utils:make_ctx(Params, HandlerContext)),
MarshaledParams = marshal_transfer_params(Params#{<<"id">> => TransferID}),
create_transfer(ID, MarshaledParams, MarshaledContext, HandlerContext);
call_create_transfer(ID, MarshaledParams, MarshaledContext, HandlerContext);
{error, {external_id_conflict, _}} = Error ->
Error
end;
@ -221,7 +226,8 @@ create_transfer(ID, Params, HandlerContext) ->
{error, notfound} ->
{error, {p2p_template, notfound}}
end.
create_transfer(ID, MarshaledParams, MarshaledContext, HandlerContext) ->
call_create_transfer(ID, MarshaledParams, MarshaledContext, HandlerContext) ->
Request = {fistful_p2p_template, 'CreateTransfer', [ID, MarshaledParams, MarshaledContext]},
case wapi_handler_utils:service_call(Request, HandlerContext) of
{ok, Transfer} ->
@ -466,10 +472,10 @@ validate_transfer_id(TransferID, Params, #{woody_context := WoodyContext} = Hand
validate_identity_id(IdentityID, TemplateID, HandlerContext) ->
case get(TemplateID, HandlerContext) of
{ok, #{identity_id := IdentityID}} ->
{ok, #{<<"identityID">> := IdentityID}} ->
ok;
{ok, _ } ->
{error, {token, {not_verified, identity_mismatch}}};
{ok, _Template} ->
{error, identity_mismatch};
Error ->
Error
end.
@ -591,23 +597,27 @@ marshal_transfer_params(#{
marshal_sender(#{
<<"token">> := Token,
<<"authData">> := AuthData,
<<"contactInfo">> := ContactInfo
}) ->
Resource = case wapi_crypto:decrypt_bankcard_token(Token) of
ResourceBankCard = case wapi_crypto:decrypt_bankcard_token(Token) of
unrecognized ->
BankCard = wapi_utils:base64url_to_map(Token),
{bank_card, #'ResourceBankCard'{
#'ResourceBankCard'{
bank_card = #'BankCard'{
token = maps:get(<<"token">>, BankCard),
bin = maps:get(<<"bin">>, BankCard),
masked_pan = maps:get(<<"lastDigits">>, BankCard)
}
}};
};
{ok, BankCard} ->
{bank_card, #'ResourceBankCard'{bank_card = BankCard}}
#'ResourceBankCard'{bank_card = BankCard}
end,
ResourceBankCardAuth = ResourceBankCard#'ResourceBankCard'{
auth_data = {session_data, #'SessionAuthData'{id = AuthData}}
},
{resource, #p2p_transfer_RawResource{
resource = Resource,
resource = {bank_card, ResourceBankCardAuth},
contact_info = marshal_contact_info(ContactInfo)
}}.

View File

@ -35,13 +35,31 @@
| {p2p_transfer, notfound}
.
-type error_get_events()
:: error_get()
| {token, {unsupported_version, _}}
| {token, {not_verified, _}}
.
-export([create_transfer/2]).
-export([quote_transfer/2]).
-export([get_transfer/2]).
-export([quote_transfer/2]).
-export([get_transfer_events/3]).
-import(ff_pipeline, [do/1, unwrap/1]).
-include_lib("fistful_proto/include/ff_proto_base_thrift.hrl").
-include_lib("fistful_proto/include/ff_proto_p2p_transfer_thrift.hrl").
-include_lib("fistful_proto/include/ff_proto_p2p_session_thrift.hrl").
-define(DEFAULT_EVENTS_LIMIT, 50).
-define(CONTINUATION_TRANSFER, <<"p2p_transfer_event_id">>).
-define(CONTINUATION_SESSION, <<"p2p_session_event_id">>).
-type event() :: #p2p_transfer_Event{} | #p2p_session_Event{}.
-type event_service() :: fistful_p2p_transfer | fistful_p2p_session.
-type event_range() :: #'EventRange'{}.
-type event_id() :: ff_proto_base_thrift:'EventID'() | undefined.
-spec create_transfer(req_data(), handler_context()) ->
{ok, response_data()} | {error, error_create()}.
@ -63,7 +81,7 @@ create_transfer(Params = #{<<"identityID">> := IdentityID}, HandlerContext) ->
-spec get_transfer(req_data(), handler_context()) ->
{ok, response_data()} | {error, error_get()}.
get_transfer(ID, HandlerContext) ->
Request = {p2p_transfer, 'Get', [ID, #'EventRange'{}]},
Request = {fistful_p2p_transfer, 'Get', [ID, #'EventRange'{}]},
case service_call(Request, HandlerContext) of
{ok, TransferThrift} ->
case wapi_access_backend:check_resource(p2p_transfer, TransferThrift, HandlerContext) of
@ -89,10 +107,23 @@ quote_transfer(Params = #{<<"identityID">> := IdentityID}, HandlerContext) ->
{error, {identity, notfound}}
end.
-spec get_transfer_events(id(), binary() | undefined, handler_context()) ->
{ok, response_data()} | {error, error_get_events()}.
get_transfer_events(ID, Token, HandlerContext) ->
case wapi_access_backend:check_resource_by_id(p2p_transfer, ID, HandlerContext) of
ok ->
do_get_events(ID, Token, HandlerContext);
{error, unauthorized} ->
{error, {p2p_transfer, unauthorized}};
{error, notfound} ->
{error, {p2p_transfer, notfound}}
end.
%% Internal
do_quote_transfer(Params, HandlerContext) ->
Request = {p2p_transfer, 'GetQuote', [marshal_quote_params(Params)]},
Request = {fistful_p2p_transfer, 'GetQuote', [marshal_quote_params(Params)]},
case service_call(Request, HandlerContext) of
{ok, Quote} ->
PartyID = wapi_handler_utils:get_owner(HandlerContext),
@ -125,7 +156,7 @@ do_create_transfer(ID, Params, HandlerContext) ->
do(fun() ->
Context = wapi_backend_utils:make_ctx(Params, HandlerContext),
TransferParams = unwrap(build_transfer_params(Params#{<<"id">> => ID})),
Request = {p2p_transfer, 'Create', [TransferParams, marshal(context, Context)]},
Request = {fistful_p2p_transfer, 'Create', [TransferParams, marshal(context, Context)]},
unwrap(process_p2p_transfer_call(Request, HandlerContext))
end).
@ -174,6 +205,202 @@ authorize_p2p_quote_token(_Quote, _IdentityID) ->
service_call(Params, HandlerContext) ->
wapi_handler_utils:service_call(Params, HandlerContext).
%% @doc
%% The function returns the list of events for the specified Transfer.
%%
%% First get Transfer for extract the Session ID.
%%
%% Then, the Continuation Token is verified. Latest EventIDs of Transfer and
%% Session are stored in the token for possibility partial load of events.
%%
%% The events are retrieved no lesser ID than those stored in the token, and count
%% is limited by wapi.events_fetch_limit option or ?DEFAULT_EVENTS_LIMIT
%%
%% The received events are then mixed and ordered by the time of occurrence.
%% The resulting set is returned to the client.
%%
%% @todo Now there is always only zero or one session. But there may be more than one
%% session in the future, so the code of polling sessions and mixing results
%% will need to be rewrited.
-spec do_get_events(id(), binary() | undefined, handler_context()) ->
{ok, response_data()} | {error, error_get_events()}.
do_get_events(ID, Token, HandlerContext) ->
do(fun() ->
PartyID = wapi_handler_utils:get_owner(HandlerContext),
SessionID = unwrap(request_session_id(ID, HandlerContext)),
DecodedToken = unwrap(continuation_token_unpack(Token, PartyID)),
PrevTransferCursor = continuation_token_cursor(p2p_transfer, DecodedToken),
PrevSessionCursor = continuation_token_cursor(p2p_session, DecodedToken),
{TransferEvents, TransferCursor} = unwrap(events_collect(
fistful_p2p_transfer,
ID,
events_range(PrevTransferCursor),
HandlerContext,
[]
)),
{SessionEvents, SessionCursor} = unwrap(events_collect(
fistful_p2p_session,
SessionID,
events_range(PrevSessionCursor),
HandlerContext,
[]
)),
NewTransferCursor = events_max(PrevTransferCursor, TransferCursor),
NewSessionCursor = events_max(PrevSessionCursor, SessionCursor),
NewToken = unwrap(continuation_token_pack(NewTransferCursor, NewSessionCursor, PartyID)),
Events = {NewToken, events_merge([TransferEvents, SessionEvents])},
unmarshal_events(Events)
end).
%% get p2p_transfer from backend and return last sesssion ID
-spec request_session_id(id(), handler_context()) ->
{ok, undefined | id ()} | {error, {p2p_transfer, notfound}}.
request_session_id(ID, HandlerContext) ->
Request = {fistful_p2p_transfer, 'Get', [ID, #'EventRange'{}]},
case service_call(Request, HandlerContext) of
{ok, #p2p_transfer_P2PTransferState{sessions = []}} ->
{ok, undefined};
{ok, #p2p_transfer_P2PTransferState{sessions = Sessions}} ->
Session = lists:last(Sessions),
{ok, Session#p2p_transfer_SessionState.id};
{exception, #fistful_P2PNotFound{}} ->
{error, {p2p_transfer, notfound}}
end.
%% create and code a new continuation token
continuation_token_pack(TransferCursor, SessionCursor, PartyID) ->
Token = genlib_map:compact(#{
<<"version">> => 1,
?CONTINUATION_TRANSFER => TransferCursor,
?CONTINUATION_SESSION => SessionCursor
}),
uac_authorizer_jwt:issue(wapi_utils:get_unique_id(), PartyID, Token, wapi_auth:get_signee()).
%% verify, decode and check version of continuation token
continuation_token_unpack(undefined, _PartyID) ->
{ok, #{}};
continuation_token_unpack(Token, PartyID) ->
case uac_authorizer_jwt:verify(Token, #{}) of
{ok, {_, PartyID, #{<<"version">> := 1} = VerifiedToken}} ->
{ok, VerifiedToken};
{ok, {_, PartyID, #{<<"version">> := Version}}} ->
{error, {token, {unsupported_version, Version}}};
{ok, {_, WrongPatryID, _}} when WrongPatryID /= PartyID ->
{error, {token, {not_verified, wrong_party_id}}};
{error, Error} ->
{error, {token, {not_verified, Error}}}
end.
%% get cursor event id by entity
continuation_token_cursor(p2p_transfer, DecodedToken) ->
maps:get(?CONTINUATION_TRANSFER, DecodedToken, undefined);
continuation_token_cursor(p2p_session, DecodedToken) ->
maps:get(?CONTINUATION_SESSION, DecodedToken, undefined).
%% collect events from EventService backend
-spec events_collect(event_service(), id() | undefined, event_range(), handler_context(), Acc0) ->
{ok, {Acc1, event_id()}} | {error, {p2p_transfer, notfound}} when
Acc0 :: [] | [event()],
Acc1 :: [] | [event()].
events_collect(fistful_p2p_session, undefined, #'EventRange'{'after' = Cursor}, _HandlerContext, Acc) ->
% no session ID is not an error
{ok, {Acc, Cursor}};
events_collect(_EventService, _EntityID, #'EventRange'{'after' = Cursor, 'limit' = Limit}, _HandlerContext, Acc)
when Limit =< 0 ->
% Limit < 0 < undefined
{ok, {Acc, Cursor}};
events_collect(EventService, EntityID, EventRange, HandlerContext, Acc) ->
#'EventRange'{'after' = Cursor, limit = Limit} = EventRange,
Request = {EventService, 'GetEvents', [EntityID, EventRange]},
case events_request(Request, HandlerContext) of
{ok, {_Received, [], undefined}} ->
% the service has not returned any events, the previous cursor must be kept
{ok, {Acc, Cursor}};
{ok, {Received, Events, NewCursor}} when Received < Limit ->
% service returned less events than requested
% or Limit is 'undefined' and service returned all events
{ok, {Acc ++ Events, NewCursor}};
{ok, {_Received, Events, NewCursor}} ->
% Limit is reached but some events can be filtered out
NewEventRange = events_range(NewCursor, Limit - length(Events)),
events_collect(EventService, EntityID, NewEventRange, HandlerContext, Acc ++ Events);
{error, _} = Error ->
Error
end.
-spec events_request(Request, handler_context()) ->
{ok, {integer(), [] | [event()], event_id()}} | {error, {p2p_transfer, notfound}} when
Request :: {event_service(), 'GetEvents', [id() | event_range()]}.
events_request(Request, HandlerContext) ->
case service_call(Request, HandlerContext) of
{ok, []} ->
{ok, {0, [], undefined}};
{ok, EventsThrift} ->
Cursor = events_cursor(lists:last(EventsThrift)),
Events = lists:filter(fun events_filter/1, EventsThrift),
{ok, {length(EventsThrift), Events, Cursor}};
{exception, #fistful_P2PNotFound{}} ->
{error, {p2p_transfer, notfound}};
{exception, #fistful_P2PSessionNotFound{}} ->
% P2PSessionNotFound not found - not error
{ok, {0, [], undefined}}
end.
events_filter(#p2p_transfer_Event{change = {status_changed, _}}) ->
true;
events_filter(#p2p_session_Event{change = {ui, #p2p_session_UserInteractionChange{payload = Payload}}}) ->
case Payload of
{status_changed, #p2p_session_UserInteractionStatusChange{
status = {pending, _}
}} ->
false;
_Other ->
% {created ...}
% {status_changed, ... status = {finished, ...}}
% take created & finished user interaction events
true
end;
events_filter(_Event) ->
false.
events_merge(EventsList) ->
lists:sort(fun(Ev1, Ev2) -> events_timestamp(Ev1) < events_timestamp(Ev2) end, lists:append(EventsList)).
events_cursor(#p2p_transfer_Event{event = ID}) ->
ID;
events_cursor(#p2p_session_Event{event = ID}) ->
ID.
events_timestamp(#p2p_transfer_Event{occured_at = OccuredAt}) ->
OccuredAt;
events_timestamp(#p2p_session_Event{occured_at = OccuredAt}) ->
OccuredAt.
events_range(CursorID) ->
events_range(CursorID, genlib_app:env(wapi, events_fetch_limit, ?DEFAULT_EVENTS_LIMIT)).
events_range(CursorID, Limit) ->
#'EventRange'{'after' = CursorID, 'limit' = Limit}.
events_max(NewEventID, OldEventID) when is_integer(NewEventID) andalso is_integer(OldEventID) ->
erlang:max(NewEventID, OldEventID);
events_max(NewEventID, OldEventID) ->
genlib:define(NewEventID, OldEventID).
%% Marshal
marshal_quote_params(#{
@ -224,23 +451,27 @@ marshal_transfer_params(#{
marshal_sender(#{
<<"token">> := Token,
<<"authData">> := AuthData,
<<"contactInfo">> := ContactInfo
}) ->
Resource = case wapi_crypto:decrypt_bankcard_token(Token) of
ResourceBankCard = case wapi_crypto:decrypt_bankcard_token(Token) of
unrecognized ->
BankCard = wapi_utils:base64url_to_map(Token),
{bank_card, #'ResourceBankCard'{
#'ResourceBankCard'{
bank_card = #'BankCard'{
token = maps:get(<<"token">>, BankCard),
bin = maps:get(<<"bin">>, BankCard),
masked_pan = maps:get(<<"lastDigits">>, BankCard)
}
}};
};
{ok, BankCard} ->
{bank_card, #'ResourceBankCard'{bank_card = BankCard}}
#'ResourceBankCard'{bank_card = BankCard}
end,
ResourceBankCardAuth = ResourceBankCard#'ResourceBankCard'{
auth_data = {session_data, #'SessionAuthData'{id = AuthData}}
},
{resource, #p2p_transfer_RawResource{
resource = Resource,
resource = {bank_card, ResourceBankCardAuth},
contact_info = marshal_contact_info(ContactInfo)
}}.
@ -377,6 +608,94 @@ unmarshal_transfer_status({failed, #p2p_status_Failed{failure = Failure}}) ->
<<"failure">> => unmarshal(failure, Failure)
}.
unmarshal_events({Token, Events}) ->
#{
<<"continuationToken">> => unmarshal(string, Token),
<<"result">> => [unmarshal_event(Ev) || Ev <- Events]
}.
unmarshal_event(#p2p_transfer_Event{
occured_at = OccuredAt,
change = Change
}) ->
#{
<<"createdAt">> => unmarshal(string, OccuredAt),
<<"change">> => unmarshal_event_change(Change)
};
unmarshal_event(#p2p_session_Event{
occured_at = OccuredAt,
change = Change
}) ->
#{
<<"createdAt">> => unmarshal(string, OccuredAt),
<<"change">> => unmarshal_event_change(Change)
}.
unmarshal_event_change({status_changed, #p2p_transfer_StatusChange{
status = Status
}}) ->
ChangeType = #{ <<"changeType">> => <<"P2PTransferStatusChanged">>},
TransferChange = unmarshal_transfer_status(Status),
maps:merge(ChangeType, TransferChange);
unmarshal_event_change({ui, #p2p_session_UserInteractionChange{
id = ID,
payload = Payload
}}) ->
#{
<<"changeType">> => <<"P2PTransferInteractionChanged">>,
<<"userInteractionID">> => unmarshal(id, ID),
<<"userInteractionChange">> => unmarshal_user_interaction_change(Payload)
}.
unmarshal_user_interaction_change({created, #p2p_session_UserInteractionCreatedChange{
ui = #p2p_session_UserInteraction{user_interaction = UserInteraction}
}}) ->
#{
<<"changeType">> => <<"UserInteractionCreated">>,
<<"userInteraction">> => unmarshal_user_interaction(UserInteraction)
};
unmarshal_user_interaction_change({status_changed, #p2p_session_UserInteractionStatusChange{
status = {finished, _} % other statuses are skipped
}}) ->
#{
<<"changeType">> => <<"UserInteractionFinished">>
}.
unmarshal_user_interaction({redirect, Redirect}) ->
#{
<<"interactionType">> => <<"Redirect">>,
<<"request">> => unmarshal_request(Redirect)
}.
unmarshal_request({get_request, #ui_BrowserGetRequest{
uri = URI
}}) ->
#{
<<"requestType">> => <<"BrowserGetRequest">>,
<<"uriTemplate">> => unmarshal(string, URI)
};
unmarshal_request({post_request, #ui_BrowserPostRequest{
uri = URI,
form = Form
}}) ->
#{
<<"requestType">> => <<"BrowserPostRequest">>,
<<"uriTemplate">> => unmarshal(string, URI),
<<"form">> => unmarshal_form(Form)
}.
unmarshal_form(Form) ->
maps:fold(
fun (Key, Template, AccIn) ->
FormField = #{
<<"key">> => unmarshal(string, Key),
<<"template">> => unmarshal(string, Template)
},
[FormField | AccIn]
end,
[], Form
).
unmarshal(T, V) ->
ff_codec:unmarshal(T, V).
@ -384,3 +703,262 @@ maybe_unmarshal(_T, undefined) ->
undefined;
maybe_unmarshal(T, V) ->
unmarshal(T, V).
%% TESTS
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-spec test() -> _.
-spec unmarshal_events_test_() ->
_.
unmarshal_events_test_() ->
Form = fun() -> {fun unmarshal_form/1,
#{ <<"arg1">> => <<"value1">>, <<"arg2">> => <<"value2">> },
[
#{ <<"key">> => <<"arg2">>, <<"template">> => <<"value2">>},
#{ <<"key">> => <<"arg1">>, <<"template">> => <<"value1">>}
]
} end,
Request = fun
({_, Woody, Swag}) -> {fun unmarshal_request/1,
{post_request, #ui_BrowserPostRequest{
uri = <<"uri://post">>,
form = Woody
}},
#{
<<"requestType">> => <<"BrowserPostRequest">>,
<<"uriTemplate">> => <<"uri://post">>,
<<"form">> => Swag
}
};
(get_request) -> {fun unmarshal_request/1,
{get_request, #ui_BrowserGetRequest{
uri = <<"uri://get">>
}},
#{
<<"requestType">> => <<"BrowserGetRequest">>,
<<"uriTemplate">> => <<"uri://get">>
}
}
end,
UIRedirect = fun({_, Woody, Swag}) -> {fun unmarshal_user_interaction/1,
{redirect, Woody},
#{
<<"interactionType">> => <<"Redirect">>,
<<"request">> => Swag
}
} end,
UIChangePayload = fun
({_, Woody, Swag}) -> {fun unmarshal_user_interaction_change/1,
{created, #p2p_session_UserInteractionCreatedChange{
ui = #p2p_session_UserInteraction{
id = <<"id://p2p_session/ui">>,
user_interaction = Woody
}
}},
#{
<<"changeType">> => <<"UserInteractionCreated">>,
<<"userInteraction">> => Swag
}
};
(ui_finished) -> {fun unmarshal_user_interaction_change/1,
{status_changed, #p2p_session_UserInteractionStatusChange{
status = {finished, #p2p_session_UserInteractionStatusFinished{}}
}},
#{
<<"changeType">> => <<"UserInteractionFinished">>
}
}
end,
EventChange = fun
({_, Woody, Swag}) -> {fun unmarshal_event_change/1,
{ui, #p2p_session_UserInteractionChange{
id = <<"id://p2p_session/change">>, payload = Woody
}},
#{
<<"changeType">> => <<"P2PTransferInteractionChanged">>,
<<"userInteractionID">> => <<"id://p2p_session/change">>,
<<"userInteractionChange">> => Swag
}
};
(TransferStatus) -> {fun unmarshal_event_change/1,
{status_changed, #p2p_transfer_StatusChange{
status = case TransferStatus of
pending -> {pending, #p2p_status_Pending{}};
succeeded -> {succeeded, #p2p_status_Succeeded{}}
end
}},
#{
<<"changeType">> => <<"P2PTransferStatusChanged">>,
<<"status">> => case TransferStatus of
pending -> <<"Pending">>;
succeeded -> <<"Succeeded">>
end
}
}
end,
Event = fun
({_, {ui, _} = Woody, Swag}) -> {fun unmarshal_event/1,
#p2p_session_Event{
event = 1,
occured_at = <<"2020-05-25T12:34:56.123456Z">>,
change = Woody
},
#{
<<"createdAt">> => <<"2020-05-25T12:34:56.123456Z">>,
<<"change">> => Swag
}
};
({_, {status_changed, _} = Woody, Swag}) -> {fun unmarshal_event/1,
#p2p_transfer_Event{
event = 1,
occured_at = <<"2020-05-25T12:34:56.123456Z">>,
change = Woody
},
#{
<<"createdAt">> => <<"2020-05-25T12:34:56.123456Z">>,
<<"change">> => Swag
}
}
end,
Events = fun(List) -> {fun unmarshal_events/1,
{
<<"token">>,
[Woody || {_, Woody, _} <- List]
},
#{
<<"continuationToken">> => <<"token">>,
<<"result">> => [Swag || {_, _, Swag} <- List]
}
} end,
EvList = [E ||
Type <- [Form(), get_request],
Change <- [UIChangePayload(UIRedirect(Request(Type))), pending, succeeded],
E <- [Event(EventChange(Change))]
],
[
?_assertEqual(ExpectedSwag, Unmarshal(Woody)) ||
{Unmarshal, Woody, ExpectedSwag} <- [Events(EvList) | EvList]
].
-spec events_collect_test_() ->
_.
events_collect_test_() ->
{setup,
fun() ->
% Construct acceptable event
Event = fun(EventID) -> #p2p_transfer_Event{
event = EventID,
occured_at = <<"2020-05-25T12:34:56.123456Z">>,
change = {status_changed, #p2p_transfer_StatusChange{
status = {succeeded, #p2p_status_Succeeded{}}
}}
} end,
% Construct rejectable event
Reject = fun(EventID) -> #p2p_transfer_Event{
event = EventID,
occured_at = <<"2020-05-25T12:34:56.123456Z">>,
change = {route, #p2p_transfer_RouteChange{}}
} end,
meck:new([wapi_handler_utils], [passthrough]),
%
% mock Request: {Service, 'GetEvents', [EntityID, EventRange]},
% use Service to select the desired 'GetEvents' result
%
meck:expect(wapi_handler_utils, service_call, fun
({produce_empty, 'GetEvents', _Params}, _Context) ->
{ok, []};
({produce_triple, 'GetEvents', _Params}, _Context) ->
{ok, [Event(N) || N <- lists:seq(1, 3)]};
({produce_even, 'GetEvents', [_, EventRange]}, _Context) ->
#'EventRange'{'after' = After, limit = Limit} = EventRange,
{ok, [Event(N) || N <- lists:seq(After + 1, After + Limit), N rem 2 =:= 0]};
({produce_reject, 'GetEvents', [_, EventRange]}, _Context) ->
#'EventRange'{'after' = After, limit = Limit} = EventRange,
{ok, [
case N rem 2 of
0 -> Reject(N);
_ -> Event(N)
end || N <- lists:seq(After + 1, After + Limit)
]};
({produce_range, 'GetEvents', [_, EventRange]}, _Context) ->
#'EventRange'{'after' = After, limit = Limit} = EventRange,
{ok, [Event(N) || N <- lists:seq(After + 1, After + Limit)]};
({transfer_not_found, 'GetEvents', _Params}, _Context) ->
{exception, #fistful_P2PNotFound{}};
({session_not_found, 'GetEvents', _Params}, _Context) ->
{exception, #fistful_P2PSessionNotFound{}}
end),
{
% Test generator - call 'events_collect' function and compare with 'Expected' result
fun _Collect(Service, EntityID, EventRange, Acc, Expected) ->
?_assertEqual(Expected, events_collect(Service, EntityID, EventRange, #{}, Acc))
end,
% Pass event constructor to test cases
Event
}
end,
fun(_) ->
meck:unload()
end,
fun({Collect, Event}) ->
[
% SessionID undefined is not an error
Collect(fistful_p2p_session, undefined, events_range(1), [Event(0)],
{ok, {[Event(0)], 1}}
),
% Limit < 0 < undefined
Collect(any, <<>>, events_range(1, 0), [],
{ok, {[], 1}}
),
% Limit < 0 < undefined
Collect(any, <<>>, events_range(1, 0), [Event(0)],
{ok, {[Event(0)], 1}}
),
% the service has not returned any events
Collect(produce_empty, <<>>, events_range(undefined), [],
{ok, {[], undefined}}
),
% the service has not returned any events
Collect(produce_empty, <<>>, events_range(0, 1), [],
{ok, {[], 0}}
),
% Limit is 'undefined' and service returned all events
Collect(produce_triple, <<>>, events_range(undefined), [Event(0)],
{ok, {[Event(0), Event(1), Event(2), Event(3)], 3}}
),
% or service returned less events than requested
Collect(produce_even, <<>>, events_range(0, 4), [],
{ok, {[Event(2), Event(4)], 4}}
),
% Limit is reached but some events can be filtered out
Collect(produce_reject, <<>>, events_range(0, 4), [],
{ok, {[Event(1), Event(3), Event(5), Event(7)], 7}}
),
% Accumulate
Collect(produce_range, <<>>, events_range(1, 2), [Event(0)],
{ok, {[Event(0), Event(2), Event(3)], 3}}
),
% transfer not found
Collect(transfer_not_found, <<>>, events_range(1), [],
{error, {p2p_transfer, notfound}}
),
% P2PSessionNotFound not found - not error
Collect(session_not_found, <<>>, events_range(1), [],
{ok, {[], 1}}
)
]
end
}.
-endif.

View File

@ -39,7 +39,7 @@ create_transfer(Params = #{<<"sender">> := SenderID}, HandlerContext) ->
create_transfer(ID, Params, Context, HandlerContext) ->
TransferParams = marshal(transfer_params, Params#{<<"id">> => ID}),
Request = {w2w_transfer, 'Create', [TransferParams, marshal(context, Context)]},
Request = {fistful_w2w_transfer, 'Create', [TransferParams, marshal(context, Context)]},
case service_call(Request, HandlerContext) of
{ok, Transfer} ->
{ok, unmarshal(transfer, Transfer)};
@ -64,7 +64,7 @@ when
get_transfer(ID, HandlerContext) ->
EventRange = #'EventRange'{},
Request = {w2w_transfer, 'Get', [ID, EventRange]},
Request = {fistful_w2w_transfer, 'Get', [ID, EventRange]},
case service_call(Request, HandlerContext) of
{ok, TransferThrift} ->
case wapi_access_backend:check_resource(w2w_transfer, TransferThrift, HandlerContext) of

View File

@ -599,6 +599,28 @@ process_request('GetP2PTransfer', #{p2pTransferID := ID}, Context, _Opts) ->
wapi_handler_utils:reply_ok(404)
end;
process_request('GetP2PTransferEvents', #{p2pTransferID := ID, continuationToken := CT}, Context, _Opts) ->
case wapi_p2p_transfer_backend:get_transfer_events(ID, CT, Context) of
{ok, P2PTransferEvents} ->
wapi_handler_utils:reply_ok(200, P2PTransferEvents);
{error, {p2p_transfer, unauthorized}} ->
wapi_handler_utils:reply_ok(404);
{error, {p2p_transfer, notfound}} ->
wapi_handler_utils:reply_ok(404);
{error, {token, {not_verified, _}}} ->
wapi_handler_utils:reply_error(400, #{
<<"errorType">> => <<"InvalidToken">>,
<<"name">> => <<"continuationToken">>,
<<"description">> => <<"Token can't be verified">>
});
{error, {token, {unsupported_version, _}}} ->
wapi_handler_utils:reply_error(400, #{
<<"errorType">> => <<"InvalidToken">>,
<<"name">> => <<"continuationToken">>,
<<"description">> => <<"Token unsupported version">>
})
end;
%% Webhooks
process_request('CreateWebhook', #{'WebhookParams' := WebhookParams}, Context, _Opts) ->

View File

@ -134,12 +134,31 @@ end_per_group(_, _) ->
init_per_testcase(Name, C) ->
C1 = ct_helper:makeup_cfg([ct_helper:test_case_name(Name), ct_helper:woody_ctx()], C),
ok = ct_helper:set_context(C1),
C1.
case Name of
woody_retry_test ->
Save = application:get_env(wapi_woody_client, service_urls, undefined),
ok = application:set_env(
wapi_woody_client,
service_urls,
Save#{fistful_stat => "http://spanish.inquision/fistful_stat"}
),
lists:keystore(service_urls, 1, C1, {service_urls, Save});
_Other ->
C1
end.
-spec end_per_testcase(test_case_name(), config()) -> _.
end_per_testcase(_Name, _C) ->
ok = ct_helper:unset_context().
end_per_testcase(_Name, C) ->
ok = ct_helper:unset_context(),
case lists:keysearch(service_urls, 1, C) of
{value, {_, undefined}} ->
application:unset_env(wapi_woody_client, service_urls);
{value, {_, Save}} ->
application:set_env(wapi_woody_client, service_urls, Save);
_ ->
ok
end.
-define(ID_PROVIDER, <<"good-one">>).
-define(ID_PROVIDER2, <<"good-two">>).
@ -538,12 +557,6 @@ quote_withdrawal_test(C) ->
ok = check_withdrawal(WalletID, DestID, WithdrawalID, C).
woody_retry_test(C) ->
Urls = application:get_env(wapi_woody_client, service_urls, #{}),
ok = application:set_env(
wapi_woody_client,
service_urls,
Urls#{fistful_stat => "http://spanish.inquision/fistful_stat"}
),
Params = #{
identityID => <<"12332">>,
currencyID => <<"RUB">>,
@ -562,8 +575,7 @@ woody_retry_test(C) ->
end,
T2 = erlang:monotonic_time(),
Time = erlang:convert_time_unit(T2 - T1, native, micro_seconds),
?assert(Time > 3000000),
ok = application:set_env(wapi_woody_client, service_urls, Urls).
?assert(Time > 3000000).
-spec get_wallet_by_external_id(config()) ->
test_return().

View File

@ -1,5 +1,6 @@
-module(wapi_p2p_transfer_tests_SUITE).
-include_lib("stdlib/include/assert.hrl").
-include_lib("common_test/include/ct.hrl").
-include_lib("stdlib/include/assert.hrl").
@ -9,6 +10,8 @@
-include_lib("wapi_wallet_dummy_data.hrl").
-include_lib("fistful_proto/include/ff_proto_p2p_transfer_thrift.hrl").
-include_lib("fistful_proto/include/ff_proto_p2p_session_thrift.hrl").
-export([all/0]).
-export([groups/0]).
@ -38,7 +41,9 @@
get_quote_fail_operation_not_permitted_test/1,
get_quote_fail_no_resource_info_test/1,
get_ok_test/1,
get_fail_p2p_notfound_test/1
get_fail_p2p_notfound_test/1,
get_events_ok/1,
get_events_fail/1
]).
% common-api is used since it is the domain used in production RN
@ -87,7 +92,9 @@ groups() ->
get_quote_fail_operation_not_permitted_test,
get_quote_fail_no_resource_info_test,
get_ok_test,
get_fail_p2p_notfound_test
get_fail_p2p_notfound_test,
get_events_ok,
get_events_fail
]
}
].
@ -159,6 +166,7 @@ end_per_testcase(_Name, C) ->
wapi_ct_helper:stop_mocked_service_sup(?config(test_sup, C)),
ok.
%%% Tests
-spec create_ok_test(config()) ->
@ -278,8 +286,10 @@ create_with_quote_token_ok_test(C) ->
{bender_thrift, fun('GenerateID', _) -> {ok, ?GENERATE_ID_RESULT} end},
{fistful_identity, fun('Get', _) -> {ok, ?IDENTITY(PartyID)};
('GetContext', _) -> {ok, ?DEFAULT_CONTEXT(PartyID)} end},
{p2p_transfer, fun('GetQuote', _) -> {ok, ?P2P_TRANSFER_QUOTE(IdentityID)};
('Create', _) -> {ok, ?P2P_TRANSFER(PartyID)} end}
{fistful_p2p_transfer, fun
('GetQuote', _) -> {ok, ?P2P_TRANSFER_QUOTE(IdentityID)};
('Create', _) -> {ok, ?P2P_TRANSFER(PartyID)}
end}
], C),
SenderToken = store_bank_card(C, <<"4150399999000900">>, <<"12/2025">>, <<"Buka Bjaka">>),
ReceiverToken = store_bank_card(C, <<"4150399999000900">>, <<"12/2025">>, <<"Buka Bjaka">>),
@ -450,6 +460,42 @@ get_fail_p2p_notfound_test(C) ->
get_call_api(C)
).
-spec get_events_ok(config()) ->
_.
get_events_ok(C) ->
PartyID = ?config(party, C),
wapi_ct_helper:mock_services([
{fistful_p2p_transfer, fun
('GetContext', _) -> {ok, ?DEFAULT_CONTEXT(PartyID)};
('Get', _) -> {ok, ?P2P_TRANSFER_SESSIONS(PartyID)};
('GetEvents', [_ID, #'EventRange'{limit = Limit}]) ->
{ok, [?P2P_TRANSFER_EVENT(EventID) || EventID <- lists:seq(1, Limit)]}
end},
{fistful_p2p_session, fun('GetEvents', [_ID, #'EventRange'{limit = Limit}]) ->
{ok, [?P2P_SESSION_EVENT(EventID) || EventID <- lists:seq(1, Limit)]}
end}
], C),
{ok, #{<<"result">> := Result}} = get_events_call_api(C),
% Limit is multiplied by two because the selection occurs twice - from session and transfer.
{ok, Limit} = application:get_env(wapi, events_fetch_limit),
?assertEqual(Limit * 2, erlang:length(Result)),
[?assertMatch(#{<<"change">> := _}, Ev) || Ev <- Result].
-spec get_events_fail(config()) ->
_.
get_events_fail(C) ->
PartyID = ?config(party, C),
wapi_ct_helper:mock_services([
{fistful_p2p_transfer, fun
('GetContext', _) -> {ok, ?DEFAULT_CONTEXT(PartyID)};
('Get', _) -> throw(#fistful_P2PNotFound{})
end}
], C),
?assertMatch({error, {404, #{}}}, get_events_call_api(C)).
%%
create_party(_C) ->
@ -464,6 +510,17 @@ call_api(F, Params, Context) ->
Response = F(Url, PreparedParams, Opts),
wapi_client_lib:handle_response(Response).
get_events_call_api(C) ->
call_api(
fun swag_client_wallet_p2_p_api:get_p2_p_transfer_events/3,
#{
binding => #{
<<"p2pTransferID">> => ?STRING
}
},
ct_helper:cfg(context, C)
).
create_p2p_transfer_call_api(C) ->
SenderToken = store_bank_card(C, <<"4150399999000900">>, <<"12/2025">>, <<"Buka Bjaka">>),
ReceiverToken = store_bank_card(C, <<"4150399999000900">>, <<"12/2025">>, <<"Buka Bjaka">>),
@ -539,7 +596,7 @@ create_ok_start_mocks(C, ContextPartyID) ->
{bender_thrift, fun('GenerateID', _) -> {ok, ?GENERATE_ID_RESULT} end},
{fistful_identity, fun('Get', _) -> {ok, ?IDENTITY(PartyID)};
('GetContext', _) -> {ok, ?DEFAULT_CONTEXT(ContextPartyID)} end},
{p2p_transfer, fun('Create', _) -> {ok, ?P2P_TRANSFER(PartyID)} end}
{fistful_p2p_transfer, fun('Create', _) -> {ok, ?P2P_TRANSFER(PartyID)} end}
], C).
create_fail_start_mocks(C, CreateResultFun) ->
@ -551,7 +608,7 @@ create_fail_start_mocks(C, ContextPartyID, CreateResultFun) ->
{bender_thrift, fun('GenerateID', _) -> {ok, ?GENERATE_ID_RESULT} end},
{fistful_identity, fun('Get', _) -> {ok, ?IDENTITY(PartyID)};
('GetContext', _) -> {ok, ?DEFAULT_CONTEXT(ContextPartyID)} end},
{p2p_transfer, fun('Create', _) -> CreateResultFun() end}
{fistful_p2p_transfer, fun('Create', _) -> CreateResultFun() end}
], C).
get_quote_start_mocks(C, GetQuoteResultFun) ->
@ -559,12 +616,12 @@ get_quote_start_mocks(C, GetQuoteResultFun) ->
wapi_ct_helper:mock_services([
{fistful_identity, fun('Get', _) -> {ok, ?IDENTITY(PartyID)};
('GetContext', _) -> {ok, ?DEFAULT_CONTEXT(PartyID)} end},
{p2p_transfer, fun('GetQuote', _) -> GetQuoteResultFun() end}
{fistful_p2p_transfer, fun('GetQuote', _) -> GetQuoteResultFun() end}
], C).
get_start_mocks(C, GetResultFun) ->
wapi_ct_helper:mock_services([
{p2p_transfer, fun('Get', _) -> GetResultFun() end}
{fistful_p2p_transfer, fun('Get', _) -> GetResultFun() end}
], C).
store_bank_card(C, Pan, ExpDate, CardHolder) ->

View File

@ -32,8 +32,6 @@
-type group_name() :: ct_helper:group_name().
-type test_return() :: _ | no_return().
% -import(ct_helper, [cfg/2]).
-spec all() -> [test_case_name() | {group, group_name()}].
all() ->
@ -60,10 +58,9 @@ groups() ->
-spec init_per_suite(config()) -> config().
init_per_suite(C) ->
ct_helper:makeup_cfg([
ct_helper:makeup_cfg([
ct_helper:test_case_name(init),
ct_payment_system:setup(#{
default_termset => get_default_termset(),
optional_apps => [
bender_client,
wapi_woody_client,
@ -198,15 +195,21 @@ w2w_transfer_check_test(C) ->
p2p_transfer_check_test(C) ->
Name = <<"Keyn Fawkes">>,
Provider = ?ID_PROVIDER,
Provider = <<"quote-owner">>,
Class = ?ID_CLASS,
IdentityID = create_identity(Name, Provider, Class, C),
Token = store_bank_card(C, <<"4150399999000900">>, <<"12/2025">>, <<"Buka Bjaka">>),
P2PTransferID = create_p2p_transfer(Token, Token, IdentityID, C),
ok = await_p2p_transfer(P2PTransferID, C),
P2PTransfer = get_p2p_transfer(P2PTransferID, C),
P2PTransferEvents = get_p2p_transfer_events(P2PTransferID, C),
ok = application:set_env(wapi, transport, thrift),
P2PTransferIDThrift = create_p2p_transfer(Token, Token, IdentityID, C),
IdentityIDThrift = IdentityID,
P2PTransferIDThrift = create_p2p_transfer(Token, Token, IdentityIDThrift, C),
ok = await_p2p_transfer(P2PTransferIDThrift, C),
P2PTransferThrift = get_p2p_transfer(P2PTransferIDThrift, C),
P2PTransferEventsThrift = get_p2p_transfer_events(P2PTransferIDThrift, C),
?assertEqual(maps:keys(P2PTransferEvents), maps:keys(P2PTransferEventsThrift)),
?assertEqual(maps:keys(P2PTransfer), maps:keys(P2PTransferThrift)),
?assertEqual(maps:without([<<"id">>, <<"createdAt">>], P2PTransfer),
maps:without([<<"id">>, <<"createdAt">>], P2PTransferThrift)).
@ -233,34 +236,31 @@ withdrawal_check_test(C) ->
p2p_template_check_test(C) ->
Name = <<"Keyn Fawkes">>,
Provider = ?ID_PROVIDER,
Provider = <<"quote-owner">>,
Class = ?ID_CLASS,
Metadata = #{ <<"some key">> => <<"some value">> },
ok = application:set_env(wapi, transport, thrift),
IdentityID = create_identity(Name, Provider, Class, C),
P2PTemplate = create_p2p_template(IdentityID, C),
P2PTemplate = create_p2p_template(IdentityID, Metadata, C),
#{<<"id">> := P2PTemplateID} = P2PTemplate,
P2PTemplateCopy = get_p2p_template(P2PTemplateID, C),
?assertEqual(maps:keys(P2PTemplate), maps:keys(P2PTemplateCopy)),
ValidUntil = woody_deadline:to_binary(woody_deadline:from_timeout(100000)),
TemplateToken = get_p2p_template_token(P2PTemplateID, ValidUntil, C),
TemplateTicket = get_p2p_template_ticket(P2PTemplateID, TemplateToken, ValidUntil, C),
{ok, #{<<"token">> := QuoteToken}} = call_p2p_template_quote(P2PTemplateID, C),
{ok, P2PTransfer} = call_p2p_template_transfer(P2PTemplateID, TemplateTicket, QuoteToken, C),
?assertEqual(maps:get(<<"identityID">>, P2PTransfer), IdentityID),
% TODO: #{<<"metadata">> := Metadata} = P2PTransfer,
?assertMatch(#{<<"identityID">> := IdentityID}, P2PTransfer),
#{<<"id">> := P2PTransferID} = P2PTransfer,
ok = await_p2p_transfer(P2PTransferID, C),
?assertMatch(#{<<"metadata">> := Metadata}, P2PTransfer),
ok = block_p2p_template(P2PTemplateID, C),
P2PTemplateBlocked = get_p2p_template(P2PTemplateID, C),
?assertEqual(maps:get(<<"isBlocked">>, P2PTemplateBlocked), true),
?assertMatch(#{<<"isBlocked">> := true}, P2PTemplateBlocked),
QuoteBlockedError = call_p2p_template_quote(P2PTemplateID, C),
?assertMatch({error, {422, _}}, QuoteBlockedError),
P2PTransferBlockedError = call_p2p_template_transfer(P2PTemplateID, TemplateTicket, QuoteToken, C),
?assertMatch({error, {422, _}}, P2PTransferBlockedError),
Quote404Error = call_p2p_template_quote(<<"404">>, C),
?assertMatch({error, {404, _}}, Quote404Error).
@ -545,6 +545,25 @@ get_p2p_transfer(P2PTransferID, C) ->
),
P2PTransfer.
get_p2p_transfer_events(P2PTransferID, C) ->
{ok, P2PTransferEvents} = call_api(
fun swag_client_wallet_p2_p_api:get_p2_p_transfer_events/3,
#{binding => #{<<"p2pTransferID">> => P2PTransferID}},
ct_helper:cfg(context, C)
),
P2PTransferEvents.
await_p2p_transfer(P2PTransferID, C) ->
<<"Succeeded">> = ct_helper:await(
<<"Succeeded">>,
fun () ->
Reply = get_p2p_transfer(P2PTransferID, C),
#{<<"status">> := #{<<"status">> := Status}} = Reply,
Status
end
),
ok.
await_destination(DestID) ->
authorized = ct_helper:await(
authorized,
@ -612,7 +631,7 @@ get_withdrawal(WithdrawalID, C) ->
%% P2PTemplate
create_p2p_template(IdentityID, C) ->
create_p2p_template(IdentityID, Metadata, C) ->
{ok, P2PTemplate} = call_api(
fun swag_client_wallet_p2_p_templates_api:create_p2_p_transfer_template/3,
#{
@ -626,9 +645,7 @@ create_p2p_template(IdentityID, C) ->
}
},
<<"metadata">> => #{
<<"defaultMetadata">> => #{
<<"some key">> => <<"some value">>
}
<<"defaultMetadata">> => Metadata
}
}
}
@ -661,7 +678,6 @@ block_p2p_template(P2PTemplateID, C) ->
),
ok.
get_p2p_template_token(P2PTemplateID, ValidUntil, C) ->
{ok, #{<<"token">> := Token}} = call_api(
fun swag_client_wallet_p2_p_templates_api:issue_p2_p_transfer_template_access_token/3,
@ -694,8 +710,7 @@ get_p2p_template_ticket(P2PTemplateID, TemplateToken, ValidUntil, C) ->
Ticket.
call_p2p_template_quote(P2PTemplateID, C) ->
SenderToken = store_bank_card(C, <<"4150399999000900">>, <<"12/2025">>, <<"Buka Bjaka">>),
ReceiverToken = store_bank_card(C, <<"4150399999000900">>, <<"12/2025">>, <<"Buka Bjaka">>),
Token = store_bank_card(C, <<"4150399999000900">>, <<"12/2025">>, <<"Buka Bjaka">>),
call_api(
fun swag_client_wallet_p2_p_templates_api:quote_p2_p_transfer_with_template/3,
#{
@ -705,11 +720,11 @@ call_p2p_template_quote(P2PTemplateID, C) ->
body => #{
<<"sender">> => #{
<<"type">> => <<"BankCardSenderResource">>,
<<"token">> => SenderToken
<<"token">> => Token
},
<<"receiver">> => #{
<<"type">> => <<"BankCardReceiverResource">>,
<<"token">> => ReceiverToken
<<"token">> => Token
},
<<"body">> => #{
<<"amount">> => ?INTEGER,
@ -721,8 +736,7 @@ call_p2p_template_quote(P2PTemplateID, C) ->
).
call_p2p_template_transfer(P2PTemplateID, TemplateTicket, QuoteToken, C) ->
SenderToken = store_bank_card(C, <<"4150399999000900">>, <<"12/2025">>, <<"Buka Bjaka">>),
ReceiverToken = store_bank_card(C, <<"4150399999000900">>, <<"12/2025">>, <<"Buka Bjaka">>),
Token = store_bank_card(C, <<"4150399999000900">>, <<"12/2025">>, <<"Buka Bjaka">>),
Context = maps:merge(ct_helper:cfg(context, C), #{token => TemplateTicket}),
call_api(
fun swag_client_wallet_p2_p_templates_api:create_p2_p_transfer_with_template/3,
@ -733,15 +747,15 @@ call_p2p_template_transfer(P2PTemplateID, TemplateTicket, QuoteToken, C) ->
body => #{
<<"sender">> => #{
<<"type">> => <<"BankCardSenderResourceParams">>,
<<"token">> => SenderToken,
<<"token">> => Token,
<<"authData">> => <<"session id">>
},
<<"receiver">> => #{
<<"type">> => <<"BankCardReceiverResourceParams">>,
<<"token">> => ReceiverToken
<<"token">> => Token
},
<<"body">> => #{
<<"amount">> => 101,
<<"amount">> => ?INTEGER,
<<"currency">> => ?RUB
},
<<"contactInfo">> => #{
@ -753,124 +767,3 @@ call_p2p_template_transfer(P2PTemplateID, TemplateTicket, QuoteToken, C) ->
},
Context
).
%%
-include_lib("ff_cth/include/ct_domain.hrl").
get_default_termset() ->
#domain_TermSet{
wallets = #domain_WalletServiceTerms{
currencies = {value, ?ordset([?cur(<<"RUB">>)])},
wallet_limit = {decisions, [
#domain_CashLimitDecision{
if_ = {condition, {currency_is, ?cur(<<"RUB">>)}},
then_ = {value, ?cashrng(
{inclusive, ?cash(-10000000, <<"RUB">>)},
{exclusive, ?cash( 10000001, <<"RUB">>)}
)}
}
]},
withdrawals = #domain_WithdrawalServiceTerms{
currencies = {value, ?ordset([?cur(<<"RUB">>)])},
cash_limit = {decisions, [
#domain_CashLimitDecision{
if_ = {condition, {currency_is, ?cur(<<"RUB">>)}},
then_ = {value, ?cashrng(
{inclusive, ?cash( 0, <<"RUB">>)},
{exclusive, ?cash(10000000, <<"RUB">>)}
)}
}
]},
cash_flow = {decisions, [
#domain_CashFlowDecision{
if_ = {condition, {currency_is, ?cur(<<"RUB">>)}},
then_ = {value, [
?cfpost(
{wallet, sender_settlement},
{wallet, receiver_destination},
?share(1, 1, operation_amount)
),
?cfpost(
{wallet, receiver_destination},
{system, settlement},
?share(10, 100, operation_amount)
)
]}
}
]}
},
p2p = #domain_P2PServiceTerms{
currencies = {value, ?ordset([?cur(<<"RUB">>), ?cur(<<"USD">>)])},
allow = {constant, true},
cash_limit = {decisions, [
#domain_CashLimitDecision{
if_ = {condition, {currency_is, ?cur(<<"RUB">>)}},
then_ = {value, ?cashrng(
{inclusive, ?cash( 0, <<"RUB">>)},
{exclusive, ?cash(10001, <<"RUB">>)}
)}
}
]},
cash_flow = {decisions, [
#domain_CashFlowDecision{
if_ = {condition, {currency_is, ?cur(<<"RUB">>)}},
then_ = {value, [
?cfpost(
{wallet, sender_settlement},
{wallet, receiver_settlement},
?share(1, 1, operation_amount)
)
]}
}
]},
fees = {decisions, [
#domain_FeeDecision{
if_ = {condition, {currency_is, ?cur(<<"RUB">>)}},
then_ = {value, #domain_Fees{
fees = #{surplus => ?share(1, 1, operation_amount)}
}}
}
]},
quote_lifetime = {value, {interval, #domain_LifetimeInterval{
days = 1, minutes = 1, seconds = 1
}}},
templates = #domain_P2PTemplateServiceTerms{
allow = {condition, {currency_is, ?cur(<<"RUB">>)}}
}
},
w2w = #domain_W2WServiceTerms{
currencies = {value, ?ordset([?cur(<<"RUB">>), ?cur(<<"USD">>)])},
allow = {constant, true},
cash_limit = {decisions, [
#domain_CashLimitDecision{
if_ = {condition, {currency_is, ?cur(<<"RUB">>)}},
then_ = {value, ?cashrng(
{inclusive, ?cash(0, <<"RUB">>)},
{exclusive, ?cash(10001, <<"RUB">>)}
)}
}
]},
cash_flow = {decisions, [
#domain_CashFlowDecision{
if_ = {condition, {currency_is, ?cur(<<"RUB">>)}},
then_ = {value, [
?cfpost(
{wallet, sender_settlement},
{wallet, receiver_settlement},
?share(1, 1, operation_amount)
)
]}
}
]},
fees = {decisions, [
#domain_FeeDecision{
if_ = {condition, {currency_is, ?cur(<<"RUB">>)}},
then_ = {value, #domain_Fees{
fees = #{surplus => ?share(1, 1, operation_amount)}
}}
}
]}
}
}
}.

View File

@ -155,7 +155,7 @@ create_fail_unauthorized_wallet_test(C) ->
wapi_ct_helper:mock_services([
{bender_thrift, fun('GenerateID', _) -> {ok, ?GENERATE_ID_RESULT} end},
{fistful_wallet, fun('GetContext', _) -> {ok, ?DEFAULT_CONTEXT(<<"someotherparty">>)} end},
{w2w_transfer, fun('Create', _) -> {ok, ?W2W_TRANSFER(PartyID)} end}
{fistful_w2w_transfer, fun('Create', _) -> {ok, ?W2W_TRANSFER(PartyID)} end}
], C),
?assertEqual(
{error, {422, #{<<"message">> => <<"No such wallet sender">>}}},
@ -295,11 +295,11 @@ create_w2_w_transfer_start_mocks(C, CreateResultFun) ->
wapi_ct_helper:mock_services([
{bender_thrift, fun('GenerateID', _) -> {ok, ?GENERATE_ID_RESULT} end},
{fistful_wallet, fun('GetContext', _) -> {ok, ?DEFAULT_CONTEXT(PartyID)} end},
{w2w_transfer, fun('Create', _) -> CreateResultFun() end}
{fistful_w2w_transfer, fun('Create', _) -> CreateResultFun() end}
], C).
get_w2_w_transfer_start_mocks(C, GetResultFun) ->
wapi_ct_helper:mock_services([
{w2w_transfer, fun('Get', _) -> GetResultFun() end}
{fistful_w2w_transfer, fun('Get', _) -> GetResultFun() end}
], C).

View File

@ -519,6 +519,34 @@
adjustments = []
}).
-define(P2P_TRANSFER_SESSIONS(PartyID), ?P2P_TRANSFER(PartyID)#p2p_transfer_P2PTransferState{
sessions = [#p2p_transfer_SessionState{id = ?STRING}]
}).
-define(P2P_TRANSFER_EVENT(EventID), #p2p_transfer_Event{
event = EventID,
occured_at = ?TIMESTAMP,
change = {status_changed, #p2p_transfer_StatusChange{
status = {succeeded, #p2p_status_Succeeded{}}
}}
}).
-define(P2P_SESSION_EVENT(EventID), #p2p_session_Event{
event = EventID,
occured_at = ?TIMESTAMP,
change = {ui, #p2p_session_UserInteractionChange{
id = ?STRING,
payload = {created, #p2p_session_UserInteractionCreatedChange{
ui = #p2p_session_UserInteraction{
id = ?STRING,
user_interaction = {redirect, {get_request, #ui_BrowserGetRequest{
uri = ?STRING
}}}
}
}}
}}
}).
-define(FEES, #'Fees'{fees = #{operation_amount => ?CASH}}).
-define(P2P_TRANSFER_QUOTE(IdentityID), #p2p_transfer_Quote{
@ -532,3 +560,4 @@
receiver = ?RESOURCE_BANK_CARD,
fees = ?FEES
}).

View File

@ -96,9 +96,11 @@ get_service_modname(fistful_p2p_template) ->
{ff_proto_p2p_template_thrift, 'Management'};
get_service_modname(webhook_manager) ->
{ff_proto_webhooker_thrift, 'WebhookManager'};
get_service_modname(p2p_transfer) ->
get_service_modname(fistful_p2p_transfer) ->
{ff_proto_p2p_transfer_thrift, 'Management'};
get_service_modname(w2w_transfer) ->
get_service_modname(fistful_p2p_session) ->
{ff_proto_p2p_session_thrift, 'Management'};
get_service_modname(fistful_w2w_transfer) ->
{ff_proto_w2w_transfer_thrift, 'Management'}.
-spec get_service_deadline(service_name()) -> undefined | woody_deadline:deadline().

View File

@ -154,10 +154,18 @@
{wapi_woody_client, [
{service_urls, #{
webhook_manager => "http://hooker:8022/hook",
cds_storage => "http://cds:8022/v1/storage",
identdoc_storage => "http://cds:8022/v1/identity_document_storage",
fistful_stat => "http://fistful-magista:8022/stat"
webhook_manager => "http://hooker:8022/hook",
cds_storage => "http://cds:8022/v1/storage",
identdoc_storage => "http://cds:8022/v1/identity_document_storage",
fistful_stat => "http://fistful-magista:8022/stat",
fistful_wallet => "http://fistful:8022/v1/wallet",
fistful_identity => "http://fistful:8022/v1/identity",
fistful_destination => "http://fistful:8022/v1/destination",
fistful_withdrawal => "http://fistful:8022/v1/withdrawal",
fistful_w2w_transfer => "http://fistful:8022/v1/w2w_transfer",
fistful_p2p_template => "http://fistful:8022/v1/p2p_template",
fistful_p2p_transfer => "http://fistful:8022/v1/p2p_transfer",
fistful_p2p_session => "http://fistful:8022/v1/p2p_transfer/session"
}},
{api_deadlines, #{
wallet => 5000 % millisec

View File

@ -189,6 +189,11 @@
]},
{test, [
{deps, [
{meck,
"0.9.0"
}
]},
{cover_enabled, true},
{cover_excl_apps, [
ff_cth,

View File

@ -1,4 +1,4 @@
{"1.2.0",
{"1.1.0",
[{<<"accept">>,{pkg,<<"accept">>,<<"0.3.5">>},2},
{<<"base64url">>,{pkg,<<"base64url">>,<<"0.0.1">>},0},
{<<"bear">>,{pkg,<<"bear">>,<<"0.8.7">>},3},
@ -68,7 +68,7 @@
0},
{<<"fistful_proto">>,
{git,"git@github.com:rbkmoney/fistful-proto.git",
{ref,"f373e09fc2e451b9ef3b5f86f54f9627fa29c59f"}},
{ref,"87b13d386969047c9c16d310754f1f18733b36ab"}},
0},
{<<"fistful_reporter_proto">>,
{git,"git@github.com:rbkmoney/fistful-reporter-proto.git",
@ -204,28 +204,5 @@
{<<"ranch">>, <<"6B1FAB51B49196860B733A49C07604465A47BDB78AA10C1C16A3D199F7F8C881">>},
{<<"ssl_verify_fun">>, <<"F0EAFFF810D2041E93F915EF59899C923F4568F4585904D010387ED74988E77B">>},
{<<"unicode_util_compat">>, <<"D869E4C68901DD9531385BB0C8C40444EBF624E60B6962D95952775CAC5E90CD">>},
{<<"uuid">>, <<"C5DF97D1A3D626235C2415E74053C47B2138BB863C5CD802AB5CAECB8ECC019F">>}]},
{pkg_hash_ext,[
{<<"accept">>, <<"11B18C220BCC2EAB63B5470C038EF10EB6783BCB1FCDB11AA4137DEFA5AC1BB8">>},
{<<"base64url">>, <<"FAB09B20E3F5DB886725544CBCF875B8E73EC93363954EB8A1A9ED834AA8C1F9">>},
{<<"bear">>, <<"534217DCE6A719D59E54FB0EB7A367900DBFC5F85757E8C1F94269DF383F6D9B">>},
{<<"cache">>, <<"3E7D6706DE5DF76C4D71C895B4BE62B01C3DE6EDB63197035E465C3BCE63F19B">>},
{<<"certifi">>, <<"805ABD97539CAF89EC6D4732C91E62BA9DA0CDA51AC462380BBD28EE697A8C42">>},
{<<"cowboy">>, <<"04FD8C6A39EDC6AAA9C26123009200FC61F92A3A94F3178C527B70B767C6E605">>},
{<<"cowlib">>, <<"79F954A7021B302186A950A32869DBC185523D99D3E44CE430CD1F3289F41ED4">>},
{<<"gproc">>, <<"580ADAFA56463B75263EF5A5DF4C86AF321F68694E7786CB057FD805D1E2A7DE">>},
{<<"hackney">>, <<"C2790C9F0F7205F4A362512192DEE8179097394400E745E4D20BAB7226A8EAAD">>},
{<<"idna">>, <<"4BDD305EB64E18B0273864920695CB18D7A2021F31A11B9C5FBCD9A253F936E2">>},
{<<"jose">>, <<"3C7DDC8A9394B92891DB7C2771DA94BF819834A1A4C92E30857B7D582E2F8257">>},
{<<"jsx">>, <<"B4C5D3230B397C8D95579E4A3D72826BB6463160130CCF4182F5BE8579B5F44C">>},
{<<"metrics">>, <<"69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16">>},
{<<"mimerl">>, <<"F278585650AA581986264638EBF698F8BB19DF297F66AD91B18910DFC6E19323">>},
{<<"prometheus">>, <<"4905FD2992F8038ECCD7AA0CD22F40637ED618C0BED1F75C05AACEC15B7545DE">>},
{<<"prometheus_cowboy">>, <<"BA286BECA9302618418892D37BCD5DC669A6CC001F4EB6D6AF85FF81F3F4F34C">>},
{<<"prometheus_httpd">>, <<"0BBE831452CFDF9588538EB2F570B26F30C348ADAE5E95A7D87F35A5910BCF92">>},
{<<"quickrand">>, <<"E05EE94A9DA317B4B7D9C453638E592D002FE8F2109A0357B0A54F966EDBBA90">>},
{<<"ranch">>, <<"451D8527787DF716D99DC36162FCA05934915DB0B6141BBDAC2EA8D3C7AFC7D7">>},
{<<"ssl_verify_fun">>, <<"603561DC0FD62F4F2EA9B890F4E20E1A0D388746D6E20557CAFB1B16950DE88C">>},
{<<"unicode_util_compat">>, <<"1D1848C40487CDB0B30E8ED975E34E025860C02E419CB615D255849F3427439D">>},
{<<"uuid">>, <<"F87BAD1A8E90373B75DAEE259A6EB880293AB178AE2B2779ACB0B00CEA81C602">>}]}
{<<"uuid">>, <<"C5DF97D1A3D626235C2415E74053C47B2138BB863C5CD802AB5CAECB8ECC019F">>}]}
].