mirror of
https://github.com/valitydev/fistful-server.git
synced 2024-11-06 10:45:21 +00:00
FF-218: p2p transfer via thrift (#301)
* update wapi access backend * add p2p transfer service * add p2p transfer dummy * update test data * fix typo * add p2p transfer thrift handling * add p2p transfer thrift test * minor fix * copy error handling * add fixme * type fixes * macro refactor * rename p2p transfer module, update errors * update handler * type fixes * update dummy date * add p2p transfer tests * DRY * update marshalling * remove redundant test permissions * rename errors * update tests * fix quote type * add quote tests to p2p transfer * add token errors to thrift handler * add p2p transfer quote handling * fix p2p transfer marshalling bug * import do/unwrap * add quote to test * fix merge * fix contract not found bug * fix thrift test * refactor p2p transfer backend * fix whitespace * add p2p quote dummy data * update create p2p transfer tests, add quote test * add p2p quote handler * update p2p quote * add p2p quote to backend * use thrift in wapi p2p quote * rework tests to use thrift quote * rework ff backend to use thrift quote * rework p2p transfer to use thrift quote * fix codec * minor fixes * update ct payment system
This commit is contained in:
parent
1766e90261
commit
ebdd7e9a25
@ -130,6 +130,7 @@ start_app(wapi_woody_client = AppName) ->
|
||||
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"
|
||||
}},
|
||||
{service_retries, #{
|
||||
|
@ -26,8 +26,8 @@ unmarshal_p2p_quote_params(#p2p_transfer_QuoteParams{
|
||||
#{
|
||||
identity_id => unmarshal(id, IdentityID),
|
||||
body => unmarshal(cash, Body),
|
||||
sender => unmarshal(participant, Sender),
|
||||
receiver => unmarshal(participant, Receiver)
|
||||
sender => unmarshal(resource, Sender),
|
||||
receiver => unmarshal(resource, Receiver)
|
||||
}.
|
||||
|
||||
-spec marshal_p2p_transfer_state(p2p_transfer:p2p_transfer_state(), ff_entity_context:context()) ->
|
||||
@ -49,7 +49,7 @@ marshal_p2p_transfer_state(P2PTransferState, Ctx) ->
|
||||
operation_timestamp = marshal(timestamp_ms, p2p_transfer:operation_timestamp(P2PTransferState)),
|
||||
created_at = marshal(timestamp_ms, p2p_transfer:created_at(P2PTransferState)),
|
||||
deadline = maybe_marshal(timestamp_ms, p2p_transfer:deadline(P2PTransferState)),
|
||||
quote = maybe_marshal(quote, p2p_transfer:quote(P2PTransferState)),
|
||||
quote = maybe_marshal(quote_state, p2p_transfer:quote(P2PTransferState)),
|
||||
client_info = maybe_marshal(client_info, p2p_transfer:client_info(P2PTransferState)),
|
||||
external_id = maybe_marshal(id, p2p_transfer:external_id(P2PTransferState)),
|
||||
metadata = marshal(ctx, p2p_transfer:metadata(P2PTransferState)),
|
||||
|
@ -280,7 +280,7 @@ get_contract_terms(PartyID, ContractID, Varset, Timestamp, PartyRevision, Domain
|
||||
{error, #payproc_PartyNotFound{}} ->
|
||||
{error, {party_not_found, PartyID}};
|
||||
{error, #payproc_ContractNotFound{}} ->
|
||||
{error, {contract_not_found, PartyID}};
|
||||
{error, {contract_not_found, ContractID}};
|
||||
{error, #payproc_PartyNotExistsYet{}} ->
|
||||
{error, {party_not_exists_yet, PartyID}};
|
||||
{error, Unexpected} ->
|
||||
|
@ -5,18 +5,27 @@
|
||||
-include_lib("fistful_proto/include/ff_proto_destination_thrift.hrl").
|
||||
-include_lib("fistful_proto/include/ff_proto_p2p_template_thrift.hrl").
|
||||
-include_lib("fistful_proto/include/ff_proto_w2w_transfer_thrift.hrl").
|
||||
-include_lib("fistful_proto/include/ff_proto_p2p_transfer_thrift.hrl").
|
||||
-include_lib("fistful_proto/include/ff_proto_withdrawal_thrift.hrl").
|
||||
|
||||
-export([check_resource/3]).
|
||||
-export([check_resource_by_id/3]).
|
||||
|
||||
-type id() :: binary().
|
||||
-type resource_type() :: identity | wallet | destination | p2p_template | w2w_transfer | withdrawal.
|
||||
-type resource_type() :: identity
|
||||
| wallet
|
||||
| destination
|
||||
| withdrawal
|
||||
| w2w_transfer
|
||||
| p2p_transfer
|
||||
| p2p_template
|
||||
.
|
||||
|
||||
-type handler_context() :: wapi_handler:context().
|
||||
-type data() ::
|
||||
ff_proto_identity_thrift:'IdentityState'() |
|
||||
ff_proto_wallet_thrift:'WalletState'() |
|
||||
ff_proto_p2p_transfer_thrift:'P2PTransferState'() |
|
||||
ff_proto_p2p_template_thrift:'P2PTemplateState'().
|
||||
|
||||
-define(CTX_NS, <<"com.rbkmoney.wapi">>).
|
||||
@ -86,6 +95,14 @@ get_context_by_id(w2w_transfer, W2WTransferID, WoodyCtx) ->
|
||||
{exception, #fistful_W2WNotFound{}} ->
|
||||
{error, notfound}
|
||||
end;
|
||||
get_context_by_id(p2p_transfer, P2PTransferID, WoodyCtx) ->
|
||||
Request = {p2p_transfer, 'GetContext', [P2PTransferID]},
|
||||
case wapi_handler_utils:service_call(Request, WoodyCtx) of
|
||||
{ok, Context} ->
|
||||
Context;
|
||||
{exception, #fistful_P2PNotFound{}} ->
|
||||
{error, notfound}
|
||||
end;
|
||||
get_context_by_id(withdrawal, WithdrawalID, WoodyCtx) ->
|
||||
Request = {fistful_withdrawal, 'GetContext', [WithdrawalID]},
|
||||
case wapi_handler_utils:service_call(Request, WoodyCtx) of
|
||||
@ -105,6 +122,8 @@ get_context_from_state(p2p_template, #p2p_template_P2PTemplateState{context = Co
|
||||
Context;
|
||||
get_context_from_state(w2w_transfer, #w2w_transfer_W2WTransferState{context = Context}) ->
|
||||
Context;
|
||||
get_context_from_state(p2p_transfer, #p2p_transfer_P2PTransferState{context = Context}) ->
|
||||
Context;
|
||||
get_context_from_state(withdrawal, #wthd_WithdrawalState{context = Context}) ->
|
||||
Context.
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
-module(wapi_p2p_quote).
|
||||
|
||||
-include_lib("fistful_proto/include/ff_proto_p2p_transfer_thrift.hrl").
|
||||
|
||||
-export([create_token_payload/2]).
|
||||
-export([decode_token_payload/1]).
|
||||
|
||||
@ -15,17 +17,20 @@
|
||||
%% Internal types
|
||||
|
||||
-type party_id() :: binary().
|
||||
-type quote() :: p2p_quote:quote().
|
||||
-type quote() :: ff_proto_p2p_transfer_thrift:'Quote'().
|
||||
|
||||
%% API
|
||||
|
||||
-spec create_token_payload(quote(), party_id()) ->
|
||||
token_payload().
|
||||
create_token_payload(Quote, PartyID) ->
|
||||
Type = {struct, struct, {ff_proto_p2p_transfer_thrift, 'Quote'}},
|
||||
Bin = ff_proto_utils:serialize(Type, Quote),
|
||||
EncodedQuote = base64:encode(Bin),
|
||||
genlib_map:compact(#{
|
||||
<<"version">> => 2,
|
||||
<<"partyID">> => PartyID,
|
||||
<<"quote">> => encode_quote(Quote)
|
||||
<<"quote">> => EncodedQuote
|
||||
}).
|
||||
|
||||
-spec decode_token_payload(token_payload()) ->
|
||||
@ -36,53 +41,46 @@ decode_token_payload(#{<<"version">> := 2} = Payload) ->
|
||||
<<"partyID">> := _PartyID,
|
||||
<<"quote">> := EncodedQuote
|
||||
} = Payload,
|
||||
Quote = decode_quote(EncodedQuote),
|
||||
Type = {struct, struct, {ff_proto_p2p_transfer_thrift, 'Quote'}},
|
||||
Bin = base64:decode(EncodedQuote),
|
||||
Quote = ff_proto_utils:deserialize(Type, Bin),
|
||||
{ok, Quote};
|
||||
decode_token_payload(#{<<"version">> := 1}) ->
|
||||
{error, token_expired}.
|
||||
|
||||
%% Internals
|
||||
|
||||
-spec encode_quote(quote()) ->
|
||||
token_payload().
|
||||
encode_quote(Quote) ->
|
||||
Type = {struct, struct, {ff_proto_p2p_transfer_thrift, 'Quote'}},
|
||||
Bin = ff_proto_utils:serialize(Type, ff_p2p_transfer_codec:marshal(quote, Quote)),
|
||||
base64:encode(Bin).
|
||||
|
||||
-spec decode_quote(token_payload()) ->
|
||||
quote().
|
||||
decode_quote(Encoded) ->
|
||||
Type = {struct, struct, {ff_proto_p2p_transfer_thrift, 'Quote'}},
|
||||
Bin = base64:decode(Encoded),
|
||||
Thrift = ff_proto_utils:deserialize(Type, Bin),
|
||||
ff_p2p_transfer_codec:unmarshal(quote, Thrift).
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-spec test() -> _.
|
||||
|
||||
-spec payload_symmetry_test() -> _.
|
||||
payload_symmetry_test() ->
|
||||
Quote = #{
|
||||
fees => #{
|
||||
fees => #{
|
||||
surplus => {1000, <<"RUB">>}
|
||||
Quote = #p2p_transfer_Quote{
|
||||
identity_id = <<"identity">>,
|
||||
created_at = <<"1970-01-01T00:00:00.123Z">>,
|
||||
expires_on = <<"1970-01-01T00:00:00.321Z">>,
|
||||
party_revision = 1,
|
||||
domain_revision = 2,
|
||||
fees = #'Fees'{fees = #{surplus => #'Cash'{
|
||||
amount = 1000000,
|
||||
currency = #'CurrencyRef'{
|
||||
symbolic_code = <<"RUB">>
|
||||
}
|
||||
}}},
|
||||
body = #'Cash'{
|
||||
amount = 1000000,
|
||||
currency = #'CurrencyRef'{
|
||||
symbolic_code = <<"RUB">>
|
||||
}
|
||||
},
|
||||
amount => {1000000, <<"RUB">>},
|
||||
party_revision => 1,
|
||||
domain_revision => 2,
|
||||
created_at => 123,
|
||||
expires_on => 321,
|
||||
identity_id => <<"identity">>,
|
||||
sender => {bank_card, #{
|
||||
token => <<"very long token">>,
|
||||
bin_data_id => nil
|
||||
sender = {bank_card, #'ResourceBankCard'{
|
||||
bank_card = #'BankCard'{token = <<"very long token">>},
|
||||
auth_data = {session_data, #'SessionAuthData'{id = <<"1">>}}
|
||||
}},
|
||||
receiver => {bank_card, #{
|
||||
token => <<"another very long token">>,
|
||||
bin_data_id => #{[nil] => [nil]}
|
||||
receiver = {bank_card, #'ResourceBankCard'{
|
||||
bank_card = #'BankCard'{token = <<"another very long token">>},
|
||||
auth_data = {session_data, #'SessionAuthData'{id = <<"2">>}}
|
||||
}}
|
||||
},
|
||||
Payload = create_token_payload(Quote, <<"party">>),
|
||||
@ -91,25 +89,35 @@ payload_symmetry_test() ->
|
||||
|
||||
-spec payload_v2_decoding_test() -> _.
|
||||
payload_v2_decoding_test() ->
|
||||
ExpectedQuote = #{
|
||||
fees => #{
|
||||
fees => #{
|
||||
surplus => {1000, <<"RUB">>}
|
||||
ExpectedQuote = #p2p_transfer_Quote{
|
||||
identity_id = <<"identity">>,
|
||||
created_at = <<"1970-01-01T00:00:00.123Z">>,
|
||||
expires_on = <<"1970-01-01T00:00:00.321Z">>,
|
||||
party_revision = 1,
|
||||
domain_revision = 2,
|
||||
fees = #'Fees'{fees = #{surplus => #'Cash'{
|
||||
amount = 1000,
|
||||
currency = #'CurrencyRef'{
|
||||
symbolic_code = <<"RUB">>
|
||||
}
|
||||
}}},
|
||||
body = #'Cash'{
|
||||
amount = 1000000,
|
||||
currency = #'CurrencyRef'{
|
||||
symbolic_code = <<"RUB">>
|
||||
}
|
||||
},
|
||||
amount => {1000000, <<"RUB">>},
|
||||
party_revision => 1,
|
||||
domain_revision => 2,
|
||||
created_at => 123,
|
||||
expires_on => 321,
|
||||
identity_id => <<"identity">>,
|
||||
sender => {bank_card, #{
|
||||
token => <<"very long token">>,
|
||||
bin_data_id => nil
|
||||
sender = {bank_card, #'ResourceBankCard'{
|
||||
bank_card = #'BankCard'{
|
||||
token = <<"very long token">>,
|
||||
bin_data_id = {nl, #msgp_Nil{}}
|
||||
}
|
||||
}},
|
||||
receiver => {bank_card, #{
|
||||
token => <<"another very long token">>,
|
||||
bin_data_id => #{[nil] => [nil]}
|
||||
receiver = {bank_card, #'ResourceBankCard'{
|
||||
bank_card = #'BankCard'{
|
||||
token = <<"another very long token">>,
|
||||
bin_data_id = {obj, #{{arr, [{nl, #msgp_Nil{}}]} => {arr, [{nl, #msgp_Nil{}}]}}}
|
||||
}
|
||||
}}
|
||||
},
|
||||
Payload = #{
|
||||
|
384
apps/wapi/src/wapi_p2p_transfer_backend.erl
Normal file
384
apps/wapi/src/wapi_p2p_transfer_backend.erl
Normal file
@ -0,0 +1,384 @@
|
||||
-module(wapi_p2p_transfer_backend).
|
||||
|
||||
-type req_data() :: wapi_handler:req_data().
|
||||
-type handler_context() :: wapi_handler:context().
|
||||
-type response_data() :: wapi_handler:response_data().
|
||||
|
||||
-type id() :: binary().
|
||||
-type external_id() :: id().
|
||||
|
||||
-type error_create()
|
||||
:: {external_id_conflict, id(), external_id()}
|
||||
| {identity, unauthorized}
|
||||
| {identity, notfound}
|
||||
| {p2p_transfer, forbidden_currency}
|
||||
| {p2p_transfer, cash_range_exceeded}
|
||||
| {p2p_transfer, operation_not_permitted}
|
||||
| {token, {not_verified, identity_mismatch}}
|
||||
| {token, {not_verified, _}}
|
||||
| {sender, invalid_resource}
|
||||
| {receiver, invalid_resource}
|
||||
.
|
||||
|
||||
-type error_create_quote()
|
||||
:: {identity, unauthorized}
|
||||
| {identity, notfound}
|
||||
| {p2p_transfer, forbidden_currency}
|
||||
| {p2p_transfer, cash_range_exceeded}
|
||||
| {p2p_transfer, operation_not_permitted}
|
||||
| {sender, invalid_resource}
|
||||
| {receiver, invalid_resource}
|
||||
.
|
||||
|
||||
-type error_get()
|
||||
:: {p2p_transfer, unauthorized}
|
||||
| {p2p_transfer, notfound}
|
||||
.
|
||||
|
||||
-export([create_transfer/2]).
|
||||
-export([quote_transfer/2]).
|
||||
-export([get_transfer/2]).
|
||||
|
||||
-import(ff_pipeline, [do/1, unwrap/1]).
|
||||
|
||||
-include_lib("fistful_proto/include/ff_proto_p2p_transfer_thrift.hrl").
|
||||
|
||||
-spec create_transfer(req_data(), handler_context()) ->
|
||||
{ok, response_data()} | {error, error_create()}.
|
||||
create_transfer(Params = #{<<"identityID">> := IdentityID}, HandlerContext) ->
|
||||
case wapi_access_backend:check_resource_by_id(identity, IdentityID, HandlerContext) of
|
||||
ok ->
|
||||
case wapi_backend_utils:gen_id(p2p_transfer, Params, HandlerContext) of
|
||||
{ok, ID} ->
|
||||
do_create_transfer(ID, Params, HandlerContext);
|
||||
{error, {external_id_conflict, ID}} ->
|
||||
{error, {external_id_conflict, ID, maps:get(<<"externalID">>, Params, undefined)}}
|
||||
end;
|
||||
{error, unauthorized} ->
|
||||
{error, {identity, unauthorized}};
|
||||
{error, notfound} ->
|
||||
{error, {identity, notfound}}
|
||||
end.
|
||||
|
||||
-spec get_transfer(req_data(), handler_context()) ->
|
||||
{ok, response_data()} | {error, error_get()}.
|
||||
get_transfer(ID, HandlerContext) ->
|
||||
Request = {p2p_transfer, 'Get', [ID, #'EventRange'{}]},
|
||||
case service_call(Request, HandlerContext) of
|
||||
{ok, TransferThrift} ->
|
||||
case wapi_access_backend:check_resource(p2p_transfer, TransferThrift, HandlerContext) of
|
||||
ok ->
|
||||
{ok, unmarshal_transfer(TransferThrift)};
|
||||
{error, unauthorized} ->
|
||||
{error, {p2p_transfer, unauthorized}}
|
||||
end;
|
||||
{exception, #fistful_P2PNotFound{}} ->
|
||||
{error, {p2p_transfer, notfound}}
|
||||
end.
|
||||
|
||||
-spec quote_transfer(req_data(), handler_context()) ->
|
||||
{ok, response_data()} | {error, error_create_quote()}.
|
||||
|
||||
quote_transfer(Params = #{<<"identityID">> := IdentityID}, HandlerContext) ->
|
||||
case wapi_access_backend:check_resource_by_id(identity, IdentityID, HandlerContext) of
|
||||
ok ->
|
||||
do_quote_transfer(Params, HandlerContext);
|
||||
{error, unauthorized} ->
|
||||
{error, {identity, unauthorized}};
|
||||
{error, notfound} ->
|
||||
{error, {identity, notfound}}
|
||||
end.
|
||||
|
||||
%% Internal
|
||||
|
||||
do_quote_transfer(Params, HandlerContext) ->
|
||||
Request = {p2p_transfer, 'GetQuote', [marshal_quote_params(Params)]},
|
||||
case service_call(Request, HandlerContext) of
|
||||
{ok, Quote} ->
|
||||
PartyID = wapi_handler_utils:get_owner(HandlerContext),
|
||||
Token = create_quote_token(Quote, PartyID),
|
||||
UnmarshaledQuote = unmarshal_quote(Quote),
|
||||
{ok, UnmarshaledQuote#{<<"token">> => Token}};
|
||||
{exception, #p2p_transfer_NoResourceInfo{type = sender}} ->
|
||||
{error, {sender, invalid_resource}};
|
||||
{exception, #p2p_transfer_NoResourceInfo{type = receiver}} ->
|
||||
{error, {receiver, invalid_resource}};
|
||||
{exception, #fistful_ForbiddenOperationCurrency{}} ->
|
||||
{error, {p2p_transfer, forbidden_currency}};
|
||||
{exception, #fistful_ForbiddenOperationAmount{}} ->
|
||||
{error, {p2p_transfer, cash_range_exceeded}};
|
||||
{exception, #fistful_IdentityNotFound{ }} ->
|
||||
{error, {identity, notfound}}
|
||||
end.
|
||||
|
||||
create_quote_token(Quote, PartyID) ->
|
||||
Payload = wapi_p2p_quote:create_token_payload(Quote, PartyID),
|
||||
{ok, Token} = issue_quote_token(PartyID, Payload),
|
||||
Token.
|
||||
|
||||
issue_quote_token(PartyID, Payload) ->
|
||||
uac_authorizer_jwt:issue(wapi_utils:get_unique_id(), PartyID, Payload, wapi_auth:get_signee()).
|
||||
|
||||
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)]},
|
||||
unwrap(process_p2p_transfer_call(Request, HandlerContext))
|
||||
end).
|
||||
|
||||
process_p2p_transfer_call(Request, HandlerContext) ->
|
||||
case service_call(Request, HandlerContext) of
|
||||
{ok, Transfer} ->
|
||||
{ok, unmarshal_transfer(Transfer)};
|
||||
{exception, #p2p_transfer_NoResourceInfo{type = sender}} ->
|
||||
{error, {sender, invalid_resource}};
|
||||
{exception, #p2p_transfer_NoResourceInfo{type = receiver}} ->
|
||||
{error, {receiver, invalid_resource}};
|
||||
{exception, #fistful_ForbiddenOperationCurrency{}} ->
|
||||
{error, {p2p_transfer, forbidden_currency}};
|
||||
{exception, #fistful_ForbiddenOperationAmount{}} ->
|
||||
{error, {p2p_transfer, cash_range_exceeded}};
|
||||
{exception, #fistful_OperationNotPermitted{}} ->
|
||||
{error, {p2p_transfer, operation_not_permitted}};
|
||||
{exception, #fistful_IdentityNotFound{ }} ->
|
||||
{error, {identity, notfound}}
|
||||
end.
|
||||
|
||||
build_transfer_params(Params = #{<<"quoteToken">> := QuoteToken, <<"identityID">> := IdentityID}) ->
|
||||
do(fun() ->
|
||||
VerifiedToken = unwrap(verify_p2p_quote_token(QuoteToken)),
|
||||
Quote = unwrap(wapi_p2p_quote:decode_token_payload(VerifiedToken)),
|
||||
ok = unwrap(authorize_p2p_quote_token(Quote, IdentityID)),
|
||||
TransferParams = marshal_transfer_params(Params),
|
||||
TransferParams#p2p_transfer_P2PTransferParams{quote = Quote}
|
||||
end);
|
||||
build_transfer_params(Params) ->
|
||||
do(fun() -> marshal_transfer_params(Params) end).
|
||||
|
||||
verify_p2p_quote_token(Token) ->
|
||||
case uac_authorizer_jwt:verify(Token, #{}) of
|
||||
{ok, {_, _, VerifiedToken}} ->
|
||||
{ok, VerifiedToken};
|
||||
{error, Error} ->
|
||||
{error, {token, {not_verified, Error}}}
|
||||
end.
|
||||
|
||||
authorize_p2p_quote_token(#p2p_transfer_Quote{identity_id = IdentityID}, IdentityID) ->
|
||||
ok;
|
||||
authorize_p2p_quote_token(_Quote, _IdentityID) ->
|
||||
{error, {token, {not_verified, identity_mismatch}}}.
|
||||
|
||||
service_call(Params, HandlerContext) ->
|
||||
wapi_handler_utils:service_call(Params, HandlerContext).
|
||||
|
||||
%% Marshal
|
||||
|
||||
marshal_quote_params(#{
|
||||
<<"body">> := Body,
|
||||
<<"identityID">> := IdentityID,
|
||||
<<"sender">> := Sender,
|
||||
<<"receiver">> := Receiver
|
||||
}) ->
|
||||
#p2p_transfer_QuoteParams{
|
||||
body = marshal_body(Body),
|
||||
identity_id = IdentityID,
|
||||
sender = marshal_quote_participant(Sender),
|
||||
receiver = marshal_quote_participant(Receiver)
|
||||
}.
|
||||
|
||||
marshal_quote_participant(#{
|
||||
<<"token">> := Token
|
||||
}) ->
|
||||
case wapi_crypto:decrypt_bankcard_token(Token) of
|
||||
unrecognized ->
|
||||
BankCard = wapi_utils:base64url_to_map(Token),
|
||||
{bank_card, #'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}}
|
||||
end.
|
||||
|
||||
marshal_transfer_params(#{
|
||||
<<"id">> := ID,
|
||||
<<"identityID">> := IdentityID,
|
||||
<<"sender">> := Sender,
|
||||
<<"receiver">> := Receiver,
|
||||
<<"body">> := Body,
|
||||
<<"contactInfo">> := ContactInfo
|
||||
}) ->
|
||||
#p2p_transfer_P2PTransferParams{
|
||||
id = ID,
|
||||
identity_id = IdentityID,
|
||||
sender = marshal_sender(Sender#{<<"contactInfo">> => ContactInfo}),
|
||||
receiver = marshal_receiver(Receiver),
|
||||
body = marshal_body(Body)
|
||||
}.
|
||||
|
||||
marshal_sender(#{
|
||||
<<"token">> := Token,
|
||||
<<"contactInfo">> := ContactInfo
|
||||
}) ->
|
||||
Resource = case wapi_crypto:decrypt_bankcard_token(Token) of
|
||||
unrecognized ->
|
||||
BankCard = wapi_utils:base64url_to_map(Token),
|
||||
{bank_card, #'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}}
|
||||
end,
|
||||
{resource, #p2p_transfer_RawResource{
|
||||
resource = Resource,
|
||||
contact_info = marshal_contact_info(ContactInfo)
|
||||
}}.
|
||||
|
||||
marshal_receiver(#{
|
||||
<<"token">> := Token
|
||||
}) ->
|
||||
Resource = case wapi_crypto:decrypt_bankcard_token(Token) of
|
||||
unrecognized ->
|
||||
BankCard = wapi_utils:base64url_to_map(Token),
|
||||
{bank_card, #'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}}
|
||||
end,
|
||||
{resource, #p2p_transfer_RawResource{
|
||||
resource = Resource,
|
||||
contact_info = #'ContactInfo'{}
|
||||
}}.
|
||||
|
||||
marshal_contact_info(ContactInfo) ->
|
||||
#'ContactInfo'{
|
||||
email = maps:get(<<"email">>, ContactInfo, undefined),
|
||||
phone_number = maps:get(<<"phoneNumber">>, ContactInfo, undefined)
|
||||
}.
|
||||
|
||||
marshal_body(#{
|
||||
<<"amount">> := Amount,
|
||||
<<"currency">> := Currency
|
||||
}) ->
|
||||
#'Cash'{
|
||||
amount = Amount,
|
||||
currency = marshal_currency(Currency)
|
||||
}.
|
||||
|
||||
marshal_currency(Currency) ->
|
||||
#'CurrencyRef'{symbolic_code = Currency}.
|
||||
|
||||
marshal(T, V) ->
|
||||
ff_codec:marshal(T, V).
|
||||
|
||||
%% Unmarshal
|
||||
|
||||
unmarshal_quote(#p2p_transfer_Quote{
|
||||
fees = Fees,
|
||||
expires_on = ExpiresOn
|
||||
}) ->
|
||||
genlib_map:compact(#{
|
||||
<<"expiresOn">> => ExpiresOn,
|
||||
<<"customerFee">> => unmarshal_fees(Fees)
|
||||
}).
|
||||
|
||||
unmarshal_fees(#'Fees'{fees = #{operation_amount := Cash}}) ->
|
||||
unmarshal_body(Cash).
|
||||
|
||||
unmarshal_transfer(#p2p_transfer_P2PTransferState{
|
||||
id = ID,
|
||||
owner = IdentityID,
|
||||
sender = SenderResource,
|
||||
receiver = ReceiverResource,
|
||||
body = Body,
|
||||
created_at = CreatedAt,
|
||||
status = Status,
|
||||
external_id = ExternalID
|
||||
}) ->
|
||||
Sender = unmarshal_sender(SenderResource),
|
||||
ContactInfo = maps:get(<<"contactInfo">>, Sender),
|
||||
genlib_map:compact(#{
|
||||
<<"id">> => ID,
|
||||
<<"identityID">> => IdentityID,
|
||||
<<"contactInfo">> => ContactInfo,
|
||||
<<"createdAt">> => CreatedAt,
|
||||
<<"body">> => unmarshal_body(Body),
|
||||
<<"sender">> => maps:remove(<<"contactInfo">>, Sender),
|
||||
<<"receiver">> => unmarshal_receiver(ReceiverResource),
|
||||
<<"status">> => unmarshal_transfer_status(Status),
|
||||
<<"externalID">> => maybe_unmarshal(id, ExternalID)
|
||||
}).
|
||||
|
||||
unmarshal_body(#'Cash'{
|
||||
amount = Amount,
|
||||
currency = Currency
|
||||
}) ->
|
||||
#{
|
||||
<<"amount">> => unmarshal(amount, Amount),
|
||||
<<"currency">> => unmarshal(currency_ref, Currency)
|
||||
}.
|
||||
|
||||
unmarshal_sender({resource, #p2p_transfer_RawResource{
|
||||
contact_info = ContactInfo,
|
||||
resource = {bank_card, #'ResourceBankCard'{
|
||||
bank_card = BankCard
|
||||
}}
|
||||
}}) ->
|
||||
genlib_map:compact(#{
|
||||
<<"type">> => <<"BankCardSenderResource">>,
|
||||
<<"contactInfo">> => unmarshal_contact_info(ContactInfo),
|
||||
<<"token">> => BankCard#'BankCard'.token,
|
||||
<<"paymentSystem">> => genlib:to_binary(BankCard#'BankCard'.payment_system),
|
||||
<<"bin">> => BankCard#'BankCard'.bin,
|
||||
<<"lastDigits">> => wapi_utils:get_last_pan_digits(BankCard#'BankCard'.masked_pan)
|
||||
}).
|
||||
|
||||
unmarshal_receiver({resource, #p2p_transfer_RawResource{
|
||||
resource = {bank_card, #'ResourceBankCard'{
|
||||
bank_card = BankCard
|
||||
}}
|
||||
}}) ->
|
||||
genlib_map:compact(#{
|
||||
<<"type">> => <<"BankCardReceiverResource">>,
|
||||
<<"token">> => BankCard#'BankCard'.token,
|
||||
<<"bin">> => BankCard#'BankCard'.bin,
|
||||
<<"paymentSystem">> => genlib:to_binary(BankCard#'BankCard'.payment_system),
|
||||
<<"lastDigits">> => wapi_utils:get_last_pan_digits(BankCard#'BankCard'.masked_pan)
|
||||
}).
|
||||
|
||||
unmarshal_contact_info(ContactInfo) ->
|
||||
genlib_map:compact(#{
|
||||
<<"phoneNumber">> => ContactInfo#'ContactInfo'.phone_number,
|
||||
<<"email">> => ContactInfo#'ContactInfo'.email
|
||||
}).
|
||||
|
||||
unmarshal_transfer_status({pending, _}) ->
|
||||
#{<<"status">> => <<"Pending">>};
|
||||
unmarshal_transfer_status({succeeded, _}) ->
|
||||
#{<<"status">> => <<"Succeeded">>};
|
||||
unmarshal_transfer_status({failed, #p2p_status_Failed{failure = Failure}}) ->
|
||||
#{
|
||||
<<"status">> => <<"Failed">>,
|
||||
<<"failure">> => unmarshal(failure, Failure)
|
||||
}.
|
||||
|
||||
unmarshal(T, V) ->
|
||||
ff_codec:unmarshal(T, V).
|
||||
|
||||
maybe_unmarshal(_T, undefined) ->
|
||||
undefined;
|
||||
maybe_unmarshal(T, V) ->
|
||||
unmarshal(T, V).
|
@ -1017,7 +1017,7 @@ create_quote_token(Quote, WalletID, DestinationID, PartyID) ->
|
||||
Token.
|
||||
|
||||
create_p2p_quote_token(Quote, PartyID) ->
|
||||
Payload = wapi_p2p_quote:create_token_payload(Quote, PartyID),
|
||||
Payload = wapi_p2p_quote:create_token_payload(ff_p2p_transfer_codec:marshal(quote, Quote), PartyID),
|
||||
{ok, Token} = issue_quote_token(PartyID, Payload),
|
||||
Token.
|
||||
|
||||
@ -1043,10 +1043,11 @@ maybe_add_p2p_template_quote_token(ID, #{quote_token := QuoteToken} = Params) ->
|
||||
do(fun() ->
|
||||
VerifiedToken = unwrap(verify_p2p_quote_token(QuoteToken)),
|
||||
Quote = unwrap(quote, wapi_p2p_quote:decode_token_payload(VerifiedToken)),
|
||||
UnmarshaledQuote = ff_p2p_transfer_codec:unmarshal(quote, Quote),
|
||||
Machine = unwrap(p2p_template_machine:get(ID)),
|
||||
State = p2p_template_machine:p2p_template(Machine),
|
||||
ok = unwrap(authorize_p2p_quote_token(Quote, p2p_template:identity_id(State))),
|
||||
Params#{quote => Quote}
|
||||
ok = unwrap(authorize_p2p_quote_token(UnmarshaledQuote, p2p_template:identity_id(State))),
|
||||
Params#{quote => UnmarshaledQuote}
|
||||
end).
|
||||
|
||||
maybe_add_p2p_quote_token(#{quote_token := undefined} = Params) ->
|
||||
@ -1055,8 +1056,9 @@ maybe_add_p2p_quote_token(#{quote_token := QuoteToken, identity_id := IdentityID
|
||||
do(fun() ->
|
||||
VerifiedToken = unwrap(verify_p2p_quote_token(QuoteToken)),
|
||||
Quote = unwrap(quote, wapi_p2p_quote:decode_token_payload(VerifiedToken)),
|
||||
ok = unwrap(authorize_p2p_quote_token(Quote, IdentityID)),
|
||||
Params#{quote => Quote}
|
||||
UnmarshaledQuote = ff_p2p_transfer_codec:unmarshal(quote, Quote),
|
||||
ok = unwrap(authorize_p2p_quote_token(UnmarshaledQuote, IdentityID)),
|
||||
Params#{quote => UnmarshaledQuote}
|
||||
end).
|
||||
|
||||
max_event_id(NewEventID, OldEventID) when is_integer(NewEventID) andalso is_integer(OldEventID) ->
|
||||
|
@ -469,6 +469,75 @@ process_request('GetW2WTransfer', #{w2wTransferID := ID}, Context, _Opts) ->
|
||||
wapi_handler_utils:reply_ok(404)
|
||||
end;
|
||||
|
||||
%% P2P
|
||||
|
||||
process_request('QuoteP2PTransfer', #{'QuoteParameters':= Params}, Context, _Opts) ->
|
||||
case wapi_p2p_transfer_backend:quote_transfer(Params, Context) of
|
||||
{ok, Quote} ->
|
||||
wapi_handler_utils:reply_ok(201, Quote);
|
||||
{error, {identity, notfound}} ->
|
||||
wapi_handler_utils:reply_ok(422,
|
||||
wapi_handler_utils:get_error_msg(<<"No such identity">>));
|
||||
{error, {identity, unauthorized}} ->
|
||||
wapi_handler_utils:reply_ok(422,
|
||||
wapi_handler_utils:get_error_msg(<<"No such identity">>));
|
||||
{error, {sender, invalid_resource}} ->
|
||||
wapi_handler_utils:reply_ok(422,
|
||||
wapi_handler_utils:get_error_msg(<<"Invalid sender resource">>));
|
||||
{error, {receiver, invalid_resource}} ->
|
||||
wapi_handler_utils:reply_ok(422,
|
||||
wapi_handler_utils:get_error_msg(<<"Invalid receiver resource">>));
|
||||
{error, {p2p_transfer, forbidden_currency}} ->
|
||||
wapi_handler_utils:reply_ok(422,
|
||||
wapi_handler_utils:get_error_msg(<<"Currency not allowed">>));
|
||||
{error, {p2p_transfer, cash_range_exceeded}} ->
|
||||
wapi_handler_utils:reply_ok(422,
|
||||
wapi_handler_utils:get_error_msg(<<"Transfer amount is out of allowed range">>))
|
||||
end;
|
||||
|
||||
process_request('CreateP2PTransfer', #{'P2PTransferParameters' := Params}, Context, _Opts) ->
|
||||
case wapi_p2p_transfer_backend:create_transfer(Params, Context) of
|
||||
{ok, P2PTransfer} ->
|
||||
wapi_handler_utils:reply_ok(202, P2PTransfer);
|
||||
{error, {external_id_conflict, ID, ExternalID}} ->
|
||||
wapi_handler_utils:logic_error(external_id_conflict, {ID, ExternalID});
|
||||
{error, {identity, notfound}} ->
|
||||
wapi_handler_utils:reply_ok(422,
|
||||
wapi_handler_utils:get_error_msg(<<"No such identity">>));
|
||||
{error, {identity, unauthorized}} ->
|
||||
wapi_handler_utils:reply_ok(422,
|
||||
wapi_handler_utils:get_error_msg(<<"No such identity">>));
|
||||
{error, {sender, invalid_resource}} ->
|
||||
wapi_handler_utils:reply_ok(422,
|
||||
wapi_handler_utils:get_error_msg(<<"Invalid sender resource">>));
|
||||
{error, {receiver, invalid_resource}} ->
|
||||
wapi_handler_utils:reply_ok(422,
|
||||
wapi_handler_utils:get_error_msg(<<"Invalid receiver resource">>));
|
||||
{error, {token, {not_verified, _}}} ->
|
||||
wapi_handler_utils:reply_ok(422,
|
||||
wapi_handler_utils:get_error_msg(<<"Token can't be verified">>));
|
||||
{error, {p2p_transfer, operation_not_permitted}} ->
|
||||
wapi_handler_utils:reply_ok(422,
|
||||
wapi_handler_utils:get_error_msg(<<"Operation not permitted">>));
|
||||
{error, {p2p_transfer, forbidden_currency}} ->
|
||||
wapi_handler_utils:reply_ok(422,
|
||||
wapi_handler_utils:get_error_msg(<<"Currency not allowed">>));
|
||||
{error, {p2p_transfer, cash_range_exceeded}} ->
|
||||
wapi_handler_utils:reply_ok(422,
|
||||
wapi_handler_utils:get_error_msg(<<"Transfer amount is out of allowed range">>))
|
||||
% note: thrift has less expressive errors
|
||||
end;
|
||||
|
||||
process_request('GetP2PTransfer', #{p2pTransferID := ID}, Context, _Opts) ->
|
||||
case wapi_p2p_transfer_backend:get_transfer(ID, Context) of
|
||||
{ok, P2PTransfer} ->
|
||||
wapi_handler_utils:reply_ok(200, P2PTransfer);
|
||||
{error, {p2p_transfer, unauthorized}} ->
|
||||
wapi_handler_utils:reply_ok(404);
|
||||
{error, {p2p_transfer, notfound}} ->
|
||||
wapi_handler_utils:reply_ok(404)
|
||||
end;
|
||||
|
||||
%% Webhooks
|
||||
|
||||
process_request('CreateWebhook', #{'WebhookParams' := WebhookParams}, Context, _Opts) ->
|
||||
|
402
apps/wapi/test/wapi_p2p_transfer_tests_SUITE.erl
Normal file
402
apps/wapi/test/wapi_p2p_transfer_tests_SUITE.erl
Normal file
@ -0,0 +1,402 @@
|
||||
-module(wapi_p2p_transfer_tests_SUITE).
|
||||
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
|
||||
-include_lib("damsel/include/dmsl_domain_config_thrift.hrl").
|
||||
|
||||
-include_lib("jose/include/jose_jwk.hrl").
|
||||
-include_lib("wapi_wallet_dummy_data.hrl").
|
||||
|
||||
-include_lib("fistful_proto/include/ff_proto_p2p_transfer_thrift.hrl").
|
||||
|
||||
-export([all/0]).
|
||||
-export([groups/0]).
|
||||
-export([init_per_suite/1]).
|
||||
-export([end_per_suite/1]).
|
||||
-export([init_per_group/2]).
|
||||
-export([end_per_group/2]).
|
||||
-export([init_per_testcase/2]).
|
||||
-export([end_per_testcase/2]).
|
||||
|
||||
-export([init/1]).
|
||||
|
||||
-export([
|
||||
create/1,
|
||||
create_quote/1,
|
||||
create_with_quote_token/1,
|
||||
create_with_bad_quote_token/1,
|
||||
get/1,
|
||||
fail_unauthorized/1
|
||||
]).
|
||||
|
||||
% common-api is used since it is the domain used in production RN
|
||||
% TODO: change to wallet-api (or just omit since it is the default one) when new tokens will be a thing
|
||||
-define(DOMAIN, <<"common-api">>).
|
||||
-define(badresp(Code), {error, {invalid_response_code, Code}}).
|
||||
-define(emptyresp(Code), {error, {Code, #{}}}).
|
||||
|
||||
-type test_case_name() :: atom().
|
||||
-type config() :: [{atom(), any()}].
|
||||
-type group_name() :: atom().
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
-spec init([]) ->
|
||||
{ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
|
||||
init([]) ->
|
||||
{ok, {#{strategy => one_for_all, intensity => 1, period => 1}, []}}.
|
||||
|
||||
-spec all() ->
|
||||
[test_case_name()].
|
||||
all() ->
|
||||
[
|
||||
{group, base}
|
||||
].
|
||||
|
||||
-spec groups() ->
|
||||
[{group_name(), list(), [test_case_name()]}].
|
||||
groups() ->
|
||||
[
|
||||
{base, [],
|
||||
[
|
||||
create,
|
||||
create_quote,
|
||||
create_with_quote_token,
|
||||
create_with_bad_quote_token,
|
||||
get,
|
||||
fail_unauthorized
|
||||
]
|
||||
}
|
||||
].
|
||||
|
||||
%%
|
||||
%% starting/stopping
|
||||
%%
|
||||
-spec init_per_suite(config()) ->
|
||||
config().
|
||||
init_per_suite(Config) ->
|
||||
%% TODO remove this after cut off wapi
|
||||
ok = application:set_env(wapi, transport, thrift),
|
||||
ct_helper:makeup_cfg([
|
||||
ct_helper:test_case_name(init),
|
||||
ct_payment_system:setup(#{
|
||||
optional_apps => [
|
||||
bender_client,
|
||||
wapi_woody_client,
|
||||
wapi
|
||||
]
|
||||
})
|
||||
], Config).
|
||||
|
||||
-spec end_per_suite(config()) ->
|
||||
_.
|
||||
end_per_suite(C) ->
|
||||
%% TODO remove this after cut off wapi
|
||||
ok = ct_payment_system:shutdown(C).
|
||||
|
||||
-spec init_per_group(group_name(), config()) ->
|
||||
config().
|
||||
init_per_group(Group, Config) when Group =:= base ->
|
||||
ok = ff_context:save(ff_context:create(#{
|
||||
party_client => party_client:create_client(),
|
||||
woody_context => woody_context:new(<<"init_per_group/", (atom_to_binary(Group, utf8))/binary>>)
|
||||
})),
|
||||
Party = create_party(Config),
|
||||
BasePermissions = [
|
||||
{[party], read},
|
||||
{[party], write}
|
||||
],
|
||||
{ok, Token} = wapi_ct_helper:issue_token(Party, BasePermissions, {deadline, 10}, ?DOMAIN),
|
||||
Config1 = [{party, Party} | Config],
|
||||
ContextPcidss = get_context("wapi-pcidss:8080", Token),
|
||||
[
|
||||
{context_pcidss, ContextPcidss},
|
||||
{context, wapi_ct_helper:get_context(Token)} |
|
||||
Config1
|
||||
];
|
||||
init_per_group(_, Config) ->
|
||||
Config.
|
||||
|
||||
-spec end_per_group(group_name(), config()) ->
|
||||
_.
|
||||
end_per_group(_Group, _C) ->
|
||||
ok.
|
||||
|
||||
-spec init_per_testcase(test_case_name(), config()) ->
|
||||
config().
|
||||
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),
|
||||
[{test_sup, wapi_ct_helper:start_mocked_service_sup(?MODULE)} | C1].
|
||||
|
||||
-spec end_per_testcase(test_case_name(), config()) ->
|
||||
config().
|
||||
end_per_testcase(_Name, C) ->
|
||||
ok = ct_helper:unset_context(),
|
||||
wapi_ct_helper:stop_mocked_service_sup(?config(test_sup, C)),
|
||||
ok.
|
||||
|
||||
%%% Tests
|
||||
|
||||
-spec create(config()) ->
|
||||
_.
|
||||
create(C) ->
|
||||
mock_services(C),
|
||||
SenderToken = store_bank_card(C, <<"4150399999000900">>, <<"12/2025">>, <<"Buka Bjaka">>),
|
||||
ReceiverToken = store_bank_card(C, <<"4150399999000900">>, <<"12/2025">>, <<"Buka Bjaka">>),
|
||||
{ok, _} = call_api(
|
||||
fun swag_client_wallet_p2_p_api:create_p2_p_transfer/3,
|
||||
#{
|
||||
body => #{
|
||||
<<"identityID">> => <<"id">>,
|
||||
<<"body">> => #{
|
||||
<<"amount">> => ?INTEGER,
|
||||
<<"currency">> => ?RUB
|
||||
},
|
||||
<<"sender">> => #{
|
||||
<<"type">> => <<"BankCardSenderResourceParams">>,
|
||||
<<"token">> => SenderToken,
|
||||
<<"authData">> => <<"session id">>
|
||||
},
|
||||
<<"receiver">> => #{
|
||||
<<"type">> => <<"BankCardReceiverResourceParams">>,
|
||||
<<"token">> => ReceiverToken
|
||||
},
|
||||
<<"contactInfo">> => #{
|
||||
<<"email">> => <<"some@mail.com">>,
|
||||
<<"phoneNumber">> => <<"+79990000101">>
|
||||
}
|
||||
}
|
||||
},
|
||||
ct_helper:cfg(context, C)
|
||||
).
|
||||
|
||||
-spec create_quote(config()) ->
|
||||
_.
|
||||
create_quote(C) ->
|
||||
IdentityID = <<"id">>,
|
||||
PartyID = ?config(party, C),
|
||||
wapi_ct_helper:mock_services([
|
||||
{fistful_identity, fun('Get', _) -> {ok, ?IDENTITY(PartyID)};
|
||||
('GetContext', _) -> {ok, ?DEFAULT_CONTEXT(PartyID)} end},
|
||||
{p2p_transfer, fun('GetQuote', _) -> {ok, ?P2P_TRANSFER_QUOTE(IdentityID)} end}
|
||||
], C),
|
||||
SenderToken = store_bank_card(C, <<"4150399999000900">>, <<"12/2025">>, <<"Buka Bjaka">>),
|
||||
ReceiverToken = store_bank_card(C, <<"4150399999000900">>, <<"12/2025">>, <<"Buka Bjaka">>),
|
||||
{ok, #{<<"token">> := Token}} = call_api(
|
||||
fun swag_client_wallet_p2_p_api:quote_p2_p_transfer/3,
|
||||
#{
|
||||
body => #{
|
||||
<<"identityID">> => IdentityID,
|
||||
<<"body">> => #{
|
||||
<<"amount">> => ?INTEGER,
|
||||
<<"currency">> => ?RUB
|
||||
},
|
||||
<<"sender">> => #{
|
||||
<<"type">> => <<"BankCardSenderResource">>,
|
||||
<<"token">> => SenderToken
|
||||
},
|
||||
<<"receiver">> => #{
|
||||
<<"type">> => <<"BankCardReceiverResource">>,
|
||||
<<"token">> => ReceiverToken
|
||||
}
|
||||
}
|
||||
},
|
||||
ct_helper:cfg(context, C)
|
||||
),
|
||||
{ok, {_, _, Payload}} = uac_authorizer_jwt:verify(Token, #{}),
|
||||
{ok, #p2p_transfer_Quote{identity_id = IdentityID}} = wapi_p2p_quote:decode_token_payload(Payload).
|
||||
|
||||
-spec create_with_quote_token(config()) ->
|
||||
_.
|
||||
create_with_quote_token(C) ->
|
||||
IdentityID = <<"id">>,
|
||||
PartyID = ?config(party, C),
|
||||
wapi_ct_helper:mock_services([
|
||||
{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}
|
||||
], C),
|
||||
SenderToken = store_bank_card(C, <<"4150399999000900">>, <<"12/2025">>, <<"Buka Bjaka">>),
|
||||
ReceiverToken = store_bank_card(C, <<"4150399999000900">>, <<"12/2025">>, <<"Buka Bjaka">>),
|
||||
{ok, #{<<"token">> := QuoteToken}} = call_api(
|
||||
fun swag_client_wallet_p2_p_api:quote_p2_p_transfer/3,
|
||||
#{
|
||||
body => #{
|
||||
<<"identityID">> => IdentityID,
|
||||
<<"body">> => #{
|
||||
<<"amount">> => ?INTEGER,
|
||||
<<"currency">> => ?RUB
|
||||
},
|
||||
<<"sender">> => #{
|
||||
<<"type">> => <<"BankCardSenderResource">>,
|
||||
<<"token">> => SenderToken
|
||||
},
|
||||
<<"receiver">> => #{
|
||||
<<"type">> => <<"BankCardReceiverResource">>,
|
||||
<<"token">> => ReceiverToken
|
||||
}
|
||||
}
|
||||
},
|
||||
ct_helper:cfg(context, C)
|
||||
),
|
||||
{ok, _} = call_api(
|
||||
fun swag_client_wallet_p2_p_api:create_p2_p_transfer/3,
|
||||
#{
|
||||
body => #{
|
||||
<<"identityID">> => IdentityID,
|
||||
<<"body">> => #{
|
||||
<<"amount">> => ?INTEGER,
|
||||
<<"currency">> => ?RUB
|
||||
},
|
||||
<<"sender">> => #{
|
||||
<<"type">> => <<"BankCardSenderResourceParams">>,
|
||||
<<"token">> => SenderToken,
|
||||
<<"authData">> => <<"session id">>
|
||||
},
|
||||
<<"quoteToken">> => QuoteToken,
|
||||
<<"receiver">> => #{
|
||||
<<"type">> => <<"BankCardReceiverResourceParams">>,
|
||||
<<"token">> => ReceiverToken
|
||||
},
|
||||
<<"contactInfo">> => #{
|
||||
<<"email">> => <<"some@mail.com">>,
|
||||
<<"phoneNumber">> => <<"+79990000101">>
|
||||
}
|
||||
}
|
||||
},
|
||||
ct_helper:cfg(context, C)
|
||||
).
|
||||
|
||||
-spec create_with_bad_quote_token(config()) ->
|
||||
_.
|
||||
create_with_bad_quote_token(C) ->
|
||||
mock_services(C),
|
||||
SenderToken = store_bank_card(C, <<"4150399999000900">>, <<"12/2025">>, <<"Buka Bjaka">>),
|
||||
ReceiverToken = store_bank_card(C, <<"4150399999000900">>, <<"12/2025">>, <<"Buka Bjaka">>),
|
||||
{error, {422, #{
|
||||
<<"message">> := <<"Token can't be verified">>
|
||||
}}} = call_api(
|
||||
fun swag_client_wallet_p2_p_api:create_p2_p_transfer/3,
|
||||
#{
|
||||
body => #{
|
||||
<<"identityID">> => <<"id">>,
|
||||
<<"body">> => #{
|
||||
<<"amount">> => ?INTEGER,
|
||||
<<"currency">> => ?RUB
|
||||
},
|
||||
<<"sender">> => #{
|
||||
<<"type">> => <<"BankCardSenderResourceParams">>,
|
||||
<<"token">> => SenderToken,
|
||||
<<"authData">> => <<"session id">>
|
||||
},
|
||||
<<"quoteToken">> => <<"bad_quote_token">>,
|
||||
<<"receiver">> => #{
|
||||
<<"type">> => <<"BankCardReceiverResourceParams">>,
|
||||
<<"token">> => ReceiverToken
|
||||
},
|
||||
<<"contactInfo">> => #{
|
||||
<<"email">> => <<"some@mail.com">>,
|
||||
<<"phoneNumber">> => <<"+79990000101">>
|
||||
}
|
||||
}
|
||||
},
|
||||
ct_helper:cfg(context, C)
|
||||
).
|
||||
|
||||
-spec get(config()) ->
|
||||
_.
|
||||
get(C) ->
|
||||
PartyID = ?config(party, C),
|
||||
wapi_ct_helper:mock_services([
|
||||
{p2p_transfer, fun('Get', _) -> {ok, ?P2P_TRANSFER(PartyID)} end}
|
||||
], C),
|
||||
{ok, _} = call_api(
|
||||
fun swag_client_wallet_p2_p_api:get_p2_p_transfer/3,
|
||||
#{
|
||||
binding => #{
|
||||
<<"p2pTransferID">> => ?STRING
|
||||
}
|
||||
},
|
||||
ct_helper:cfg(context, C)
|
||||
).
|
||||
|
||||
-spec fail_unauthorized(config()) ->
|
||||
_.
|
||||
fail_unauthorized(C) ->
|
||||
WrongPartyID = <<"kek">>,
|
||||
mock_services(C, WrongPartyID),
|
||||
SenderToken = store_bank_card(C, <<"4150399999000900">>, <<"12/2025">>, <<"Buka Bjaka">>),
|
||||
ReceiverToken = store_bank_card(C, <<"4150399999000900">>, <<"12/2025">>, <<"Buka Bjaka">>),
|
||||
{error, {422, #{
|
||||
<<"message">> := <<"No such identity">>
|
||||
}}} = call_api(
|
||||
fun swag_client_wallet_p2_p_api:create_p2_p_transfer/3,
|
||||
#{
|
||||
body => #{
|
||||
<<"identityID">> => <<"id">>,
|
||||
<<"body">> => #{
|
||||
<<"amount">> => ?INTEGER,
|
||||
<<"currency">> => ?RUB
|
||||
},
|
||||
<<"sender">> => #{
|
||||
<<"type">> => <<"BankCardSenderResourceParams">>,
|
||||
<<"token">> => SenderToken,
|
||||
<<"authData">> => <<"session id">>
|
||||
},
|
||||
<<"receiver">> => #{
|
||||
<<"type">> => <<"BankCardReceiverResourceParams">>,
|
||||
<<"token">> => ReceiverToken
|
||||
},
|
||||
<<"contactInfo">> => #{
|
||||
<<"email">> => <<"some@mail.com">>,
|
||||
<<"phoneNumber">> => <<"+79990000101">>
|
||||
}
|
||||
}
|
||||
},
|
||||
ct_helper:cfg(context, C)
|
||||
).
|
||||
|
||||
%%
|
||||
|
||||
-spec call_api(function(), map(), wapi_client_lib:context()) ->
|
||||
{ok, term()} | {error, term()}.
|
||||
call_api(F, Params, Context) ->
|
||||
{Url, PreparedParams, Opts} = wapi_client_lib:make_request(Context, Params),
|
||||
Response = F(Url, PreparedParams, Opts),
|
||||
wapi_client_lib:handle_response(Response).
|
||||
|
||||
create_party(_C) ->
|
||||
ID = genlib:bsuuid(),
|
||||
_ = ff_party:create(ID),
|
||||
ID.
|
||||
|
||||
mock_services(C) ->
|
||||
mock_services(C, ?config(party, C)).
|
||||
|
||||
mock_services(C, ContextPartyID) ->
|
||||
PartyID = ?config(party, C),
|
||||
wapi_ct_helper:mock_services([
|
||||
{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}
|
||||
], C).
|
||||
|
||||
store_bank_card(C, Pan, ExpDate, CardHolder) ->
|
||||
{ok, Res} = call_api(
|
||||
fun swag_client_payres_payment_resources_api:store_bank_card/3,
|
||||
#{body => genlib_map:compact(#{
|
||||
<<"type">> => <<"BankCard">>,
|
||||
<<"cardNumber">> => Pan,
|
||||
<<"expDate">> => ExpDate,
|
||||
<<"cardHolder">> => CardHolder
|
||||
})},
|
||||
ct_helper:cfg(context_pcidss, C)
|
||||
),
|
||||
maps:get(<<"token">>, Res).
|
||||
|
||||
get_context(Endpoint, Token) ->
|
||||
wapi_client_lib:get_context(Endpoint, Token, 10000, ipv4).
|
@ -2,6 +2,7 @@
|
||||
|
||||
-include_lib("stdlib/include/assert.hrl").
|
||||
-include_lib("fistful_proto/include/ff_proto_fistful_thrift.hrl").
|
||||
-include_lib("fistful_proto/include/ff_proto_p2p_transfer_thrift.hrl").
|
||||
-include_lib("wapi_wallet_dummy_data.hrl").
|
||||
|
||||
-export([all/0]).
|
||||
@ -18,6 +19,7 @@
|
||||
-export([identity_challenge_check_test/1]).
|
||||
-export([destination_check_test/1]).
|
||||
-export([w2w_transfer_check_test/1]).
|
||||
-export([p2p_transfer_check_test/1]).
|
||||
-export([withdrawal_check_test/1]).
|
||||
|
||||
% common-api is used since it is the domain used in production RN
|
||||
@ -48,6 +50,7 @@ groups() ->
|
||||
wallet_check_test,
|
||||
destination_check_test,
|
||||
w2w_transfer_check_test,
|
||||
p2p_transfer_check_test,
|
||||
withdrawal_check_test
|
||||
]}
|
||||
].
|
||||
@ -189,6 +192,23 @@ w2w_transfer_check_test(C) ->
|
||||
W2WTransferID2 = create_w2w_transfer(WalletID21, WalletID22, C),
|
||||
?assertEqual(Keys, maps:keys(get_w2w_transfer(W2WTransferID2, C))).
|
||||
|
||||
-spec p2p_transfer_check_test(config()) -> test_return().
|
||||
|
||||
p2p_transfer_check_test(C) ->
|
||||
Name = <<"Keyn Fawkes">>,
|
||||
Provider = ?ID_PROVIDER,
|
||||
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),
|
||||
P2PTransfer = get_p2p_transfer(P2PTransferID, C),
|
||||
ok = application:set_env(wapi, transport, thrift),
|
||||
P2PTransferIDThrift = create_p2p_transfer(Token, Token, IdentityID, C),
|
||||
P2PTransferThrift = get_p2p_transfer(P2PTransferIDThrift, C),
|
||||
?assertEqual(maps:keys(P2PTransfer), maps:keys(P2PTransferThrift)),
|
||||
?assertEqual(maps:without([<<"id">>, <<"createdAt">>], P2PTransfer),
|
||||
maps:without([<<"id">>, <<"createdAt">>], P2PTransferThrift)).
|
||||
|
||||
-spec withdrawal_check_test(config()) -> test_return().
|
||||
|
||||
withdrawal_check_test(C) ->
|
||||
@ -228,6 +248,21 @@ get_context(Endpoint, Token) ->
|
||||
|
||||
%%
|
||||
|
||||
store_bank_card(C, Pan, ExpDate, CardHolder) ->
|
||||
{ok, Res} = call_api(
|
||||
fun swag_client_payres_payment_resources_api:store_bank_card/3,
|
||||
#{body => genlib_map:compact(#{
|
||||
<<"type">> => <<"BankCard">>,
|
||||
<<"cardNumber">> => Pan,
|
||||
<<"expDate">> => ExpDate,
|
||||
<<"cardHolder">> => CardHolder
|
||||
})},
|
||||
ct_helper:cfg(context_pcidss, C)
|
||||
),
|
||||
maps:get(<<"token">>, Res).
|
||||
|
||||
%%
|
||||
|
||||
create_identity(Name, Provider, Class, C) ->
|
||||
{ok, Identity} = call_api(
|
||||
fun swag_client_wallet_identities_api:create_identity/3,
|
||||
@ -401,6 +436,78 @@ get_w2w_transfer(W2WTransferID2, C) ->
|
||||
),
|
||||
W2WTransfer.
|
||||
|
||||
create_p2p_transfer(SenderToken, ReceiverToken, IdentityID, C) ->
|
||||
DefaultParams = #{
|
||||
<<"identityID">> => IdentityID,
|
||||
<<"sender">> => #{
|
||||
<<"type">> => <<"BankCardSenderResourceParams">>,
|
||||
<<"token">> => SenderToken,
|
||||
<<"authData">> => <<"session id">>
|
||||
},
|
||||
<<"receiver">> => #{
|
||||
<<"type">> => <<"BankCardReceiverResourceParams">>,
|
||||
<<"token">> => ReceiverToken
|
||||
},
|
||||
<<"quoteToken">> => get_quote_token(SenderToken, ReceiverToken, IdentityID, C),
|
||||
<<"body">> => #{
|
||||
<<"amount">> => ?INTEGER,
|
||||
<<"currency">> => ?RUB
|
||||
},
|
||||
<<"contactInfo">> => #{
|
||||
<<"email">> => <<"some@mail.com">>,
|
||||
<<"phoneNumber">> => <<"+79990000101">>
|
||||
}
|
||||
},
|
||||
{ok, P2PTransfer} = call_api(
|
||||
fun swag_client_wallet_p2_p_api:create_p2_p_transfer/3,
|
||||
#{body => DefaultParams},
|
||||
ct_helper:cfg(context, C)
|
||||
),
|
||||
maps:get(<<"id">>, P2PTransfer).
|
||||
|
||||
get_quote_token(SenderToken, ReceiverToken, IdentityID, C) ->
|
||||
PartyID = ct_helper:cfg(party, C),
|
||||
{ok, SenderBankCard} = wapi_crypto:decrypt_bankcard_token(SenderToken),
|
||||
{ok, ReceiverBankCard} = wapi_crypto:decrypt_bankcard_token(ReceiverToken),
|
||||
{ok, PartyRevision} = ff_party:get_revision(PartyID),
|
||||
Quote = #p2p_transfer_Quote{
|
||||
identity_id = IdentityID,
|
||||
created_at = <<"1970-01-01T00:00:00.123Z">>,
|
||||
expires_on = <<"1970-01-01T00:00:00.321Z">>,
|
||||
party_revision = PartyRevision,
|
||||
domain_revision = 1,
|
||||
fees = #'Fees'{fees = #{}},
|
||||
body = #'Cash'{
|
||||
amount = ?INTEGER,
|
||||
currency = #'CurrencyRef'{
|
||||
symbolic_code = ?RUB
|
||||
}
|
||||
},
|
||||
sender = {bank_card, #'ResourceBankCard'{
|
||||
bank_card = #'BankCard'{
|
||||
token = SenderBankCard#'BankCard'.token,
|
||||
bin_data_id = {i, 123}
|
||||
}
|
||||
}},
|
||||
receiver = {bank_card, #'ResourceBankCard'{
|
||||
bank_card = #'BankCard'{
|
||||
token = ReceiverBankCard#'BankCard'.token,
|
||||
bin_data_id = {i, 123}
|
||||
}
|
||||
}}
|
||||
},
|
||||
Payload = wapi_p2p_quote:create_token_payload(Quote, PartyID),
|
||||
{ok, QuoteToken} = uac_authorizer_jwt:issue(wapi_utils:get_unique_id(), PartyID, Payload, wapi_auth:get_signee()),
|
||||
QuoteToken.
|
||||
|
||||
get_p2p_transfer(P2PTransferID, C) ->
|
||||
{ok, P2PTransfer} = call_api(
|
||||
fun swag_client_wallet_p2_p_api:get_p2_p_transfer/3,
|
||||
#{binding => #{<<"p2pTransferID">> => P2PTransferID}},
|
||||
ct_helper:cfg(context, C)
|
||||
),
|
||||
P2PTransfer.
|
||||
|
||||
await_destination(DestID) ->
|
||||
authorized = ct_helper:await(
|
||||
authorized,
|
||||
@ -511,6 +618,39 @@ get_default_termset() ->
|
||||
}
|
||||
]}
|
||||
},
|
||||
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)}
|
||||
}}
|
||||
}
|
||||
]}
|
||||
},
|
||||
w2w = #domain_W2WServiceTerms{
|
||||
currencies = {value, ?ordset([?cur(<<"RUB">>), ?cur(<<"USD">>)])},
|
||||
allow = {constant, true},
|
||||
|
@ -110,7 +110,8 @@
|
||||
expected_max = ?INTEGER
|
||||
}).
|
||||
|
||||
-define(RESOURCE, {bank_card, #'BankCard'{
|
||||
-define(BANK_CARD, #'BankCard'{
|
||||
bin_data_id = {i, ?INTEGER},
|
||||
token = ?STRING,
|
||||
bin = <<"424242">>,
|
||||
masked_pan = <<"4242">>,
|
||||
@ -118,7 +119,9 @@
|
||||
payment_system = visa,
|
||||
issuer_country = rus,
|
||||
card_type = debit
|
||||
}}).
|
||||
}).
|
||||
|
||||
-define(RESOURCE, {bank_card, ?BANK_CARD}).
|
||||
|
||||
-define(DESTINATION_STATUS, {authorized, #dst_Authorized{}}).
|
||||
|
||||
@ -437,3 +440,47 @@
|
||||
])
|
||||
}
|
||||
}).
|
||||
|
||||
-define(RESOURCE_BANK_CARD, {bank_card, #'ResourceBankCard'{
|
||||
bank_card = ?BANK_CARD
|
||||
}}).
|
||||
|
||||
-define(RAW_RESOURCE, {resource, #'p2p_transfer_RawResource'{
|
||||
contact_info = #'ContactInfo'{},
|
||||
resource = ?RESOURCE_BANK_CARD
|
||||
}}).
|
||||
|
||||
-define(P2P_TRANSFER(PartyID), #p2p_transfer_P2PTransferState{
|
||||
id = ?STRING,
|
||||
owner = ?STRING,
|
||||
sender = ?RAW_RESOURCE,
|
||||
receiver = ?RAW_RESOURCE,
|
||||
body = ?CASH,
|
||||
status = {pending, #p2p_status_Pending{}},
|
||||
created_at = ?TIMESTAMP,
|
||||
domain_revision = ?INTEGER,
|
||||
party_revision = ?INTEGER,
|
||||
operation_timestamp = ?TIMESTAMP,
|
||||
external_id = ?STRING,
|
||||
metadata = ?DEFAULT_METADATA(),
|
||||
context = ?DEFAULT_CONTEXT(PartyID),
|
||||
effective_final_cash_flow = #cashflow_FinalCashFlow{
|
||||
postings = []
|
||||
},
|
||||
sessions = [],
|
||||
adjustments = []
|
||||
}).
|
||||
|
||||
-define(FEES, #'Fees'{fees = #{operation_amount => ?CASH}}).
|
||||
|
||||
-define(P2P_TRANSFER_QUOTE(IdentityID), #p2p_transfer_Quote{
|
||||
body = ?CASH,
|
||||
created_at = ?TIMESTAMP,
|
||||
expires_on = ?TIMESTAMP,
|
||||
domain_revision = ?INTEGER,
|
||||
party_revision = ?INTEGER,
|
||||
identity_id = IdentityID,
|
||||
sender = ?RESOURCE_BANK_CARD,
|
||||
receiver = ?RESOURCE_BANK_CARD,
|
||||
fees = ?FEES
|
||||
}).
|
||||
|
@ -94,6 +94,8 @@ 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) ->
|
||||
{ff_proto_p2p_transfer_thrift, 'Management'};
|
||||
get_service_modname(w2w_transfer) ->
|
||||
{ff_proto_w2w_transfer_thrift, 'Management'}.
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user