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:
Roman Pushkov 2020-10-06 11:43:28 +03:00 committed by GitHub
parent 1766e90261
commit ebdd7e9a25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1137 additions and 63 deletions

View File

@ -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, #{

View File

@ -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)),
@ -583,4 +583,4 @@ p2p_timestamped_change_codec_test() ->
Decoded = ff_proto_utils:deserialize(Type, Binary),
?assertEqual(TimestampedChange, unmarshal(timestamped_change, Decoded)).
-endif.
-endif.

View File

@ -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} ->

View File

@ -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.

View File

@ -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 = #{

View 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).

View File

@ -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) ->

View File

@ -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) ->

View 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).

View File

@ -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},

View File

@ -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
}).

View File

@ -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'}.