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_identity => "http://localhost:8022/v1/identity",
|
||||||
fistful_destination => "http://localhost:8022/v1/destination",
|
fistful_destination => "http://localhost:8022/v1/destination",
|
||||||
w2w_transfer => "http://localhost:8022/v1/w2w_transfer",
|
w2w_transfer => "http://localhost:8022/v1/w2w_transfer",
|
||||||
|
p2p_transfer => "http://localhost:8022/v1/p2p_transfer",
|
||||||
fistful_withdrawal => "http://localhost:8022/v1/withdrawal"
|
fistful_withdrawal => "http://localhost:8022/v1/withdrawal"
|
||||||
}},
|
}},
|
||||||
{service_retries, #{
|
{service_retries, #{
|
||||||
|
@ -26,8 +26,8 @@ unmarshal_p2p_quote_params(#p2p_transfer_QuoteParams{
|
|||||||
#{
|
#{
|
||||||
identity_id => unmarshal(id, IdentityID),
|
identity_id => unmarshal(id, IdentityID),
|
||||||
body => unmarshal(cash, Body),
|
body => unmarshal(cash, Body),
|
||||||
sender => unmarshal(participant, Sender),
|
sender => unmarshal(resource, Sender),
|
||||||
receiver => unmarshal(participant, Receiver)
|
receiver => unmarshal(resource, Receiver)
|
||||||
}.
|
}.
|
||||||
|
|
||||||
-spec marshal_p2p_transfer_state(p2p_transfer:p2p_transfer_state(), ff_entity_context:context()) ->
|
-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)),
|
operation_timestamp = marshal(timestamp_ms, p2p_transfer:operation_timestamp(P2PTransferState)),
|
||||||
created_at = marshal(timestamp_ms, p2p_transfer:created_at(P2PTransferState)),
|
created_at = marshal(timestamp_ms, p2p_transfer:created_at(P2PTransferState)),
|
||||||
deadline = maybe_marshal(timestamp_ms, p2p_transfer:deadline(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)),
|
client_info = maybe_marshal(client_info, p2p_transfer:client_info(P2PTransferState)),
|
||||||
external_id = maybe_marshal(id, p2p_transfer:external_id(P2PTransferState)),
|
external_id = maybe_marshal(id, p2p_transfer:external_id(P2PTransferState)),
|
||||||
metadata = marshal(ctx, p2p_transfer:metadata(P2PTransferState)),
|
metadata = marshal(ctx, p2p_transfer:metadata(P2PTransferState)),
|
||||||
@ -583,4 +583,4 @@ p2p_timestamped_change_codec_test() ->
|
|||||||
Decoded = ff_proto_utils:deserialize(Type, Binary),
|
Decoded = ff_proto_utils:deserialize(Type, Binary),
|
||||||
?assertEqual(TimestampedChange, unmarshal(timestamped_change, Decoded)).
|
?assertEqual(TimestampedChange, unmarshal(timestamped_change, Decoded)).
|
||||||
|
|
||||||
-endif.
|
-endif.
|
||||||
|
@ -280,7 +280,7 @@ get_contract_terms(PartyID, ContractID, Varset, Timestamp, PartyRevision, Domain
|
|||||||
{error, #payproc_PartyNotFound{}} ->
|
{error, #payproc_PartyNotFound{}} ->
|
||||||
{error, {party_not_found, PartyID}};
|
{error, {party_not_found, PartyID}};
|
||||||
{error, #payproc_ContractNotFound{}} ->
|
{error, #payproc_ContractNotFound{}} ->
|
||||||
{error, {contract_not_found, PartyID}};
|
{error, {contract_not_found, ContractID}};
|
||||||
{error, #payproc_PartyNotExistsYet{}} ->
|
{error, #payproc_PartyNotExistsYet{}} ->
|
||||||
{error, {party_not_exists_yet, PartyID}};
|
{error, {party_not_exists_yet, PartyID}};
|
||||||
{error, Unexpected} ->
|
{error, Unexpected} ->
|
||||||
|
@ -5,18 +5,27 @@
|
|||||||
-include_lib("fistful_proto/include/ff_proto_destination_thrift.hrl").
|
-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_p2p_template_thrift.hrl").
|
||||||
-include_lib("fistful_proto/include/ff_proto_w2w_transfer_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").
|
-include_lib("fistful_proto/include/ff_proto_withdrawal_thrift.hrl").
|
||||||
|
|
||||||
-export([check_resource/3]).
|
-export([check_resource/3]).
|
||||||
-export([check_resource_by_id/3]).
|
-export([check_resource_by_id/3]).
|
||||||
|
|
||||||
-type id() :: binary().
|
-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 handler_context() :: wapi_handler:context().
|
||||||
-type data() ::
|
-type data() ::
|
||||||
ff_proto_identity_thrift:'IdentityState'() |
|
ff_proto_identity_thrift:'IdentityState'() |
|
||||||
ff_proto_wallet_thrift:'WalletState'() |
|
ff_proto_wallet_thrift:'WalletState'() |
|
||||||
|
ff_proto_p2p_transfer_thrift:'P2PTransferState'() |
|
||||||
ff_proto_p2p_template_thrift:'P2PTemplateState'().
|
ff_proto_p2p_template_thrift:'P2PTemplateState'().
|
||||||
|
|
||||||
-define(CTX_NS, <<"com.rbkmoney.wapi">>).
|
-define(CTX_NS, <<"com.rbkmoney.wapi">>).
|
||||||
@ -86,6 +95,14 @@ get_context_by_id(w2w_transfer, W2WTransferID, WoodyCtx) ->
|
|||||||
{exception, #fistful_W2WNotFound{}} ->
|
{exception, #fistful_W2WNotFound{}} ->
|
||||||
{error, notfound}
|
{error, notfound}
|
||||||
end;
|
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) ->
|
get_context_by_id(withdrawal, WithdrawalID, WoodyCtx) ->
|
||||||
Request = {fistful_withdrawal, 'GetContext', [WithdrawalID]},
|
Request = {fistful_withdrawal, 'GetContext', [WithdrawalID]},
|
||||||
case wapi_handler_utils:service_call(Request, WoodyCtx) of
|
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;
|
Context;
|
||||||
get_context_from_state(w2w_transfer, #w2w_transfer_W2WTransferState{context = Context}) ->
|
get_context_from_state(w2w_transfer, #w2w_transfer_W2WTransferState{context = Context}) ->
|
||||||
Context;
|
Context;
|
||||||
|
get_context_from_state(p2p_transfer, #p2p_transfer_P2PTransferState{context = Context}) ->
|
||||||
|
Context;
|
||||||
get_context_from_state(withdrawal, #wthd_WithdrawalState{context = Context}) ->
|
get_context_from_state(withdrawal, #wthd_WithdrawalState{context = Context}) ->
|
||||||
Context.
|
Context.
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
-module(wapi_p2p_quote).
|
-module(wapi_p2p_quote).
|
||||||
|
|
||||||
|
-include_lib("fistful_proto/include/ff_proto_p2p_transfer_thrift.hrl").
|
||||||
|
|
||||||
-export([create_token_payload/2]).
|
-export([create_token_payload/2]).
|
||||||
-export([decode_token_payload/1]).
|
-export([decode_token_payload/1]).
|
||||||
|
|
||||||
@ -15,17 +17,20 @@
|
|||||||
%% Internal types
|
%% Internal types
|
||||||
|
|
||||||
-type party_id() :: binary().
|
-type party_id() :: binary().
|
||||||
-type quote() :: p2p_quote:quote().
|
-type quote() :: ff_proto_p2p_transfer_thrift:'Quote'().
|
||||||
|
|
||||||
%% API
|
%% API
|
||||||
|
|
||||||
-spec create_token_payload(quote(), party_id()) ->
|
-spec create_token_payload(quote(), party_id()) ->
|
||||||
token_payload().
|
token_payload().
|
||||||
create_token_payload(Quote, PartyID) ->
|
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(#{
|
genlib_map:compact(#{
|
||||||
<<"version">> => 2,
|
<<"version">> => 2,
|
||||||
<<"partyID">> => PartyID,
|
<<"partyID">> => PartyID,
|
||||||
<<"quote">> => encode_quote(Quote)
|
<<"quote">> => EncodedQuote
|
||||||
}).
|
}).
|
||||||
|
|
||||||
-spec decode_token_payload(token_payload()) ->
|
-spec decode_token_payload(token_payload()) ->
|
||||||
@ -36,53 +41,46 @@ decode_token_payload(#{<<"version">> := 2} = Payload) ->
|
|||||||
<<"partyID">> := _PartyID,
|
<<"partyID">> := _PartyID,
|
||||||
<<"quote">> := EncodedQuote
|
<<"quote">> := EncodedQuote
|
||||||
} = Payload,
|
} = 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};
|
{ok, Quote};
|
||||||
decode_token_payload(#{<<"version">> := 1}) ->
|
decode_token_payload(#{<<"version">> := 1}) ->
|
||||||
{error, token_expired}.
|
{error, token_expired}.
|
||||||
|
|
||||||
%% Internals
|
%% 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).
|
-ifdef(TEST).
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
-spec test() -> _.
|
-spec test() -> _.
|
||||||
|
|
||||||
-spec payload_symmetry_test() -> _.
|
-spec payload_symmetry_test() -> _.
|
||||||
payload_symmetry_test() ->
|
payload_symmetry_test() ->
|
||||||
Quote = #{
|
Quote = #p2p_transfer_Quote{
|
||||||
fees => #{
|
identity_id = <<"identity">>,
|
||||||
fees => #{
|
created_at = <<"1970-01-01T00:00:00.123Z">>,
|
||||||
surplus => {1000, <<"RUB">>}
|
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">>},
|
sender = {bank_card, #'ResourceBankCard'{
|
||||||
party_revision => 1,
|
bank_card = #'BankCard'{token = <<"very long token">>},
|
||||||
domain_revision => 2,
|
auth_data = {session_data, #'SessionAuthData'{id = <<"1">>}}
|
||||||
created_at => 123,
|
|
||||||
expires_on => 321,
|
|
||||||
identity_id => <<"identity">>,
|
|
||||||
sender => {bank_card, #{
|
|
||||||
token => <<"very long token">>,
|
|
||||||
bin_data_id => nil
|
|
||||||
}},
|
}},
|
||||||
receiver => {bank_card, #{
|
receiver = {bank_card, #'ResourceBankCard'{
|
||||||
token => <<"another very long token">>,
|
bank_card = #'BankCard'{token = <<"another very long token">>},
|
||||||
bin_data_id => #{[nil] => [nil]}
|
auth_data = {session_data, #'SessionAuthData'{id = <<"2">>}}
|
||||||
}}
|
}}
|
||||||
},
|
},
|
||||||
Payload = create_token_payload(Quote, <<"party">>),
|
Payload = create_token_payload(Quote, <<"party">>),
|
||||||
@ -91,25 +89,35 @@ payload_symmetry_test() ->
|
|||||||
|
|
||||||
-spec payload_v2_decoding_test() -> _.
|
-spec payload_v2_decoding_test() -> _.
|
||||||
payload_v2_decoding_test() ->
|
payload_v2_decoding_test() ->
|
||||||
ExpectedQuote = #{
|
ExpectedQuote = #p2p_transfer_Quote{
|
||||||
fees => #{
|
identity_id = <<"identity">>,
|
||||||
fees => #{
|
created_at = <<"1970-01-01T00:00:00.123Z">>,
|
||||||
surplus => {1000, <<"RUB">>}
|
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">>},
|
sender = {bank_card, #'ResourceBankCard'{
|
||||||
party_revision => 1,
|
bank_card = #'BankCard'{
|
||||||
domain_revision => 2,
|
token = <<"very long token">>,
|
||||||
created_at => 123,
|
bin_data_id = {nl, #msgp_Nil{}}
|
||||||
expires_on => 321,
|
}
|
||||||
identity_id => <<"identity">>,
|
|
||||||
sender => {bank_card, #{
|
|
||||||
token => <<"very long token">>,
|
|
||||||
bin_data_id => nil
|
|
||||||
}},
|
}},
|
||||||
receiver => {bank_card, #{
|
receiver = {bank_card, #'ResourceBankCard'{
|
||||||
token => <<"another very long token">>,
|
bank_card = #'BankCard'{
|
||||||
bin_data_id => #{[nil] => [nil]}
|
token = <<"another very long token">>,
|
||||||
|
bin_data_id = {obj, #{{arr, [{nl, #msgp_Nil{}}]} => {arr, [{nl, #msgp_Nil{}}]}}}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
},
|
},
|
||||||
Payload = #{
|
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.
|
Token.
|
||||||
|
|
||||||
create_p2p_quote_token(Quote, PartyID) ->
|
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),
|
{ok, Token} = issue_quote_token(PartyID, Payload),
|
||||||
Token.
|
Token.
|
||||||
|
|
||||||
@ -1043,10 +1043,11 @@ maybe_add_p2p_template_quote_token(ID, #{quote_token := QuoteToken} = Params) ->
|
|||||||
do(fun() ->
|
do(fun() ->
|
||||||
VerifiedToken = unwrap(verify_p2p_quote_token(QuoteToken)),
|
VerifiedToken = unwrap(verify_p2p_quote_token(QuoteToken)),
|
||||||
Quote = unwrap(quote, wapi_p2p_quote:decode_token_payload(VerifiedToken)),
|
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)),
|
Machine = unwrap(p2p_template_machine:get(ID)),
|
||||||
State = p2p_template_machine:p2p_template(Machine),
|
State = p2p_template_machine:p2p_template(Machine),
|
||||||
ok = unwrap(authorize_p2p_quote_token(Quote, p2p_template:identity_id(State))),
|
ok = unwrap(authorize_p2p_quote_token(UnmarshaledQuote, p2p_template:identity_id(State))),
|
||||||
Params#{quote => Quote}
|
Params#{quote => UnmarshaledQuote}
|
||||||
end).
|
end).
|
||||||
|
|
||||||
maybe_add_p2p_quote_token(#{quote_token := undefined} = Params) ->
|
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() ->
|
do(fun() ->
|
||||||
VerifiedToken = unwrap(verify_p2p_quote_token(QuoteToken)),
|
VerifiedToken = unwrap(verify_p2p_quote_token(QuoteToken)),
|
||||||
Quote = unwrap(quote, wapi_p2p_quote:decode_token_payload(VerifiedToken)),
|
Quote = unwrap(quote, wapi_p2p_quote:decode_token_payload(VerifiedToken)),
|
||||||
ok = unwrap(authorize_p2p_quote_token(Quote, IdentityID)),
|
UnmarshaledQuote = ff_p2p_transfer_codec:unmarshal(quote, Quote),
|
||||||
Params#{quote => Quote}
|
ok = unwrap(authorize_p2p_quote_token(UnmarshaledQuote, IdentityID)),
|
||||||
|
Params#{quote => UnmarshaledQuote}
|
||||||
end).
|
end).
|
||||||
|
|
||||||
max_event_id(NewEventID, OldEventID) when is_integer(NewEventID) andalso is_integer(OldEventID) ->
|
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)
|
wapi_handler_utils:reply_ok(404)
|
||||||
end;
|
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
|
%% Webhooks
|
||||||
|
|
||||||
process_request('CreateWebhook', #{'WebhookParams' := WebhookParams}, Context, _Opts) ->
|
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("stdlib/include/assert.hrl").
|
||||||
-include_lib("fistful_proto/include/ff_proto_fistful_thrift.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").
|
-include_lib("wapi_wallet_dummy_data.hrl").
|
||||||
|
|
||||||
-export([all/0]).
|
-export([all/0]).
|
||||||
@ -18,6 +19,7 @@
|
|||||||
-export([identity_challenge_check_test/1]).
|
-export([identity_challenge_check_test/1]).
|
||||||
-export([destination_check_test/1]).
|
-export([destination_check_test/1]).
|
||||||
-export([w2w_transfer_check_test/1]).
|
-export([w2w_transfer_check_test/1]).
|
||||||
|
-export([p2p_transfer_check_test/1]).
|
||||||
-export([withdrawal_check_test/1]).
|
-export([withdrawal_check_test/1]).
|
||||||
|
|
||||||
% common-api is used since it is the domain used in production RN
|
% common-api is used since it is the domain used in production RN
|
||||||
@ -48,6 +50,7 @@ groups() ->
|
|||||||
wallet_check_test,
|
wallet_check_test,
|
||||||
destination_check_test,
|
destination_check_test,
|
||||||
w2w_transfer_check_test,
|
w2w_transfer_check_test,
|
||||||
|
p2p_transfer_check_test,
|
||||||
withdrawal_check_test
|
withdrawal_check_test
|
||||||
]}
|
]}
|
||||||
].
|
].
|
||||||
@ -189,6 +192,23 @@ w2w_transfer_check_test(C) ->
|
|||||||
W2WTransferID2 = create_w2w_transfer(WalletID21, WalletID22, C),
|
W2WTransferID2 = create_w2w_transfer(WalletID21, WalletID22, C),
|
||||||
?assertEqual(Keys, maps:keys(get_w2w_transfer(W2WTransferID2, 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().
|
-spec withdrawal_check_test(config()) -> test_return().
|
||||||
|
|
||||||
withdrawal_check_test(C) ->
|
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) ->
|
create_identity(Name, Provider, Class, C) ->
|
||||||
{ok, Identity} = call_api(
|
{ok, Identity} = call_api(
|
||||||
fun swag_client_wallet_identities_api:create_identity/3,
|
fun swag_client_wallet_identities_api:create_identity/3,
|
||||||
@ -401,6 +436,78 @@ get_w2w_transfer(W2WTransferID2, C) ->
|
|||||||
),
|
),
|
||||||
W2WTransfer.
|
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) ->
|
await_destination(DestID) ->
|
||||||
authorized = ct_helper:await(
|
authorized = ct_helper:await(
|
||||||
authorized,
|
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{
|
w2w = #domain_W2WServiceTerms{
|
||||||
currencies = {value, ?ordset([?cur(<<"RUB">>), ?cur(<<"USD">>)])},
|
currencies = {value, ?ordset([?cur(<<"RUB">>), ?cur(<<"USD">>)])},
|
||||||
allow = {constant, true},
|
allow = {constant, true},
|
||||||
|
@ -110,7 +110,8 @@
|
|||||||
expected_max = ?INTEGER
|
expected_max = ?INTEGER
|
||||||
}).
|
}).
|
||||||
|
|
||||||
-define(RESOURCE, {bank_card, #'BankCard'{
|
-define(BANK_CARD, #'BankCard'{
|
||||||
|
bin_data_id = {i, ?INTEGER},
|
||||||
token = ?STRING,
|
token = ?STRING,
|
||||||
bin = <<"424242">>,
|
bin = <<"424242">>,
|
||||||
masked_pan = <<"4242">>,
|
masked_pan = <<"4242">>,
|
||||||
@ -118,7 +119,9 @@
|
|||||||
payment_system = visa,
|
payment_system = visa,
|
||||||
issuer_country = rus,
|
issuer_country = rus,
|
||||||
card_type = debit
|
card_type = debit
|
||||||
}}).
|
}).
|
||||||
|
|
||||||
|
-define(RESOURCE, {bank_card, ?BANK_CARD}).
|
||||||
|
|
||||||
-define(DESTINATION_STATUS, {authorized, #dst_Authorized{}}).
|
-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'};
|
{ff_proto_p2p_template_thrift, 'Management'};
|
||||||
get_service_modname(webhook_manager) ->
|
get_service_modname(webhook_manager) ->
|
||||||
{ff_proto_webhooker_thrift, 'WebhookManager'};
|
{ff_proto_webhooker_thrift, 'WebhookManager'};
|
||||||
|
get_service_modname(p2p_transfer) ->
|
||||||
|
{ff_proto_p2p_transfer_thrift, 'Management'};
|
||||||
get_service_modname(w2w_transfer) ->
|
get_service_modname(w2w_transfer) ->
|
||||||
{ff_proto_w2w_transfer_thrift, 'Management'}.
|
{ff_proto_w2w_transfer_thrift, 'Management'}.
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user