fixed and refactored

This commit is contained in:
WWW_cool 2020-09-14 15:58:11 +03:00
parent a8cb04b608
commit ed35865617
45 changed files with 2122 additions and 1038 deletions

View File

@ -19,6 +19,7 @@
file_storage_proto,
swag_server_wallet,
swag_client_payres,
party_client,
scoper,
jose,
jsx,
@ -29,7 +30,6 @@
snowflake,
woody_user_identity,
payproc_errors,
ff_server,
uac
]},
{env, []}

View File

@ -97,7 +97,7 @@ get_context_from_state(withdrawal, #wthd_WithdrawalState{context = Context}) ->
Context.
get_owner(ContextThrift) ->
Context = ff_codec:unmarshal(context, ContextThrift),
Context = wapi_codec:unmarshal(context, ContextThrift),
wapi_backend_utils:get_from_ctx(<<"owner">>, Context).
is_resource_owner(Owner, HandlerCtx) ->

View File

@ -6,8 +6,18 @@
-define(BENDER_DOMAIN, <<"wapi">>).
%% Context
-type md() :: ff_entity_context:md().
-type context() :: ff_entity_context:context().
-type context() :: #{namespace() => md()}.
-type namespace() :: binary().
-type md() :: %% as stolen from `machinery_msgpack`
nil |
boolean() |
integer() |
float() |
binary() | %% string
{binary, binary()} | %% binary
[md()] |
#{md() => md()} .
-type handler_context() :: wapi_handler:context().
-type id() :: binary().
-type hash() :: integer().

View File

@ -0,0 +1,601 @@
-module(wapi_codec).
-include_lib("fistful_proto/include/ff_proto_base_thrift.hrl").
-include_lib("fistful_proto/include/ff_proto_repairer_thrift.hrl").
-include_lib("fistful_proto/include/ff_proto_account_thrift.hrl").
-include_lib("fistful_proto/include/ff_proto_msgpack_thrift.hrl").
-export([unmarshal/2]).
-export([unmarshal/3]).
-export([marshal/2]).
-export([marshal/3]).
%% Types
-type type_name() :: atom() | {list, atom()} | {set, atom()}.
-type codec() :: module().
-type encoded_value() :: encoded_value(any()).
-type encoded_value(T) :: T.
-type decoded_value() :: decoded_value(any()).
-type decoded_value(T) :: T.
-export_type([codec/0]).
-export_type([type_name/0]).
-export_type([encoded_value/0]).
-export_type([encoded_value/1]).
-export_type([decoded_value/0]).
-export_type([decoded_value/1]).
%% Callbacks
-callback unmarshal(type_name(), encoded_value()) ->
decoded_value().
-callback marshal(type_name(), decoded_value()) ->
encoded_value().
%% API
-spec unmarshal(codec(), type_name(), encoded_value()) ->
decoded_value().
unmarshal(Codec, Type, Value) ->
Codec:unmarshal(Type, Value).
-spec marshal(codec(), type_name(), decoded_value()) ->
encoded_value().
marshal(Codec, Type, Value) ->
Codec:marshal(Type, Value).
%% Generic codec
-spec marshal(type_name(), decoded_value()) ->
encoded_value().
marshal({list, T}, V) ->
[marshal(T, E) || E <- V];
marshal({set, T}, V) ->
ordsets:from_list([marshal(T, E) || E <- ordsets:to_list(V)]);
marshal(id, V) ->
marshal(string, V);
marshal(event_id, V) ->
marshal(integer, V);
marshal(provider_id, V) ->
marshal(integer, V);
marshal(terminal_id, V) ->
marshal(integer, V);
marshal(blocking, blocked) ->
blocked;
marshal(blocking, unblocked) ->
unblocked;
marshal(identity_provider, Provider) when is_binary(Provider) ->
Provider;
marshal(transaction_info, TransactionInfo = #{
id := TransactionID,
extra := Extra
}) ->
Timestamp = maps:get(timestamp, TransactionInfo, undefined),
AddInfo = maps:get(additional_info, TransactionInfo, undefined),
#'TransactionInfo'{
id = marshal(id, TransactionID),
timestamp = marshal(timestamp, Timestamp),
extra = Extra,
additional_info = marshal(additional_transaction_info, AddInfo)
};
marshal(additional_transaction_info, AddInfo = #{}) ->
#'AdditionalTransactionInfo'{
rrn = marshal(string, maps:get(rrn, AddInfo, undefined)),
approval_code = marshal(string, maps:get(approval_code, AddInfo, undefined)),
acs_url = marshal(string, maps:get(acs_url, AddInfo, undefined)),
pareq = marshal(string, maps:get(pareq, AddInfo, undefined)),
md = marshal(string, maps:get(md, AddInfo, undefined)),
term_url = marshal(string, maps:get(term_url, AddInfo, undefined)),
pares = marshal(string, maps:get(pares, AddInfo, undefined)),
eci = marshal(string, maps:get(eci, AddInfo, undefined)),
cavv = marshal(string, maps:get(cavv, AddInfo, undefined)),
xid = marshal(string, maps:get(xid, AddInfo, undefined)),
cavv_algorithm = marshal(string, maps:get(cavv_algorithm, AddInfo, undefined)),
three_ds_verification = marshal(
three_ds_verification,
maps:get(three_ds_verification, AddInfo, undefined)
)
};
marshal(three_ds_verification, Value) when
Value =:= authentication_successful orelse
Value =:= attempts_processing_performed orelse
Value =:= authentication_failed orelse
Value =:= authentication_could_not_be_performed
->
Value;
marshal(account_change, {created, Account}) ->
{created, marshal(account, Account)};
marshal(account, #{
id := ID,
identity := IdentityID,
currency := CurrencyID,
accounter_account_id := AAID
}) ->
#'account_Account'{
id = marshal(id, ID),
identity = marshal(id, IdentityID),
currency = marshal(currency_ref, CurrencyID),
accounter_account_id = marshal(event_id, AAID)
};
marshal(resource, {bank_card, #{bank_card := BankCard} = ResourceBankCard}) ->
{bank_card, #'ResourceBankCard'{
bank_card = marshal(bank_card, BankCard),
auth_data = maybe_marshal(bank_card_auth_data, maps:get(auth_data, ResourceBankCard, undefined))
}};
marshal(resource, {crypto_wallet, #{crypto_wallet := CryptoWallet}}) ->
{crypto_wallet, #'ResourceCryptoWallet'{
crypto_wallet = marshal(crypto_wallet, CryptoWallet)
}};
marshal(resource_descriptor, {bank_card, BinDataID}) ->
{bank_card, #'ResourceDescriptorBankCard'{
bin_data_id = marshal(msgpack, BinDataID)
}};
marshal(bank_card, BankCard = #{token := Token}) ->
Bin = maps:get(bin, BankCard, undefined),
PaymentSystem = maps:get(payment_system, BankCard, undefined),
MaskedPan = maps:get(masked_pan, BankCard, undefined),
BankName = maps:get(bank_name, BankCard, undefined),
IsoCountryCode = maps:get(iso_country_code, BankCard, undefined),
CardType = maps:get(card_type, BankCard, undefined),
ExpDate = maps:get(exp_date, BankCard, undefined),
CardholderName = maps:get(cardholder_name, BankCard, undefined),
BinDataID = maps:get(bin_data_id, BankCard, undefined),
#'BankCard'{
token = marshal(string, Token),
bin = marshal(string, Bin),
masked_pan = marshal(string, MaskedPan),
bank_name = marshal(string, BankName),
payment_system = maybe_marshal(payment_system, PaymentSystem),
issuer_country = maybe_marshal(iso_country_code, IsoCountryCode),
card_type = maybe_marshal(card_type, CardType),
exp_date = maybe_marshal(exp_date, ExpDate),
cardholder_name = maybe_marshal(string, CardholderName),
bin_data_id = maybe_marshal(msgpack, BinDataID)
};
marshal(bank_card_auth_data, {session, #{session_id := ID}}) ->
{session_data, #'SessionAuthData'{
id = marshal(string, ID)
}};
marshal(crypto_wallet, #{id := ID, currency := Currency}) ->
#'CryptoWallet'{
id = marshal(string, ID),
currency = marshal(crypto_currency, Currency),
data = marshal(crypto_data, Currency)
};
marshal(exp_date, {Month, Year}) ->
#'BankCardExpDate'{
month = marshal(integer, Month),
year = marshal(integer, Year)
};
marshal(crypto_currency, {Currency, _}) ->
Currency;
marshal(crypto_data, {bitcoin, #{}}) ->
{bitcoin, #'CryptoDataBitcoin'{}};
marshal(crypto_data, {litecoin, #{}}) ->
{litecoin, #'CryptoDataLitecoin'{}};
marshal(crypto_data, {bitcoin_cash, #{}}) ->
{bitcoin_cash, #'CryptoDataBitcoinCash'{}};
marshal(crypto_data, {ethereum, #{}}) ->
{ethereum, #'CryptoDataEthereum'{}};
marshal(crypto_data, {zcash, #{}}) ->
{zcash, #'CryptoDataZcash'{}};
marshal(crypto_data, {usdt, #{}}) ->
{usdt, #'CryptoDataUSDT'{}};
marshal(crypto_data, {ripple, Data}) ->
{ripple, #'CryptoDataRipple'{
tag = maybe_marshal(string, maps:get(tag, Data, undefined))
}};
marshal(payment_system, V) when is_atom(V) ->
V;
marshal(iso_country_code, V) when is_atom(V) ->
V;
marshal(card_type, V) when is_atom(V) ->
V;
marshal(cash, {Amount, CurrencyRef}) ->
#'Cash'{
amount = marshal(amount, Amount),
currency = marshal(currency_ref, CurrencyRef)
};
marshal(cash_range, {{BoundLower, CashLower}, {BoundUpper, CashUpper}}) ->
#'CashRange'{
lower = {BoundLower, marshal(cash, CashLower)},
upper = {BoundUpper, marshal(cash, CashUpper)}
};
marshal(currency_ref, CurrencyID) when is_binary(CurrencyID) ->
#'CurrencyRef'{
symbolic_code = CurrencyID
};
marshal(amount, V) ->
marshal(integer, V);
marshal(event_range, {After, Limit}) ->
#'EventRange'{
'after' = maybe_marshal(integer, After),
limit = maybe_marshal(integer, Limit)
};
marshal(failure, Failure) ->
#'Failure'{
code = marshal(string, wapi_failure:code(Failure)),
reason = maybe_marshal(string, wapi_failure:reason(Failure)),
sub = maybe_marshal(sub_failure, wapi_failure:sub_failure(Failure))
};
marshal(sub_failure, Failure) ->
#'SubFailure'{
code = marshal(string, wapi_failure:code(Failure)),
sub = maybe_marshal(sub_failure, wapi_failure:sub_failure(Failure))
};
marshal(fees, Fees) ->
#'Fees'{
fees = maps:map(fun(_Constant, Value) -> marshal(cash, Value) end, maps:get(fees, Fees))
};
marshal(timestamp, {DateTime, USec}) ->
DateTimeinSeconds = genlib_time:daytime_to_unixtime(DateTime),
{TimeinUnit, Unit} =
case USec of
0 ->
{DateTimeinSeconds, second};
USec ->
MicroSec = erlang:convert_time_unit(DateTimeinSeconds, second, microsecond),
{MicroSec + USec, microsecond}
end,
genlib_rfc3339:format_relaxed(TimeinUnit, Unit);
marshal(timestamp_ms, V) ->
wapi_time:to_rfc3339(V);
marshal(domain_revision, V) when is_integer(V) ->
V;
marshal(party_revision, V) when is_integer(V) ->
V;
marshal(string, V) when is_binary(V) ->
V;
marshal(integer, V) when is_integer(V) ->
V;
marshal(bool, V) when is_boolean(V) ->
V;
marshal(context, Ctx) when is_map(Ctx) ->
maps:map(fun(_NS, V) -> marshal(msgpack, V) end, Ctx);
marshal(msgpack, V) ->
wapi_msgpack_codec:marshal(msgpack, V);
% Catch this up in thrift validation
marshal(_, Other) ->
Other.
-spec unmarshal(type_name(), encoded_value()) ->
decoded_value().
unmarshal({list, T}, V) ->
[marshal(T, E) || E <- V];
unmarshal({set, T}, V) ->
ordsets:from_list([unmarshal(T, E) || E <- ordsets:to_list(V)]);
unmarshal(id, V) ->
unmarshal(string, V);
unmarshal(event_id, V) ->
unmarshal(integer, V);
unmarshal(provider_id, V) ->
unmarshal(integer, V);
unmarshal(terminal_id, V) ->
unmarshal(integer, V);
unmarshal(blocking, blocked) ->
blocked;
unmarshal(blocking, unblocked) ->
unblocked;
unmarshal(transaction_info, #'TransactionInfo'{
id = TransactionID,
timestamp = Timestamp,
extra = Extra,
additional_info = AddInfo
}) ->
genlib_map:compact(#{
id => unmarshal(string, TransactionID),
timestamp => maybe_unmarshal(string, Timestamp),
extra => Extra,
additional_info => maybe_unmarshal(additional_transaction_info, AddInfo)
});
unmarshal(additional_transaction_info, #'AdditionalTransactionInfo'{
rrn = RRN,
approval_code = ApprovalCode,
acs_url = AcsURL,
pareq = Pareq,
md = MD,
term_url = TermURL,
pares = Pares,
eci = ECI,
cavv = CAVV,
xid = XID,
cavv_algorithm = CAVVAlgorithm,
three_ds_verification = ThreeDSVerification
}) ->
genlib_map:compact(#{
rrn => maybe_unmarshal(string, RRN),
approval_code => maybe_unmarshal(string, ApprovalCode),
acs_url => maybe_unmarshal(string, AcsURL),
pareq => maybe_unmarshal(string, Pareq),
md => maybe_unmarshal(string, MD),
term_url => maybe_unmarshal(string, TermURL),
pares => maybe_unmarshal(string, Pares),
eci => maybe_unmarshal(string, ECI),
cavv => maybe_unmarshal(string, CAVV),
xid => maybe_unmarshal(string, XID),
cavv_algorithm => maybe_unmarshal(string, CAVVAlgorithm),
three_ds_verification => maybe_unmarshal(three_ds_verification, ThreeDSVerification)
});
unmarshal(three_ds_verification, Value) when
Value =:= authentication_successful orelse
Value =:= attempts_processing_performed orelse
Value =:= authentication_failed orelse
Value =:= authentication_could_not_be_performed
->
Value;
unmarshal(complex_action, #ff_repairer_ComplexAction{
timer = TimerAction,
remove = RemoveAction
}) ->
unmarshal(timer_action, TimerAction) ++ unmarshal(remove_action, RemoveAction);
unmarshal(timer_action, undefined) ->
[];
unmarshal(timer_action, {set_timer, SetTimer}) ->
[{set_timer, unmarshal(set_timer_action, SetTimer)}];
unmarshal(timer_action, {unset_timer, #ff_repairer_UnsetTimerAction{}}) ->
[unset_timer];
unmarshal(remove_action, undefined) ->
[];
unmarshal(remove_action, #ff_repairer_RemoveAction{}) ->
[remove];
unmarshal(set_timer_action, {timeout, Timeout}) ->
{timeout, unmarshal(integer, Timeout)};
unmarshal(set_timer_action, {deadline, Deadline}) ->
{deadline, unmarshal(timestamp, Deadline)};
unmarshal(account_change, {created, Account}) ->
{created, unmarshal(account, Account)};
unmarshal(account, #'account_Account'{
id = ID,
identity = IdentityID,
currency = CurrencyRef,
accounter_account_id = AAID
}) ->
#{
id => unmarshal(id, ID),
identity => unmarshal(id, IdentityID),
currency => unmarshal(currency_ref, CurrencyRef),
accounter_account_id => unmarshal(accounter_account_id, AAID)
};
unmarshal(accounter_account_id, V) ->
unmarshal(integer, V);
unmarshal(resource, {bank_card, #'ResourceBankCard'{
bank_card = BankCard,
auth_data = AuthData
}}) ->
{bank_card, genlib_map:compact(#{
bank_card => unmarshal(bank_card, BankCard),
auth_data => maybe_unmarshal(bank_card_auth_data, AuthData)
})};
unmarshal(resource, {crypto_wallet, #'ResourceCryptoWallet'{crypto_wallet = CryptoWallet}}) ->
{crypto_wallet, #{
crypto_wallet => unmarshal(crypto_wallet, CryptoWallet)
}};
unmarshal(resource_descriptor, {bank_card, BankCard}) ->
{bank_card, unmarshal(msgpack, BankCard#'ResourceDescriptorBankCard'.bin_data_id)};
unmarshal(bank_card_auth_data, {session_data, #'SessionAuthData'{id = ID}}) ->
{session, #{
session_id => unmarshal(string, ID)
}};
unmarshal(bank_card, #'BankCard'{
token = Token,
bin = Bin,
masked_pan = MaskedPan,
bank_name = BankName,
payment_system = PaymentSystem,
issuer_country = IsoCountryCode,
card_type = CardType,
bin_data_id = BinDataID,
exp_date = ExpDate,
cardholder_name = CardholderName
}) ->
genlib_map:compact(#{
token => unmarshal(string, Token),
payment_system => maybe_unmarshal(payment_system, PaymentSystem),
bin => maybe_unmarshal(string, Bin),
masked_pan => maybe_unmarshal(string, MaskedPan),
bank_name => maybe_unmarshal(string, BankName),
iso_country_code => maybe_unmarshal(iso_country_code, IsoCountryCode),
card_type => maybe_unmarshal(card_type, CardType),
exp_date => maybe_unmarshal(exp_date, ExpDate),
cardholder_name => maybe_unmarshal(string, CardholderName),
bin_data_id => maybe_unmarshal(msgpack, BinDataID)
});
unmarshal(exp_date, #'BankCardExpDate'{
month = Month,
year = Year
}) ->
{unmarshal(integer, Month), unmarshal(integer, Year)};
unmarshal(payment_system, V) when is_atom(V) ->
V;
unmarshal(iso_country_code, V) when is_atom(V) ->
V;
unmarshal(card_type, V) when is_atom(V) ->
V;
unmarshal(crypto_wallet, #'CryptoWallet'{
id = CryptoWalletID,
currency = CryptoWalletCurrency,
data = Data
}) ->
genlib_map:compact(#{
id => unmarshal(string, CryptoWalletID),
currency => {CryptoWalletCurrency, unmarshal(crypto_data, Data)}
});
unmarshal(crypto_data, {ripple, #'CryptoDataRipple'{tag = Tag}}) ->
genlib_map:compact(#{
tag => maybe_unmarshal(string, Tag)
});
unmarshal(crypto_data, _) ->
#{};
unmarshal(cash, #'Cash'{
amount = Amount,
currency = CurrencyRef
}) ->
{unmarshal(amount, Amount), unmarshal(currency_ref, CurrencyRef)};
unmarshal(cash_range, #'CashRange'{
lower = {BoundLower, CashLower},
upper = {BoundUpper, CashUpper}
}) ->
{
{BoundLower, unmarshal(cash, CashLower)},
{BoundUpper, unmarshal(cash, CashUpper)}
};
unmarshal(currency_ref, #'CurrencyRef'{
symbolic_code = SymbolicCode
}) ->
unmarshal(string, SymbolicCode);
unmarshal(amount, V) ->
unmarshal(integer, V);
unmarshal(event_range, #'EventRange'{'after' = After, limit = Limit}) ->
{maybe_unmarshal(integer, After), maybe_unmarshal(integer, Limit)};
unmarshal(failure, Failure) ->
genlib_map:compact(#{
code => unmarshal(string, Failure#'Failure'.code),
reason => maybe_unmarshal(string, Failure#'Failure'.reason),
sub => maybe_unmarshal(sub_failure, Failure#'Failure'.sub)
});
unmarshal(sub_failure, Failure) ->
genlib_map:compact(#{
code => unmarshal(string, Failure#'SubFailure'.code),
sub => maybe_unmarshal(sub_failure, Failure#'SubFailure'.sub)
});
unmarshal(range, #evsink_EventRange{
'after' = Cursor,
limit = Limit
}) ->
{Cursor, Limit, forward};
unmarshal(fees, Fees) ->
#{
fees => maps:map(fun(_Constant, Value) -> unmarshal(cash, Value) end, Fees#'Fees'.fees)
};
unmarshal(timestamp_ms, V) ->
wapi_time:from_rfc3339(V);
unmarshal(domain_revision, V) when is_integer(V) ->
V;
unmarshal(party_revision, V) when is_integer(V) ->
V;
unmarshal(string, V) when is_binary(V) ->
V;
unmarshal(integer, V) when is_integer(V) ->
V;
unmarshal(context, Ctx) when is_map(Ctx) ->
maps:map(fun(_K, V) -> unmarshal(msgpack, V) end, Ctx);
unmarshal(msgpack, V) ->
wapi_msgpack_codec:unmarshal(msgpack, V);
unmarshal(range, #'EventRange'{
'after' = Cursor,
limit = Limit
}) ->
{Cursor, Limit, forward};
unmarshal(bool, V) when is_boolean(V) ->
V.
maybe_unmarshal(_Type, undefined) ->
undefined;
maybe_unmarshal(Type, Value) ->
unmarshal(Type, Value).
maybe_marshal(_Type, undefined) ->
undefined;
maybe_marshal(Type, Value) ->
marshal(Type, Value).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-spec test() -> _.
-spec bank_card_codec_test() -> _.
bank_card_codec_test() ->
BankCard = #{
token => <<"token">>,
payment_system => visa,
bin => <<"12345">>,
masked_pan => <<"7890">>,
bank_name => <<"bank">>,
iso_country_code => zmb,
card_type => credit_or_debit,
exp_date => {12, 3456},
cardholder_name => <<"name">>,
bin_data_id => #{<<"foo">> => 1}
},
Type = {struct, struct, {ff_proto_base_thrift, 'BankCard'}},
Binary = wapi_thrift_utils:serialize(Type, marshal(bank_card, BankCard)),
Decoded = wapi_thrift_utils:deserialize(Type, Binary),
?assertEqual(BankCard, unmarshal(bank_card, Decoded)).
-spec fees_codec_test() -> _.
fees_codec_test() ->
Expected = #{
fees => #{
operation_amount => {100, <<"RUB">>},
surplus => {200, <<"RUB">>}
}
},
Type = {struct, struct, {ff_proto_base_thrift, 'Fees'}},
Binary = wapi_thrift_utils:serialize(Type, marshal(fees, Expected)),
Decoded = wapi_thrift_utils:deserialize(Type, Binary),
?assertEqual(Expected, unmarshal(fees, Decoded)).
-endif.

View File

@ -119,7 +119,7 @@ when Type =:= <<"BankCardDestinationResource">> ->
cardholder_name => BankCard#'BankCard'.cardholder_name,
exp_date => {Month, Year}
}}},
{ok, ff_codec:marshal(resource, CostructedResource)};
{ok, wapi_codec:marshal(resource, CostructedResource)};
{error, {decryption_failed, _} = Error} ->
logger:warning("Resource token decryption failed: ~p", [Error]),
{error, invalid_resource_token}
@ -133,7 +133,7 @@ when Type =:= <<"CryptoWalletDestinationResource">> ->
id => CryptoWalletID,
currency => marshal_crypto_currency_data(Resource)
})}},
{ok, ff_codec:marshal(resource, CostructedResource)}.
{ok, wapi_codec:marshal(resource, CostructedResource)}.
service_call(Params, Context) ->
wapi_handler_utils:service_call(Params, Context).
@ -167,13 +167,13 @@ marshal(resource, #{
bin => maps:get(<<"bin">>, BankCard),
masked_pan => maps:get(<<"lastDigits">>, BankCard)
}}},
ff_codec:marshal(resource, Resource);
wapi_codec:marshal(resource, Resource);
marshal(context, Context) ->
ff_codec:marshal(context, Context);
wapi_codec:marshal(context, Context);
marshal(T, V) ->
ff_codec:marshal(T, V).
wapi_codec:marshal(T, V).
maybe_marshal(_, undefined) ->
undefined;
@ -243,10 +243,10 @@ unmarshal(resource, {crypto_wallet, #'ResourceCryptoWallet'{crypto_wallet = #'Cr
});
unmarshal(context, Context) ->
ff_codec:unmarshal(context, Context);
wapi_codec:unmarshal(context, Context);
unmarshal(T, V) ->
ff_codec:unmarshal(T, V).
wapi_codec:unmarshal(T, V).
maybe_unmarshal(_, undefined) ->
undefined;

View File

@ -69,10 +69,10 @@ process_request(Tag, OperationID, Req, SwagContext, Opts, WoodyContext) ->
_ = logger:info("Processing request ~p", [OperationID]),
try
%% TODO remove this fistful specific step, when separating the wapi service.
ok = ff_context:save(create_ff_context(WoodyContext, Opts)),
ok = wapi_context:save(create_wapi_context(WoodyContext, Opts)),
Context = create_handler_context(SwagContext, WoodyContext),
Handler = get_handler(Tag, genlib_app:env(?APP, transport)),
Handler = get_handler(Tag),
case wapi_auth:authorize_operation(OperationID, Req, Context) of
ok ->
ok = logger:debug("Operation ~p authorized", [OperationID]),
@ -87,7 +87,7 @@ process_request(Tag, OperationID, Req, SwagContext, Opts, WoodyContext) ->
error:{woody_error, {Source, Class, Details}} ->
process_woody_error(Source, Class, Details)
after
ff_context:cleanup()
wapi_context:cleanup()
end.
-spec throw_result(request_result()) ->
@ -95,9 +95,8 @@ process_request(Tag, OperationID, Req, SwagContext, Opts, WoodyContext) ->
throw_result(Res) ->
erlang:throw({?request_result, Res}).
get_handler(wallet, thrift) -> wapi_wallet_thrift_handler;
get_handler(wallet, _) -> wapi_wallet_handler;
get_handler(payres, _) -> wapi_payres_handler.
get_handler(wallet) -> wapi_wallet_handler;
get_handler(payres) -> wapi_payres_handler.
-spec create_woody_context(tag(), req_data(), wapi_auth:context(), opts()) ->
woody_context:ctx().
@ -142,11 +141,11 @@ process_woody_error(_Source, resource_unavailable, _Details) ->
process_woody_error(_Source, result_unknown, _Details) ->
wapi_handler_utils:reply_error(504).
-spec create_ff_context(woody_context:ctx(), opts()) ->
ff_context:context().
create_ff_context(WoodyContext, Opts) ->
-spec create_wapi_context(woody_context:ctx(), opts()) ->
wapi_context:context().
create_wapi_context(WoodyContext, Opts) ->
ContextOptions = #{
woody_context => WoodyContext,
party_client => maps:get(party_client, Opts)
},
ff_context:create(ContextOptions).
wapi_context:create(ContextOptions).

View File

@ -355,7 +355,7 @@ marshal(event_range, {Cursor, Limit}) ->
};
marshal(context, Ctx) ->
ff_codec:marshal(context, Ctx);
wapi_codec:marshal(context, Ctx);
marshal(proof_type, <<"RUSDomesticPassport">>) ->
rus_domestic_passport;
@ -363,7 +363,7 @@ marshal(proof_type, <<"RUSRetireeInsuranceCertificate">>) ->
rus_retiree_insurance_cert;
marshal(T, V) ->
ff_codec:marshal(T, V).
wapi_codec:marshal(T, V).
%%
@ -463,7 +463,7 @@ unmarshal(blocking, blocked) ->
true;
unmarshal(T, V) ->
ff_codec:unmarshal(T, V).
wapi_codec:unmarshal(T, V).
maybe_unmarshal(_, undefined) ->
undefined;

View File

@ -0,0 +1,49 @@
-module(wapi_msgpack_codec).
-include_lib("fistful_proto/include/ff_proto_msgpack_thrift.hrl").
-export([unmarshal/2]).
-export([marshal/2]).
%% Types
-type type_name() :: msgpack.
-type decoded_value() ::
#{decoded_value() => decoded_value()} |
[decoded_value()] |
integer() |
float() |
binary() |
{binary, binary()} |
nil.
-type encoded_value() :: ff_proto_msgpack_thrift:'Value'().
-export_type([type_name/0]).
-export_type([encoded_value/0]).
-export_type([decoded_value/0]).
-spec marshal(type_name(), decoded_value()) ->
encoded_value().
marshal(msgpack, nil) -> {nl, #msgp_Nil{}};
marshal(msgpack, V) when is_boolean(V) -> {b, V};
marshal(msgpack, V) when is_integer(V) -> {i, V};
marshal(msgpack, V) when is_float(V) -> V;
marshal(msgpack, V) when is_binary(V) -> {str, V}; % Assuming well-formed UTF-8 bytestring.
marshal(msgpack, {binary, V}) when is_binary(V) ->
{bin, V};
marshal(msgpack, V) when is_list(V) ->
{arr, [marshal(msgpack, ListItem) || ListItem <- V]};
marshal(msgpack, V) when is_map(V) ->
{obj, maps:fold(fun(Key, Value, Map) -> Map#{marshal(msgpack, Key) => marshal(msgpack, Value)} end, #{}, V)}.
-spec unmarshal(type_name(), encoded_value()) ->
decoded_value().
unmarshal(msgpack, {nl, #msgp_Nil{}}) -> nil;
unmarshal(msgpack, {b, V}) when is_boolean(V) -> V;
unmarshal(msgpack, {i, V}) when is_integer(V) -> V;
unmarshal(msgpack, {flt, V}) when is_float(V) -> V;
unmarshal(msgpack, {str, V}) when is_binary(V) -> V; % Assuming well-formed UTF-8 bytestring.
unmarshal(msgpack, {bin, V}) when is_binary(V) -> {binary, V};
unmarshal(msgpack, {arr, V}) when is_list(V) -> [unmarshal(msgpack, ListItem) || ListItem <- V];
unmarshal(msgpack, {obj, V}) when is_map(V) ->
maps:fold(fun(Key, Value, Map) -> Map#{unmarshal(msgpack, Key) => unmarshal(msgpack, Value)} end, #{}, V).

View File

@ -222,7 +222,7 @@ unmarshal_response(identities, Response) ->
<<"id">> => Response#fistfulstat_StatIdentity.id,
<<"name">> => Response#fistfulstat_StatIdentity.name,
<<"createdAt">> => Response#fistfulstat_StatIdentity.created_at,
<<"provider">> => Response#fistfulstat_StatIdentity.provider,
<<"provider">> => unmarshal_identity_stat_provider(Response#fistfulstat_StatIdentity.provider),
<<"class">> => Response#fistfulstat_StatIdentity.identity_class,
<<"level">> => Response#fistfulstat_StatIdentity.identity_level,
<<"effectiveChallenge">> => Response#fistfulstat_StatIdentity.effective_challenge,
@ -294,3 +294,6 @@ unmarshal_crypto_currency_name({ripple, _}) -> <<"Ripple">>;
unmarshal_crypto_currency_name({ethereum, _}) -> <<"Ethereum">>;
unmarshal_crypto_currency_name({usdt, _}) -> <<"USDT">>;
unmarshal_crypto_currency_name({zcash, _}) -> <<"Zcash">>.
unmarshal_identity_stat_provider(Provider) ->
genlib:to_binary(Provider).

View File

@ -238,7 +238,7 @@ clamp_max_request_deadline(Value) when is_integer(Value)->
-spec get_unique_id() -> binary().
get_unique_id() ->
ff_id:generate_snowflake_id().
wapi_id:generate_snowflake_id().
%%
-ifdef(TEST).
@ -273,25 +273,25 @@ get_path_test() ->
-spec mask_test() -> _.
mask_test() ->
?assertEqual(<<"Жур">>, mask(leading, 0, $*, <<"Жур">>)),
?assertEqual(<<"*ур">>, mask(leading, 1, $*, <<"Жур">>)),
?assertEqual(<<"**р">>, mask(leading, 2, $*, <<"Жур">>)),
?assertEqual(<<"***">>, mask(leading, 3, $*, <<"Жур">>)),
?assertEqual(<<"Жур">>, mask(trailing, 0, $*, <<"Жур">>)),
?assertEqual(<<"Жу*">>, mask(trailing, 1, $*, <<"Жур">>)),
?assertEqual(<<"Ж**">>, mask(trailing, 2, $*, <<"Жур">>)),
?assertEqual(<<"***">>, mask(trailing, 3, $*, <<"Жур">>)).
?assertEqual(<<"Хуй">>, mask(leading, 0, $*, <<"Хуй">>)),
?assertEqual(<<"*уй">>, mask(leading, 1, $*, <<"Хуй">>)),
?assertEqual(<<"**й">>, mask(leading, 2, $*, <<"Хуй">>)),
?assertEqual(<<"***">>, mask(leading, 3, $*, <<"Хуй">>)),
?assertEqual(<<"Хуй">>, mask(trailing, 0, $*, <<"Хуй">>)),
?assertEqual(<<"Ху*">>, mask(trailing, 1, $*, <<"Хуй">>)),
?assertEqual(<<"Х**">>, mask(trailing, 2, $*, <<"Хуй">>)),
?assertEqual(<<"***">>, mask(trailing, 3, $*, <<"Хуй">>)).
-spec mask_and_keep_test() -> _.
mask_and_keep_test() ->
?assertEqual(<<"***">>, mask_and_keep(leading, 0, $*, <<"Жур">>)),
?assertEqual(<<"Ж**">>, mask_and_keep(leading, 1, $*, <<"Жур">>)),
?assertEqual(<<"Жу*">>, mask_and_keep(leading, 2, $*, <<"Жур">>)),
?assertEqual(<<"Жур">>, mask_and_keep(leading, 3, $*, <<"Жур">>)),
?assertEqual(<<"***">>, mask_and_keep(trailing, 0, $*, <<"Жур">>)),
?assertEqual(<<"**р">>, mask_and_keep(trailing, 1, $*, <<"Жур">>)),
?assertEqual(<<"*ур">>, mask_and_keep(trailing, 2, $*, <<"Жур">>)),
?assertEqual(<<"Жур">>, mask_and_keep(trailing, 3, $*, <<"Жур">>)).
?assertEqual(<<"***">>, mask_and_keep(leading, 0, $*, <<"Хуй">>)),
?assertEqual(<<"Х**">>, mask_and_keep(leading, 1, $*, <<"Хуй">>)),
?assertEqual(<<"Ху*">>, mask_and_keep(leading, 2, $*, <<"Хуй">>)),
?assertEqual(<<"Хуй">>, mask_and_keep(leading, 3, $*, <<"Хуй">>)),
?assertEqual(<<"***">>, mask_and_keep(trailing, 0, $*, <<"Хуй">>)),
?assertEqual(<<"**й">>, mask_and_keep(trailing, 1, $*, <<"Хуй">>)),
?assertEqual(<<"*уй">>, mask_and_keep(trailing, 2, $*, <<"Хуй">>)),
?assertEqual(<<"Хуй">>, mask_and_keep(trailing, 3, $*, <<"Хуй">>)).
-spec parse_deadline_test() -> _.
parse_deadline_test() ->

View File

@ -113,10 +113,10 @@ marshal(body, #{
};
marshal(context, Ctx) ->
ff_codec:marshal(context, Ctx);
wapi_codec:marshal(context, Ctx);
marshal(T, V) ->
ff_codec:marshal(T, V).
wapi_codec:marshal(T, V).
unmarshal(transfer, #w2w_transfer_W2WTransferState{
id = ID,
@ -157,7 +157,7 @@ unmarshal(transfer_status, {failed, #w2w_status_Failed{failure = Failure}}) ->
};
unmarshal(T, V) ->
ff_codec:unmarshal(T, V).
wapi_codec:unmarshal(T, V).
maybe_unmarshal(_T, undefined) ->
undefined;

View File

@ -141,10 +141,10 @@ marshal(account_params, {IdentityID, CurrencyID}) ->
};
marshal(context, Ctx) ->
ff_codec:marshal(context, Ctx);
wapi_codec:marshal(context, Ctx);
marshal(T, V) ->
ff_codec:marshal(T, V).
wapi_codec:marshal(T, V).
%%
@ -197,10 +197,10 @@ unmarshal(wallet_account_balance, #account_AccountBalance{
};
unmarshal(context, Ctx) ->
ff_codec:unmarshal(context, Ctx);
wapi_codec:unmarshal(context, Ctx);
unmarshal(T, V) ->
ff_codec:unmarshal(T, V).
wapi_codec:unmarshal(T, V).
maybe_unmarshal(_, undefined) ->
undefined;

View File

@ -25,13 +25,13 @@
-spec authorize_api_key(operation_id(), api_key(), handler_opts()) ->
false | {true, wapi_auth:context()}.
authorize_api_key(OperationID, ApiKey, _Opts) ->
ok = scoper:add_scope('swag.server', #{api => wallet, operation_id => OperationID}),
ok = scoper:add_meta(#{api => wallet, operation_id => OperationID}),
case uac:authorize_api_key(ApiKey, wapi_auth:get_verification_options()) of
{ok, Context0} ->
Context = wapi_auth:create_wapi_context(Context0),
{true, Context};
{error, Error} ->
_ = logger:info("API Key authorization failed for ~p due to ~p", [OperationID, Error]),
_ = logger:info("API Key authorization failed: ~p", [Error]),
false
end.
@ -40,50 +40,8 @@ authorize_api_key(OperationID, ApiKey, _Opts) ->
handle_request(OperationID, Req, SwagContext, Opts) ->
wapi_handler:handle_request(wallet, OperationID, Req, SwagContext, Opts).
%% Providers
-spec process_request(operation_id(), req_data(), handler_context(), handler_opts()) ->
request_result().
process_request('ListProviders', #{'residence' := Residence}, Context, _Opts) ->
Providers = wapi_wallet_ff_backend:get_providers(ff_maybe:to_list(Residence), Context),
wapi_handler_utils:reply_ok(200, Providers);
process_request('GetProvider', #{'providerID' := Id}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_provider(Id, Context) of
{ok, Provider} -> wapi_handler_utils:reply_ok(200, Provider);
{error, notfound} -> wapi_handler_utils:reply_ok(404)
end;
process_request('ListProviderIdentityClasses', #{'providerID' := Id}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_provider_identity_classes(Id, Context) of
{ok, Classes} -> wapi_handler_utils:reply_ok(200, Classes);
{error, notfound} -> wapi_handler_utils:reply_ok(404)
end;
process_request('GetProviderIdentityClass', #{
'providerID' := ProviderId,
'identityClassID' := ClassId
}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_provider_identity_class(ProviderId, ClassId, Context) of
{ok, Class} -> wapi_handler_utils:reply_ok(200, Class);
{error, notfound} -> wapi_handler_utils:reply_ok(404)
end;
process_request('ListProviderIdentityLevels', #{
'providerID' := _ProviderId,
'identityClassID' := _ClassId
}, _Context, _Opts) ->
%% case wapi_wallet_ff_backend:get_provider_identity_class_levels(ProviderId, ClassId, Context) of
%% {ok, Levels} -> wapi_handler_utils:reply_ok(200, Levels);
%% {error, notfound} -> wapi_handler_utils:reply_ok(404)
%% end;
not_implemented();
process_request('GetProviderIdentityLevel', #{
'providerID' := _ProviderId,
'identityClassID' := _ClassId,
'identityLevelID' := _LevelId
}, _Context, _Opts) ->
%% case wapi_wallet_ff_backend:get_provider_identity_class_level(ProviderId, ClassId, LevelId, Context) of
%% {ok, Level} -> wapi_handler_utils:reply_ok(200, Level);
%% {error, notfound} -> wapi_handler_utils:reply_ok(404)
%% end;
not_implemented();
%% Identities
process_request('ListIdentities', Params, Context, _Opts) ->
@ -101,32 +59,26 @@ process_request('ListIdentities', Params, Context, _Opts) ->
})
end;
process_request('GetIdentity', #{'identityID' := IdentityId}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_identity(IdentityId, Context) of
case wapi_identity_backend:get_identity(IdentityId, Context) of
{ok, Identity} -> wapi_handler_utils:reply_ok(200, Identity);
{error, {identity, notfound}} -> wapi_handler_utils:reply_ok(404);
{error, {identity, unauthorized}} -> wapi_handler_utils:reply_ok(404)
end;
process_request('CreateIdentity', #{'Identity' := Params}, Context, Opts) ->
case wapi_wallet_ff_backend:create_identity(Params, Context) of
case wapi_identity_backend:create_identity(Params, Context) of
{ok, Identity = #{<<"id">> := IdentityId}} ->
wapi_handler_utils:reply_ok(201, Identity, get_location('GetIdentity', [IdentityId], Opts));
{error, {inaccessible, _}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Party inaccessible">>));
{error, {provider, notfound}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such provider">>));
{error, {identity_class, notfound}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such identity class">>));
{error, {external_id_conflict, ID, ExternalID}} ->
wapi_handler_utils:logic_error(external_id_conflict, {ID, ExternalID});
{error, {email, notfound}} ->
wapi_handler_utils:reply_error(400, #{
<<"errorType">> => <<"NotFound">>,
<<"name">> => <<"email">>,
<<"description">> => <<"No email in JWT">>
})
{error, inaccessible} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Identity inaccessible">>));
{error, {external_id_conflict, ID}} ->
wapi_handler_utils:reply_error(409, #{<<"id">> => ID})
end;
process_request('ListIdentityChallenges', #{'identityID' := Id, 'status' := Status}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_identity_challenges(Id, ff_maybe:to_list(Status), Context) of
case wapi_identity_backend:get_identity_challenges(Id, Status, Context) of
{ok, Challenges} -> wapi_handler_utils:reply_ok(200, Challenges);
{error, {identity, notfound}} -> wapi_handler_utils:reply_ok(404);
{error, {identity, unauthorized}} -> wapi_handler_utils:reply_ok(404)
@ -135,7 +87,7 @@ process_request('StartIdentityChallenge', #{
'identityID' := IdentityId,
'IdentityChallenge' := Params
}, Context, Opts) ->
case wapi_wallet_ff_backend:create_identity_challenge(IdentityId, Params, Context) of
case wapi_identity_backend:create_identity_challenge(IdentityId, Params, Context) of
{ok, Challenge = #{<<"id">> := ChallengeId}} ->
wapi_handler_utils:reply_ok(202, Challenge, get_location('GetIdentityChallenge', [ChallengeId], Opts));
{error, {identity, notfound}} ->
@ -144,7 +96,9 @@ process_request('StartIdentityChallenge', #{
wapi_handler_utils:reply_ok(404);
{error, {challenge, conflict}} ->
wapi_handler_utils:reply_ok(409);
{error, {challenge, {pending, _}}} ->
{error, {external_id_conflict, ID}} ->
wapi_handler_utils:reply_ok(409, #{<<"id">> => ID});
{error, {challenge, pending}} ->
wapi_handler_utils:reply_ok(409);
{error, {challenge, {class, notfound}}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such challenge type">>));
@ -152,7 +106,7 @@ process_request('StartIdentityChallenge', #{
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Proof not found">>));
{error, {challenge, {proof, insufficient}}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Insufficient proof">>));
{error, {challenge, {level, _}}} ->
{error, {challenge, level}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Illegal identification type for current identity level">>)
)
@ -162,20 +116,20 @@ process_request('GetIdentityChallenge', #{
'identityID' := IdentityId,
'challengeID' := ChallengeId
}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_identity_challenge(IdentityId, ChallengeId, Context) of
case wapi_identity_backend:get_identity_challenge(IdentityId, ChallengeId, Context) of
{ok, Challenge} -> wapi_handler_utils:reply_ok(200, Challenge);
{error, {identity, notfound}} -> wapi_handler_utils:reply_ok(404);
{error, {identity, unauthorized}} -> wapi_handler_utils:reply_ok(404);
{error, {challenge, notfound}} -> wapi_handler_utils:reply_ok(404)
end;
process_request('PollIdentityChallengeEvents', Params, Context, _Opts) ->
case wapi_wallet_ff_backend:get_identity_challenge_events(Params, Context) of
case wapi_identity_backend:get_identity_challenge_events(Params, Context) of
{ok, Events} -> wapi_handler_utils:reply_ok(200, Events);
{error, {identity, notfound}} -> wapi_handler_utils:reply_ok(404);
{error, {identity, unauthorized}} -> wapi_handler_utils:reply_ok(404)
end;
process_request('GetIdentityChallengeEvent', Params, Context, _Opts) ->
case wapi_wallet_ff_backend:get_identity_challenge_event(Params, Context) of
case wapi_identity_backend:get_identity_challenge_event(Params, Context) of
{ok, Event} -> wapi_handler_utils:reply_ok(200, Event);
{error, {identity, notfound}} -> wapi_handler_utils:reply_ok(404);
{error, {identity, unauthorized}} -> wapi_handler_utils:reply_ok(404);
@ -183,25 +137,36 @@ process_request('GetIdentityChallengeEvent', Params, Context, _Opts) ->
end;
%% Wallets
process_request('ListWallets', Params, Context, _Opts) ->
case wapi_wallet_ff_backend:list_wallets(Params, Context) of
{ok, {200, _, List}} -> wapi_handler_utils:reply_ok(200, List);
{error, {Code, _, Error}} -> wapi_handler_utils:reply_error(Code, Error)
case wapi_stat_backend:list_wallets(Params, Context) of
{ok, List} -> wapi_handler_utils:reply_ok(200, List);
{error, {invalid, Errors}} ->
wapi_handler_utils:reply_error(400, #{
<<"errorType">> => <<"NoMatch">>,
<<"description">> => Errors
});
{error, {bad_token, Reason}} ->
wapi_handler_utils:reply_error(400, #{
<<"errorType">> => <<"InvalidToken">>,
<<"description">> => Reason
})
end;
process_request('GetWallet', #{'walletID' := WalletId}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_wallet(WalletId, Context) of
case wapi_wallet_backend:get(WalletId, Context) of
{ok, Wallet} -> wapi_handler_utils:reply_ok(200, Wallet);
{error, {wallet, notfound}} -> wapi_handler_utils:reply_ok(404);
{error, {wallet, unauthorized}} -> wapi_handler_utils:reply_ok(404)
end;
process_request('GetWalletByExternalID', #{externalID := ExternalID}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_wallet_by_external_id(ExternalID, Context) of
case wapi_wallet_backend:get_by_external_id(ExternalID, Context) of
{ok, Wallet} -> wapi_handler_utils:reply_ok(200, Wallet);
{error, {wallet, notfound}} -> wapi_handler_utils:reply_ok(404);
{error, {wallet, unauthorized}} -> wapi_handler_utils:reply_ok(404)
{error, {wallet, unauthorized}} -> wapi_handler_utils:reply_ok(404);
{error, {external_id, {unknown_external_id, ExternalID}}} -> wapi_handler_utils:reply_ok(404)
end;
process_request('CreateWallet', #{'Wallet' := Params}, Context, Opts) ->
case wapi_wallet_ff_backend:create_wallet(Params, Context) of
case wapi_wallet_backend:create(Params, Context) of
{ok, Wallet = #{<<"id">> := WalletId}} ->
wapi_handler_utils:reply_ok(201, Wallet, get_location('GetWallet', [WalletId], Opts));
{error, {identity, unauthorized}} ->
@ -210,46 +175,20 @@ process_request('CreateWallet', #{'Wallet' := Params}, Context, Opts) ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such identity">>));
{error, {currency, notfound}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Currency not supported">>));
{error, {inaccessible, _}} ->
{error, inaccessible} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Identity inaccessible">>));
{error, {external_id_conflict, ID, ExternalID}} ->
wapi_handler_utils:logic_error(external_id_conflict, {ID, ExternalID});
{error, invalid} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Invalid currency">>));
{error, {terms, {terms_violation, {not_allowed_currency, _Data}}}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Currency not allowed">>))
{error, {external_id_conflict, ID}} ->
wapi_handler_utils:reply_error(409, #{<<"id">> => ID})
end;
process_request('GetWalletAccount', #{'walletID' := WalletId}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_wallet_account(WalletId, Context) of
case wapi_wallet_backend:get_account(WalletId, Context) of
{ok, WalletAccount} -> wapi_handler_utils:reply_ok(200, WalletAccount);
{error, {wallet, notfound}} -> wapi_handler_utils:reply_ok(404);
{error, {wallet, unauthorized}} -> wapi_handler_utils:reply_ok(404)
end;
process_request('IssueWalletGrant', #{
'walletID' := WalletId,
'WalletGrantRequest' := #{<<"validUntil">> := Expiration, <<"asset">> := Asset}
}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_wallet(WalletId, Context) of
{ok, _} ->
case wapi_backend_utils:issue_grant_token({wallets, WalletId, Asset}, Expiration, Context) of
{ok, Token} ->
wapi_handler_utils:reply_ok(201, #{
<<"token">> => Token,
<<"validUntil">> => Expiration,
<<"asset">> => Asset
});
{error, expired} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Invalid expiration: already expired">>)
)
end;
{error, {wallet, notfound}} ->
wapi_handler_utils:reply_ok(404);
{error, {wallet, unauthorized}} ->
wapi_handler_utils:reply_ok(404)
end;
%% Withdrawals
%% Destinations
process_request('ListDestinations', Params, Context, _Opts) ->
case wapi_stat_backend:list_destinations(Params, Context) of
{ok, Result} -> wapi_handler_utils:reply_ok(200, Result);
@ -265,13 +204,13 @@ process_request('ListDestinations', Params, Context, _Opts) ->
})
end;
process_request('GetDestination', #{'destinationID' := DestinationId}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_destination(DestinationId, Context) of
case wapi_destination_backend:get(DestinationId, Context) of
{ok, Destination} -> wapi_handler_utils:reply_ok(200, Destination);
{error, {destination, notfound}} -> wapi_handler_utils:reply_ok(404);
{error, {destination, unauthorized}} -> wapi_handler_utils:reply_ok(404)
end;
process_request('GetDestinationByExternalID', #{'externalID' := ExternalID}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_destination_by_external_id(ExternalID, Context) of
case wapi_destination_backend:get_by_external_id(ExternalID, Context) of
{ok, Destination} ->
wapi_handler_utils:reply_ok(200, Destination);
{error, {external_id, {unknown_external_id, ExternalID}}} ->
@ -282,7 +221,7 @@ process_request('GetDestinationByExternalID', #{'externalID' := ExternalID}, Con
wapi_handler_utils:reply_ok(404)
end;
process_request('CreateDestination', #{'Destination' := Params}, Context, Opts) ->
case wapi_wallet_ff_backend:create_destination(Params, Context) of
case wapi_destination_backend:create(Params, Context) of
{ok, Destination = #{<<"id">> := DestinationId}} ->
wapi_handler_utils:reply_ok(201, Destination, get_location('GetDestination', [DestinationId], Opts));
{error, {identity, unauthorized}} ->
@ -291,16 +230,14 @@ process_request('CreateDestination', #{'Destination' := Params}, Context, Opts)
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such identity">>));
{error, {currency, notfound}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Currency not supported">>));
{error, {inaccessible, _}} ->
{error, inaccessible} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Identity inaccessible">>));
{error, {external_id_conflict, ID, ExternalID}} ->
{error, {external_id_conflict, {ID, ExternalID}}} ->
wapi_handler_utils:logic_error(external_id_conflict, {ID, ExternalID});
{error, invalid} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Invalid currency">>));
{error, {invalid_resource_token, Type}} ->
{error, invalid_resource_token} ->
wapi_handler_utils:reply_error(400, #{
<<"errorType">> => <<"InvalidResourceToken">>,
<<"name">> => Type,
<<"name">> => <<"BankCardDestinationResource">>,
<<"description">> => <<"Specified resource token is invalid">>
})
end;
@ -308,9 +245,9 @@ process_request('IssueDestinationGrant', #{
'destinationID' := DestinationId,
'DestinationGrantRequest' := #{<<"validUntil">> := Expiration}
}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_destination(DestinationId, Context) of
case wapi_destination_backend:get(DestinationId, Context) of
{ok, _} ->
case wapi_backend_utils:issue_grant_token({destinations, DestinationId}, Expiration, Context) of
case issue_grant_token({destinations, DestinationId}, Expiration, Context) of
{ok, Token} ->
wapi_handler_utils:reply_ok(201, #{
<<"token">> => Token,
@ -326,40 +263,59 @@ process_request('IssueDestinationGrant', #{
{error, {destination, unauthorized}} ->
wapi_handler_utils:reply_ok(404)
end;
process_request('CreateWithdrawal', #{'WithdrawalParameters' := Params}, Context, Opts) ->
case wapi_wallet_ff_backend:create_withdrawal(Params, Context) of
{ok, Withdrawal = #{<<"id">> := WithdrawalId}} ->
wapi_handler_utils:reply_ok(202, Withdrawal, get_location('GetWithdrawal', [WithdrawalId], Opts));
{error, {source, notfound}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such wallet">>));
%% Withdrawals
process_request('CreateQuote', Params, Context, _Opts) ->
case wapi_withdrawal_backend:create_quote(Params, Context) of
{ok, Quote} ->
wapi_handler_utils:reply_ok(202, Quote);
{error, {destination, notfound}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such destination">>));
{error, {destination, {unauthorized, _}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Destination unauthorized">>)
);
{error, {destination, unauthorized}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Destination unauthorized">>));
{error, {provider, notfound}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such provider">>));
{error, {external_id_conflict, ID, ExternalID}} ->
{error, {wallet, notfound}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such wallet">>));
{error, {wallet, unauthorized}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Wallet unauthorized">>));
{error, {forbidden_currency, _}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Forbidden currency">>)
);
{error, {forbidden_amount, _}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Invalid cash amount">>)
);
{error, {inconsistent_currency, _}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Invalid currency">>)
);
{error, {identity_providers_mismatch, _}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(
<<"This wallet and destination cannot be used together">>
)
);
{error, {destination_resource, {bin_data, not_found}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Unknown card issuer">>)
)
end;
process_request('CreateWithdrawal', #{'WithdrawalParameters' := Params}, Context, Opts) ->
case wapi_withdrawal_backend:create(Params, Context) of
{ok, Withdrawal = #{<<"id">> := WithdrawalId}} ->
wapi_handler_utils:reply_ok(202, Withdrawal, get_location('GetWithdrawal', [WithdrawalId], Opts));
{error, {destination, notfound}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such destination">>));
{error, {destination, unauthorized}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Destination unauthorized">>));
{error, {external_id_conflict, ID}} ->
ExternalID = maps:get(<<"externalID">>, Params, undefined),
wapi_handler_utils:logic_error(external_id_conflict, {ID, ExternalID});
{error, {wallet, notfound}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such wallet">>));
{error, {wallet, {unauthorized, _}}} ->
{error, {wallet, unauthorized}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Wallet unauthorized">>));
{error, {wallet, {inaccessible, _}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Inaccessible source or destination">>)
);
{error, {wallet, {currency, invalid}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Invalid currency for source or destination">>)
);
{error, {wallet, {provider, invalid}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Invalid provider for source or destination">>)
);
{error, {quote_invalid_party, _}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Withdrawal owner differs from quote`s one">>)
@ -376,52 +332,68 @@ process_request('CreateWithdrawal', #{'WithdrawalParameters' := Params}, Context
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Withdrawal body differs from quote`s one">>)
);
{error, {terms, {terms_violation, {cash_range, _}}}} ->
{error, {forbidden_currency, _}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Forbidden currency">>)
);
{error, {forbidden_amount, _}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Invalid cash amount">>)
);
{error, {inconsistent_currency, _}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Invalid currency">>)
);
{error, {identity_providers_mismatch, _}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(
<<"This wallet and destination cannot be used together">>
)
);
{error, {destination_resource, {bin_data, not_found}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Unknown card issuer">>)
);
{error, {destination_resource, {bin_data, {unknown_payment_system, _PaymentSystem}}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Unknown card payment system">>)
);
{error, {destination_resource, {bin_data, {unknown_residence, _Residence}}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Unknown card issuer residence">>)
);
{error, {identity_providers_mismatch, _}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"This wallet and destination cannot be used together">>)
)
end;
process_request('GetWithdrawal', #{'withdrawalID' := WithdrawalId}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_withdrawal(WithdrawalId, Context) of
case wapi_withdrawal_backend:get(WithdrawalId, Context) of
{ok, Withdrawal} ->
wapi_handler_utils:reply_ok(200, Withdrawal);
{error, {withdrawal, {unknown_withdrawal, WithdrawalId}}} ->
{error, {withdrawal, notfound}} ->
wapi_handler_utils:reply_ok(404);
{error, {withdrawal, unauthorized}} ->
wapi_handler_utils:reply_ok(404)
end;
process_request('GetWithdrawalByExternalID', #{'externalID' := ExternalID}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_withdrawal_by_external_id(ExternalID, Context) of
case wapi_withdrawal_backend:get_by_external_id(ExternalID, Context) of
{ok, Withdrawal} ->
wapi_handler_utils:reply_ok(200, Withdrawal);
{error, {external_id, {unknown_external_id, ExternalID}}} ->
wapi_handler_utils:reply_ok(404);
{error, {withdrawal, {unknown_withdrawal, _WithdrawalId}}} ->
{error, {withdrawal, notfound}} ->
wapi_handler_utils:reply_ok(404);
{error, {withdrawal, unauthorized}} ->
wapi_handler_utils:reply_ok(404)
end;
process_request('ListWithdrawals', Params, Context, _Opts) ->
case wapi_stat_backend:list_withdrawals(Params, Context) of
{ok, List} -> wapi_handler_utils:reply_ok(200, List);
{error, {invalid, Errors}} ->
wapi_handler_utils:reply_error(400, #{
<<"errorType">> => <<"NoMatch">>,
<<"description">> => Errors
});
{error, {bad_token, Reason}} ->
wapi_handler_utils:reply_error(400, #{
<<"errorType">> => <<"InvalidToken">>,
<<"description">> => Reason
})
end;
process_request('PollWithdrawalEvents', Params, Context, _Opts) ->
case wapi_wallet_ff_backend:get_withdrawal_events(Params, Context) of
case wapi_withdrawal_backend:get_events(Params, Context) of
{ok, Events} ->
wapi_handler_utils:reply_ok(200, Events);
{error, {withdrawal, {unknown_withdrawal, _WithdrawalId}}} ->
{error, {withdrawal, notfound}} ->
wapi_handler_utils:reply_ok(404);
{error, {withdrawal, unauthorized}} ->
wapi_handler_utils:reply_ok(404)
@ -430,457 +402,62 @@ process_request('GetWithdrawalEvents', #{
'withdrawalID' := WithdrawalId,
'eventID' := EventId
}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_withdrawal_event(WithdrawalId, EventId, Context) of
case wapi_withdrawal_backend:get_event(WithdrawalId, EventId, Context) of
{ok, Event} ->
wapi_handler_utils:reply_ok(200, Event);
{error, {withdrawal, {unknown_withdrawal, WithdrawalId}}} ->
{error, {withdrawal, notfound}} ->
wapi_handler_utils:reply_ok(404);
{error, {withdrawal, unauthorized}} ->
wapi_handler_utils:reply_ok(404);
{error, {event, notfound}} ->
wapi_handler_utils:reply_ok(404)
end;
process_request('ListWithdrawals', Params, Context, _Opts) ->
case wapi_wallet_ff_backend:list_withdrawals(Params, Context) of
{ok, {200, _, List}} -> wapi_handler_utils:reply_ok(200, List);
{error, {Code, _, Error}} -> wapi_handler_utils:reply_error(Code, Error)
end;
process_request('CreateQuote', Params, Context, _Opts) ->
case wapi_wallet_ff_backend:create_quote(Params, Context) of
{ok, Promise} -> wapi_handler_utils:reply_ok(202, Promise);
{error, {destination, notfound}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Destination not found">>)
);
{error, {destination, unauthorized}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Destination unauthorized">>)
);
{error, {route, route_not_found}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Provider not found">>)
);
{error, {wallet, notfound}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Wallet not found">>)
);
{error, {identity_providers_mismatch, _}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(
<<"This wallet and destination cannot be used together">>
)
)
end;
%% Residences
process_request('GetResidence', #{'residence' := ResidenceId}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_residence(ResidenceId, Context) of
{ok, Residence} -> wapi_handler_utils:reply_ok(200, Residence);
{error, notfound} -> wapi_handler_utils:reply_ok(404)
end;
%% Currencies
process_request('GetCurrency', #{'currencyID' := CurrencyId}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_currency(CurrencyId, Context) of
{ok, Currency} -> wapi_handler_utils:reply_ok(200, Currency);
{error, notfound} -> wapi_handler_utils:reply_ok(404)
end;
%% Reports
process_request('CreateReport', Params, Context, _Opts) ->
case wapi_wallet_ff_backend:create_report(Params, Context) of
{ok, Report} -> wapi_handler_utils:reply_ok(201, Report);
{error, {identity, notfound}} -> wapi_handler_utils:reply_ok(400, #{
<<"errorType">> => <<"NotFound">>,
<<"name">> => <<"identity">>,
<<"description">> => <<"identity not found">>
});
{error, {identity, unauthorized}} -> wapi_handler_utils:reply_ok(400, #{
<<"errorType">> => <<"NotFound">>,
<<"name">> => <<"identity">>,
<<"description">> => <<"identity not found">>
});
{error, invalid_request} -> wapi_handler_utils:reply_ok(400, #{
<<"errorType">> => <<"NoMatch">>,
<<"name">> => <<"timestamps">>,
<<"description">> => <<"invalid time range">>
});
{error, invalid_contract} -> wapi_handler_utils:reply_ok(400, #{
<<"errorType">> => <<"NotFound">>,
<<"name">> => <<"contractID">>,
<<"description">> => <<"contract not found">>
})
end;
process_request('GetReport', #{
identityID := IdentityID,
reportID := ReportId
}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_report(ReportId, IdentityID, Context) of
{ok, Report} -> wapi_handler_utils:reply_ok(200, Report);
{error, {identity, notfound}} -> wapi_handler_utils:reply_ok(400, #{
<<"errorType">> => <<"NotFound">>,
<<"name">> => <<"identity">>,
<<"description">> => <<"identity not found">>
});
{error, {identity, unauthorized}} -> wapi_handler_utils:reply_ok(400, #{
<<"errorType">> => <<"NotFound">>,
<<"name">> => <<"identity">>,
<<"description">> => <<"identity not found">>
});
{error, notfound} -> wapi_handler_utils:reply_ok(404)
end;
process_request('GetReports', Params, Context, _Opts) ->
case wapi_wallet_ff_backend:get_reports(Params, Context) of
{ok, ReportList} -> wapi_handler_utils:reply_ok(200, ReportList);
{error, {identity, notfound}} -> wapi_handler_utils:reply_ok(400, #{
<<"errorType">> => <<"NotFound">>,
<<"name">> => <<"identity">>,
<<"description">> => <<"identity not found">>
});
{error, {identity, unauthorized}} -> wapi_handler_utils:reply_ok(400, #{
<<"errorType">> => <<"NotFound">>,
<<"name">> => <<"identity">>,
<<"description">> => <<"identity not found">>
});
{error, invalid_request} -> wapi_handler_utils:reply_ok(400, #{
<<"errorType">> => <<"NoMatch">>,
<<"name">> => <<"timestamps">>,
<<"description">> => <<"invalid time range">>
});
{error, {dataset_too_big, Limit}} -> wapi_handler_utils:reply_ok(400, #{
<<"errorType">> => <<"WrongLength">>,
<<"name">> => <<"limitExceeded">>,
<<"description">> => io_lib:format("Max limit: ~p", [Limit])
})
end;
process_request('DownloadFile', #{fileID := FileId}, Context, _Opts) ->
ExpiresAt = get_default_url_lifetime(),
case wapi_wallet_ff_backend:download_file(FileId, ExpiresAt, Context) of
{ok, URL} ->
wapi_handler_utils:reply_ok(201, #{<<"url">> => URL, <<"expiresAt">> => ExpiresAt});
{error, notfound} ->
wapi_handler_utils:reply_ok(404)
end;
%% Deposits
process_request('ListDeposits', Params, Context, _Opts) ->
case wapi_wallet_ff_backend:list_deposits(Params, Context) of
{ok, {200, _, List}} -> wapi_handler_utils:reply_ok(200, List);
{error, {Code, _, Error}} -> wapi_handler_utils:reply_error(Code, Error)
end;
%% Webhooks
process_request('CreateWebhook', Params, Context, _Opts) ->
case wapi_wallet_ff_backend:create_webhook(Params, Context) of
{ok, Webhook} -> wapi_handler_utils:reply_ok(201, Webhook);
{error, {identity, unauthorized}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such identity">>));
{error, {identity, notfound}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such identity">>));
{error, {wallet, unauthorized}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such wallet">>));
{error, {wallet, notfound}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such wallet">>))
end;
process_request('GetWebhooks', #{identityID := IdentityID}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_webhooks(IdentityID, Context) of
{ok, Webhooks} -> wapi_handler_utils:reply_ok(200, Webhooks);
{error, {identity, unauthorized}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such identity">>));
{error, {identity, notfound}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such identity">>))
end;
process_request('GetWebhookByID', #{identityID := IdentityID, webhookID := WebhookID}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_webhook(WebhookID, IdentityID, Context) of
{ok, Webhook} -> wapi_handler_utils:reply_ok(200, Webhook);
{error, notfound} ->
wapi_handler_utils:reply_ok(404);
{error, {identity, unauthorized}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such identity">>));
{error, {identity, notfound}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such identity">>))
end;
process_request('DeleteWebhookByID', #{identityID := IdentityID, webhookID := WebhookID}, Context, _Opts) ->
case wapi_wallet_ff_backend:delete_webhook(WebhookID, IdentityID, Context) of
ok -> wapi_handler_utils:reply_ok(204);
{error, notfound} ->
wapi_handler_utils:reply_ok(404);
{error, {identity, unauthorized}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such identity">>));
{error, {identity, notfound}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such identity">>))
end;
%% P2P
process_request('QuoteP2PTransfer', #{'QuoteParameters' := Params}, Context, _Opts) ->
case wapi_wallet_ff_backend:quote_p2p_transfer(Params, Context) of
{ok, Quote} ->
wapi_handler_utils:reply_ok(201, Quote);
{error, {identity, not_found}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"No such identity">>));
{error, {party, _PartyContractError}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"No such party">>));
{error, {sender, {bin_data, _}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Invalid sender resource">>));
{error, {receiver, {bin_data, _}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Invalid receiver resource">>));
{error, {terms, {terms_violation, {not_allowed_currency, _Details}}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Currency not allowed">>));
{error, {terms, {terms_violation, {cash_range, {_Cash, _CashRange}}}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Transfer amount is out of allowed range">>));
{error, {terms, {terms_violation, p2p_forbidden}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"P2P transfer not allowed">>));
{error, {invalid_resource_token, Type}} ->
case wapi_stat_backend:list_deposits(Params, Context) of
{ok, List} -> wapi_handler_utils:reply_ok(200, List);
{error, {invalid, Errors}} ->
wapi_handler_utils:reply_error(400, #{
<<"errorType">> => <<"InvalidResourceToken">>,
<<"name">> => Type,
<<"description">> => <<"Specified resource token is invalid">>
})
end;
process_request('CreateP2PTransfer', #{'P2PTransferParameters' := Params}, Context, _Opts) ->
case wapi_wallet_ff_backend:create_p2p_transfer(Params, Context) of
{ok, P2PTransfer} ->
wapi_handler_utils:reply_ok(202, P2PTransfer);
{error, {identity, notfound}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"No such identity">>));
{error, {external_id_conflict, ID, ExternalID}} ->
wapi_handler_utils:logic_error(external_id_conflict, {ID, ExternalID});
{error, {identity, unauthorized}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"No such identity">>));
{error, {sender, {bin_data, _}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Invalid sender resource">>));
{error, {sender, different_resource}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Invalid sender resource">>));
{error, {receiver, {bin_data, _}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Invalid receiver resource">>));
{error, {receiver, different_resource}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Invalid receiver resource">>));
{error, {terms, {terms_violation, {not_allowed_currency, _Details}}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Currency not allowed">>));
{error, {terms, {terms_violation, {cash_range, {_Cash, _CashRange}}}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Transfer amount is out of allowed range">>));
{error, {terms, {terms_violation, p2p_forbidden}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"P2P transfer not allowed">>));
{error, {token, {not_verified, _}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Token can't be verified">>));
{error, {invalid_resource_token, Type}} ->
wapi_handler_utils:reply_error(400, #{
<<"errorType">> => <<"InvalidResourceToken">>,
<<"name">> => Type,
<<"description">> => <<"Specified resource token is invalid">>
})
end;
process_request('GetP2PTransfer', #{p2pTransferID := ID}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_p2p_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, {unknown_p2p_transfer, _ID}}} ->
wapi_handler_utils:reply_ok(404)
end;
process_request('GetP2PTransferEvents', #{p2pTransferID := ID, continuationToken := CT}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_p2p_transfer_events({ID, CT}, Context) of
{ok, P2PTransferEvents} ->
wapi_handler_utils:reply_ok(200, P2PTransferEvents);
{error, {p2p_transfer, unauthorized}} ->
wapi_handler_utils:reply_ok(404);
{error, {p2p_transfer, not_found}} ->
wapi_handler_utils:reply_ok(404);
{error, {token, {not_verified, invalid_signature}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Continuation Token can't be verified">>))
end;
%% P2P Templates
process_request('CreateP2PTransferTemplate', #{'P2PTransferTemplateParameters' := Params}, Context, _Opts) ->
case wapi_wallet_ff_backend:create_p2p_template(Params, Context) of
{ok, P2PTemplate} ->
wapi_handler_utils:reply_ok(201, P2PTemplate);
{error, {identity, notfound}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"No such identity">>));
{error, {external_id_conflict, ID, ExternalID}} ->
wapi_handler_utils:logic_error(external_id_conflict, {ID, ExternalID});
{error, {identity, unauthorized}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"No such identity">>));
{error, {terms, {terms_violation, p2p_template_forbidden}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"P2P template not allowed">>))
end;
process_request('GetP2PTransferTemplateByID', #{p2pTransferTemplateID := ID}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_p2p_template(ID, Context) of
{ok, P2PTemplate} ->
wapi_handler_utils:reply_ok(200, P2PTemplate);
{error, {unknown_p2p_template, _ID}} ->
wapi_handler_utils:reply_ok(404)
end;
process_request('BlockP2PTransferTemplate', #{p2pTransferTemplateID := ID}, Context, _Opts) ->
case wapi_wallet_ff_backend:block_p2p_template(ID, Context) of
ok ->
wapi_handler_utils:reply_ok(204);
{error, {unknown_p2p_template, _ID}} ->
wapi_handler_utils:reply_ok(404);
{error, {p2p_template, unauthorized}} ->
wapi_handler_utils:reply_ok(404)
end;
process_request('IssueP2PTransferTemplateAccessToken', #{
p2pTransferTemplateID := ID,
'P2PTransferTemplateTokenRequest' := #{<<"validUntil">> := Expiration}
}, Context, _Opts) ->
case wapi_wallet_ff_backend:issue_p2p_template_access_token(ID, Expiration, Context) of
{ok, Token} ->
wapi_handler_utils:reply_ok(201, #{
<<"token">> => Token,
<<"validUntil">> => Expiration
<<"errorType">> => <<"NoMatch">>,
<<"description">> => Errors
});
{error, {p2p_template, unauthorized}} ->
wapi_handler_utils:reply_ok(404);
{error, expired} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Invalid expiration: already expired">>)
);
{error, {unknown_p2p_template, _ID}} ->
wapi_handler_utils:reply_ok(404)
end;
process_request('IssueP2PTransferTicket', #{
p2pTransferTemplateID := ID,
'P2PTransferTemplateTicketRequest' := #{<<"validUntil">> := Expiration0}
}, Context, _Opts) ->
case wapi_wallet_ff_backend:issue_p2p_transfer_ticket(ID, Expiration0, Context) of
{ok, {Token, Expiration1}} ->
wapi_handler_utils:reply_ok(201, #{
<<"token">> => Token,
<<"validUntil">> => Expiration1
});
{error, expired} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Invalid expiration: already expired">>)
);
{error, {unknown_p2p_template, _ID}} ->
wapi_handler_utils:reply_ok(404)
end;
process_request('CreateP2PTransferWithTemplate', #{
p2pTransferTemplateID := TemplateID,
'P2PTransferWithTemplateParameters' := Params
}, Context, _Opts) ->
case wapi_wallet_ff_backend:create_p2p_transfer_with_template(TemplateID, Params, Context) of
{ok, P2PTransfer} ->
wapi_handler_utils:reply_ok(202, P2PTransfer);
{error, {unknown_p2p_template, _ID}} ->
wapi_handler_utils:reply_ok(404);
{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, {sender, {bin_data, _}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Invalid sender resource">>));
{error, {receiver, {bin_data, _}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Invalid receiver resource">>));
{error, {terms, {terms_violation, {not_allowed_currency, _Details}}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Currency not allowed">>));
{error, {terms, {terms_violation, {cash_range, {_Cash, _CashRange}}}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Transfer amount is out of allowed range">>));
{error, {terms, {terms_violation, p2p_forbidden}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"P2P transfer not allowed">>));
{error, {token, {not_verified, _}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Token can't be verified">>));
{error, {invalid_resource_token, Type}} ->
{error, {bad_token, Reason}} ->
wapi_handler_utils:reply_error(400, #{
<<"errorType">> => <<"InvalidResourceToken">>,
<<"name">> => Type,
<<"description">> => <<"Specified resource token is invalid">>
})
end;
process_request('QuoteP2PTransferWithTemplate', #{
p2pTransferTemplateID := TemplateID,
'P2PTransferTemplateQuoteParameters' := Params
}, Context, _Opts) ->
case wapi_wallet_ff_backend:quote_p2p_transfer_with_template(TemplateID, Params, Context) of
{ok, Quote} ->
wapi_handler_utils:reply_ok(201, Quote);
{error, {unknown_p2p_template, _ID}} ->
wapi_handler_utils:reply_ok(404);
{error, {identity, not_found}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"No such identity">>));
{error, {party, _PartyContractError}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"No such party">>));
{error, {sender, {bin_data, _}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Invalid sender resource">>));
{error, {receiver, {bin_data, _}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Invalid receiver resource">>));
{error, {terms, {terms_violation, {not_allowed_currency, _Details}}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Currency not allowed">>));
{error, {terms, {terms_violation, {cash_range, {_Cash, _CashRange}}}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Transfer amount is out of allowed range">>));
{error, {terms, {terms_violation, p2p_forbidden}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"P2P transfer not allowed">>));
{error, {invalid_resource_token, Type}} ->
wapi_handler_utils:reply_error(400, #{
<<"errorType">> => <<"InvalidResourceToken">>,
<<"name">> => Type,
<<"description">> => <<"Specified resource token is invalid">>
<<"errorType">> => <<"InvalidToken">>,
<<"description">> => Reason
})
end;
%% W2W
process_request('CreateW2WTransfer', #{'W2WTransferParameters' := Params}, Context, _Opts) ->
case wapi_wallet_ff_backend:create_w2w_transfer(Params, Context) of
case wapi_w2w_backend:create_transfer(Params, Context) of
{ok, W2WTransfer} ->
wapi_handler_utils:reply_ok(202, W2WTransfer);
{error, {wallet_from, notfound}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"No such wallet sender">>));
{error, {wallet_from, unauthorized}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"No such wallet sender">>));
{error, {wallet_to, notfound}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"No such wallet receiver">>));
{error, {wallet_from, {inaccessible, _}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Sender wallet is unaccessible">>));
{error, {wallet_to, {inaccessible, _}}} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Receiver wallet is unaccessible">>));
{error, {terms, {terms_violation, {not_allowed_currency, _Details}}}} ->
{error, not_allowed_currency} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Currency not allowed">>));
{error, {terms, {terms_violation, w2w_forbidden}}} ->
{error, bad_w2w_transfer_amount} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"W2W transfer not allowed">>))
wapi_handler_utils:get_error_msg(<<"Bad transfer amount">>));
{error, inconsistent_currency} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Inconsistent currency">>))
end;
process_request('GetW2WTransfer', #{w2wTransferID := ID}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_w2w_transfer(ID, Context) of
case wapi_w2w_backend:get_transfer(ID, Context) of
{ok, W2WTransfer} ->
wapi_handler_utils:reply_ok(200, W2WTransfer);
{error, {w2w_transfer, unauthorized}} ->
@ -895,13 +472,20 @@ get_location(OperationId, Params, Opts) ->
#{path := PathSpec} = swag_server_wallet_router:get_operation(OperationId),
wapi_handler_utils:get_location(PathSpec, Params, Opts).
-spec not_implemented() -> no_return().
not_implemented() ->
wapi_handler_utils:throw_not_implemented().
issue_grant_token(TokenSpec, Expiration, Context) ->
case get_expiration_deadline(Expiration) of
{ok, Deadline} ->
{ok, wapi_auth:issue_access_token(wapi_handler_utils:get_owner(Context), TokenSpec, {deadline, Deadline})};
Error = {error, _} ->
Error
end.
-define(DEFAULT_URL_LIFETIME, 60). % seconds
get_default_url_lifetime() ->
Now = erlang:system_time(second),
Lifetime = application:get_env(wapi, file_storage_url_lifetime, ?DEFAULT_URL_LIFETIME),
genlib_rfc3339:format(Now + Lifetime, second).
get_expiration_deadline(Expiration) ->
{DateTime, MilliSec} = woody_deadline:from_binary(wapi_utils:to_universal_time(Expiration)),
Deadline = genlib_time:daytime_to_unixtime(DateTime) + MilliSec div 1000,
case genlib_time:unow() - Deadline < 0 of
true ->
{ok, Deadline};
false ->
{error, expired}
end.

View File

@ -1,6 +1,13 @@
-module(wapi_withdrawal_backend).
-define(DOMAIN, <<"wallet-api">>).
-define(event(ID, Timestamp, Change), #wthd_Event{
event_id = ID,
occured_at = Timestamp,
change = Change
}).
-define(statusChange(Status), {status_changed, #wthd_StatusChange{status = Status}}).
-type req_data() :: wapi_handler:req_data().
-type handler_context() :: wapi_handler:context().
@ -16,20 +23,34 @@
{quote_invalid_wallet, _} |
{quote, {invalid_body, _}} |
{quote, {invalid_destination, _}} |
{quote, token_expired} |
{forbidden_currency, _} |
{forbidden_amount, _} |
{inconsistent_currency, _} |
{identity_providers_mismatch, {id(), id()}} |
{destination_resource, {bin_data, not_found}}.
-type create_quote_error() ::
{destination, notfound | unauthorized} |
{wallet, notfound | unauthorized} |
{forbidden_currency, _} |
{forbidden_amount, _} |
{inconsistent_currency, _} |
{identity_providers_mismatch, {id(), id()}} |
{destination_resource, {bin_data, not_found}}.
-export([create/2]).
-export([get/2]).
-export([get_by_external_id/2]).
-export([create_quote/2]).
-export([get_events/2]).
-export([get_event/3]).
-include_lib("fistful_proto/include/ff_proto_withdrawal_thrift.hrl").
%% Pipeline
-import(ff_pipeline, [do/1, unwrap/1, unwrap/2]).
-import(wapi_pipeline, [do/1, unwrap/1, unwrap/2]).
-spec create(req_data(), handler_context()) ->
{ok, response_data()} | {error, create_error()}.
@ -76,9 +97,7 @@ create(Params, Context, HandlerContext) ->
}} ->
{error, {identity_providers_mismatch, {WalletProvider, DestinationProvider}}};
{exception, #wthd_NoDestinationResourceInfo{}} ->
{error, {destination_resource, {bin_data, not_found}}};
{exception, Details} ->
{error, Details}
{error, {destination_resource, {bin_data, not_found}}}
end.
-spec get(id(), handler_context()) ->
@ -116,15 +135,165 @@ get_by_external_id(ExternalID, HandlerContext = #{woody_context := WoodyContext}
{error, {external_id, {unknown_external_id, ExternalID}}}
end.
-spec create_quote(req_data(), handler_context()) ->
{ok, response_data()} | {error, create_quote_error()}.
create_quote(#{'WithdrawalQuoteParams' := Params}, HandlerContext) ->
case authorize_quote(Params, HandlerContext) of
ok ->
create_quote_(Params, HandlerContext);
{error, _} = Error ->
Error
end.
create_quote_(Params, HandlerContext) ->
CreateQuoteParams = marshal(create_quote_params, Params),
Request = {fistful_withdrawal, 'GetQuote', [CreateQuoteParams]},
case service_call(Request, HandlerContext) of
{ok, QuoteThrift} ->
Token = create_quote_token(
QuoteThrift,
maps:get(<<"walletID">>, Params),
maps:get(<<"destinationID">>, Params, undefined),
wapi_handler_utils:get_owner(HandlerContext)
),
UnmarshaledQuote = unmarshal(quote, QuoteThrift),
{ok, UnmarshaledQuote#{<<"quoteToken">> => Token}};
{exception, #fistful_WalletNotFound{}} ->
{error, {wallet, notfound}};
{exception, #fistful_DestinationNotFound{}} ->
{error, {destination, notfound}};
{exception, #fistful_DestinationUnauthorized{}} ->
{error, {destination, unauthorized}};
{exception, #fistful_ForbiddenOperationCurrency{currency = Currency}} ->
{error, {forbidden_currency, unmarshal_currency_ref(Currency)}};
{exception, #fistful_ForbiddenOperationAmount{amount = Amount}} ->
{error, {forbidden_amount, unmarshal_body(Amount)}};
{exception, #wthd_InconsistentWithdrawalCurrency{
withdrawal_currency = WithdrawalCurrency,
destination_currency = DestinationCurrency,
wallet_currency = WalletCurrency
}} ->
{error, {inconsistent_currency, {
unmarshal_currency_ref(WithdrawalCurrency),
unmarshal_currency_ref(DestinationCurrency),
unmarshal_currency_ref(WalletCurrency)
}}};
{exception, #wthd_IdentityProvidersMismatch{
wallet_provider = WalletProvider,
destination_provider = DestinationProvider
}} ->
{error, {identity_providers_mismatch, {WalletProvider, DestinationProvider}}};
{exception, #wthd_NoDestinationResourceInfo{}} ->
{error, {destination_resource, {bin_data, not_found}}}
end.
-spec get_events(req_data(), handler_context()) ->
{ok, response_data()} |
{error, {withdrawal, notfound}} |
{error, {withdrawal, unauthorized}}.
get_events(Params = #{'withdrawalID' := WithdrawalId, 'limit' := Limit}, HandlerContext) ->
Cursor = maps:get('eventCursor', Params, undefined),
case get_events(WithdrawalId, {Cursor, Limit}, HandlerContext) of
{ok, Events} ->
{ok, Events};
{error, {withdrawal, unauthorized}} = Error ->
Error;
{error, {withdrawal, notfound}} = Error ->
Error;
{exception, #fistful_WithdrawalNotFound{}} ->
{error, {withdrawal, notfound}}
end.
-spec get_event(id(), integer(), handler_context()) ->
{ok, response_data()} |
{error, {withdrawal, notfound}} |
{error, {withdrawal, unauthorized}} |
{error, {event, notfound}}.
get_event(WithdrawalId, EventId, HandlerContext) ->
case get_events(WithdrawalId, {EventId - 1, 1}, HandlerContext) of
{ok, [Event]} ->
{ok, Event};
{ok, []} ->
{error, {event, notfound}};
{error, {withdrawal, unauthorized}} = Error ->
Error;
{error, {withdrawal, notfound}} = Error ->
Error;
{exception, #fistful_WithdrawalNotFound{}} ->
{error, {withdrawal, notfound}}
end.
%%
%% Internal
%%
service_call(Params, Context) ->
wapi_handler_utils:service_call(Params, Context).
create_quote_token(Quote, WalletID, DestinationID, PartyID) ->
Payload = wapi_withdrawal_quote:create_token_payload(Quote, WalletID, DestinationID, PartyID),
{ok, Token} = issue_quote_token(PartyID, Payload),
Token.
issue_quote_token(PartyID, Data) ->
uac_authorizer_jwt:issue(wapi_utils:get_unique_id(), PartyID, Data, wapi_auth:get_signee()).
service_call(Params, HandlerContext) ->
wapi_handler_utils:service_call(Params, HandlerContext).
get_events(WithdrawalId, EventRange, HandlerContext) ->
case get_events_(WithdrawalId, EventRange, HandlerContext) of
{ok, Events0} ->
Events1 = lists:filter(fun event_filter/1, Events0),
{ok, unmarshal({list, event}, Events1)};
{error, _} = Error ->
Error;
{exception, _} = Exception ->
Exception
end.
get_events_(WithdrawalId, EventRange, HandlerContext) ->
case authorize_resource_by_bearer(withdrawal, WithdrawalId, HandlerContext) of
ok ->
collect_events(WithdrawalId, EventRange, HandlerContext, []);
{error, _} = Error ->
Error
end.
collect_events(WithdrawalId, {Cursor, Limit}, HandlerContext, AccEvents) ->
Request = {fistful_withdrawal, 'GetEvents', [WithdrawalId, marshal_event_range(Cursor, Limit)]},
case service_call(Request, HandlerContext) of
{exception, _} = Exception ->
Exception;
{ok, []} ->
{ok, AccEvents};
{ok, Events} ->
?event(NewCursor, _, _) = lists:last(Events),
collect_events(WithdrawalId, {NewCursor, Limit - length(Events)}, HandlerContext, AccEvents ++ Events)
end.
event_filter(?event(_, _, ?statusChange(_)))->
true;
event_filter(_) ->
false.
%% Validators
authorize_quote(Params = #{<<"walletID">> := WalletID}, HandlerContext) ->
do(fun() ->
unwrap(wallet, wapi_access_backend:check_resource_by_id(wallet, WalletID, HandlerContext)),
case maps:get(<<"destinationID">>, Params, undefined) of
undefined ->
ok;
DestinationID ->
unwrap(destination, wapi_access_backend:check_resource_by_id(
destination,
DestinationID,
HandlerContext
))
end
end).
check_withdrawal_params(Params0, HandlerContext) ->
do(fun() ->
Params1 = unwrap(try_decode_quote_token(Params0)),
@ -137,7 +306,7 @@ check_withdrawal_params(Params0, HandlerContext) ->
try_decode_quote_token(Params = #{<<"quoteToken">> := QuoteToken}) ->
do(fun() ->
{_, _, Data} = unwrap(uac_authorizer_jwt:verify(QuoteToken, #{})),
{ok, Quote, WalletID, DestinationID, PartyID} = wapi_withdrawal_quote:decode_token_payload(Data),
{Quote, WalletID, DestinationID, PartyID} = unwrap(quote, wapi_withdrawal_quote:decode_token_payload(Data)),
Params#{<<"quoteToken">> => #{
quote => Quote,
wallet_id => WalletID,
@ -239,12 +408,12 @@ maybe_check_quote_token(Params = #{<<"quoteToken">> := #{
wallet_id := WalletID,
destination_id := DestinationID,
party_id := PartyID
}}, Context) ->
}}, HandlerContext) ->
do(fun() ->
unwrap(quote_invalid_party, valid(PartyID, wapi_handler_utils:get_owner(Context))),
unwrap(quote_invalid_party, valid(PartyID, wapi_handler_utils:get_owner(HandlerContext))),
unwrap(quote_invalid_wallet, valid(WalletID, maps:get(<<"wallet">>, Params))),
unwrap(check_quote_withdrawal(DestinationID, maps:get(<<"destination">>, Params))),
unwrap(check_quote_body(maps:get(cash_from, Quote), marshal_quote_body(maps:get(<<"body">>, Params)))),
unwrap(check_quote_body(Quote#wthd_Quote.cash_from, marshal_body(maps:get(<<"body">>, Params)))),
Params#{<<"quote">> => Quote}
end);
maybe_check_quote_token(Params, _Context) ->
@ -267,9 +436,6 @@ check_quote_withdrawal(DestinationID, DestinationID) ->
check_quote_withdrawal(_, DestinationID) ->
{error, {quote, {invalid_destination, DestinationID}}}.
marshal_quote_body(Body) ->
{genlib:to_int(maps:get(<<"amount">>, Body)), maps:get(<<"currency">>, Body)}.
%% Marshaling
marshal(withdrawal_params, Params = #{
@ -286,34 +452,61 @@ marshal(withdrawal_params, Params = #{
wallet_id = marshal(id, WalletID),
destination_id = marshal(id, DestinationID),
body = marshal_body(Body),
quote = marshal_quote(Quote),
quote = Quote,
external_id = maybe_marshal(id, ExternalID),
metadata = maybe_marshal(context, Metadata)
};
marshal(create_quote_params, Params = #{
<<"walletID">> := WalletID,
<<"currencyFrom">> := CurrencyFrom,
<<"currencyTo">> := CurrencyTo,
<<"cash">> := Body
}) ->
ExternalID = maps:get(<<"externalID">>, Params, undefined),
DestinationID = maps:get(<<"destinationID">>, Params, undefined),
#wthd_QuoteParams{
wallet_id = marshal(id, WalletID),
body = marshal_body(Body),
currency_from = marshal_currency_ref(CurrencyFrom),
currency_to = marshal_currency_ref(CurrencyTo),
destination_id = maybe_marshal(id, DestinationID),
external_id = maybe_marshal(id, ExternalID)
};
marshal(context, Context) ->
ff_codec:marshal(context, Context);
wapi_codec:marshal(context, Context);
marshal(T, V) ->
ff_codec:marshal(T, V).
wapi_codec:marshal(T, V).
maybe_marshal(_, undefined) ->
undefined;
maybe_marshal(T, V) ->
marshal(T, V).
marshal_event_range(Cursor, Limit) when
(is_integer(Cursor) orelse Cursor =:= undefined) andalso
(is_integer(Limit) orelse Limit =:= undefined)
->
#'EventRange'{
'after' = Cursor,
'limit' = Limit
}.
marshal_body(Body) ->
#'Cash'{
amount = genlib:to_int(maps:get(<<"amount">>, Body)),
currency = #'CurrencyRef'{
symbolic_code = maps:get(<<"currency">>, Body)
}
currency = marshal_currency_ref(maps:get(<<"currency">>, Body))
}.
marshal_quote(undefined) ->
undefined;
marshal_quote(Quote) ->
ff_withdrawal_codec:marshal(quote, Quote).
marshal_currency_ref(Currency) ->
#'CurrencyRef'{
symbolic_code = Currency
}.
unmarshal({list, Type}, List) ->
lists:map(fun(V) -> unmarshal(Type, V) end, List);
unmarshal(withdrawal, #wthd_WithdrawalState{
id = ID,
@ -336,8 +529,31 @@ unmarshal(withdrawal, #wthd_WithdrawalState{
<<"metadata">> => UnmarshaledMetadata
}, unmarshal_status(Status)));
unmarshal(quote, #wthd_Quote{
cash_from = CashFrom,
cash_to = CashTo,
created_at = CreatedAt,
expires_on = ExpiresOn
}) ->
#{
<<"cashFrom">> => unmarshal_body(CashFrom),
<<"cashTo">> => unmarshal_body(CashTo),
<<"createdAt">> => CreatedAt,
<<"expiresOn">> => ExpiresOn
};
unmarshal(event, ?event(EventId, OccuredAt, ?statusChange(Status))) ->
genlib_map:compact(#{
<<"eventID">> => EventId,
<<"occuredAt">> => OccuredAt,
<<"changes">> => [maps:merge(
#{<<"type">> => <<"WithdrawalStatusChanged">>},
unmarshal_status(Status)
)]
});
unmarshal(T, V) ->
ff_codec:unmarshal(T, V).
wapi_codec:unmarshal(T, V).
maybe_unmarshal(_, undefined) ->
undefined;

View File

@ -1,5 +1,7 @@
-module(wapi_withdrawal_quote).
-include_lib("fistful_proto/include/ff_proto_withdrawal_thrift.hrl").
-export([create_token_payload/4]).
-export([decode_token_payload/1]).
@ -17,23 +19,26 @@
-type wallet_id() :: binary().
-type destination_id() :: binary() | undefined.
-type party_id() :: binary().
-type quote() :: ff_withdrawal:quote().
-type quote() :: ff_proto_withdrawal_thrift:'Quote'().
%% API
-spec create_token_payload(quote(), wallet_id(), destination_id(), party_id()) ->
token_payload().
create_token_payload(Quote, WalletID, DestinationID, PartyID) ->
do_create_token_payload(encode_quote(Quote), WalletID, DestinationID, PartyID).
do_create_token_payload(Quote, WalletID, DestinationID, PartyID) ->
genlib_map:compact(#{
<<"version">> => 2,
<<"walletID">> => WalletID,
<<"destinationID">> => DestinationID,
<<"partyID">> => PartyID,
<<"quote">> => encode_quote(Quote)
<<"quote">> => Quote
}).
-spec decode_token_payload(token_payload()) ->
{ok, quote(), wallet_id(), destination_id(), party_id()}.
{ok, {quote(), wallet_id(), destination_id(), party_id()}} | {error, token_expired}.
decode_token_payload(#{<<"version">> := 2} = Payload) ->
#{
<<"version">> := 2,
@ -43,42 +48,9 @@ decode_token_payload(#{<<"version">> := 2} = Payload) ->
} = Payload,
Quote = decode_quote(EncodedQuote),
DestinationID = maps:get(<<"destinationID">>, Payload, undefined),
{ok, Quote, WalletID, DestinationID, PartyID};
decode_token_payload(#{<<"version">> := 1} = Payload) ->
#{
<<"version">> := 1,
<<"walletID">> := WalletID,
<<"partyID">> := PartyID,
<<"cashFrom">> := CashFrom,
<<"cashTo">> := CashTo,
<<"createdAt">> := CreatedAt,
<<"expiresOn">> := ExpiresOn,
<<"quoteData">> := LegacyQuote
} = Payload,
DestinationID = maps:get(<<"destinationID">>, Payload, undefined),
#{
<<"version">> := 1,
<<"quote_data">> := QuoteData,
<<"provider_id">> := ProviderID,
<<"resource_id">> := ResourceID,
<<"timestamp">> := Timestamp,
<<"domain_revision">> := DomainRevision,
<<"party_revision">> := PartyRevision
} = LegacyQuote,
TerminalID = maps:get(<<"terminal_id">>, LegacyQuote, undefined),
Quote = genlib_map:compact(#{
cash_from => decode_legacy_cash(CashFrom),
cash_to => decode_legacy_cash(CashTo),
created_at => CreatedAt,
expires_on => ExpiresOn,
quote_data => QuoteData,
route => ff_withdrawal_routing:make_route(ProviderID, TerminalID),
operation_timestamp => Timestamp,
resource_descriptor => decode_legacy_resource_id(ResourceID),
domain_revision => DomainRevision,
party_revision => PartyRevision
}),
{ok, Quote, WalletID, DestinationID, PartyID}.
{ok, {Quote, WalletID, DestinationID, PartyID}};
decode_token_payload(#{<<"version">> := 1} = _Payload) ->
{error, token_expired}.
%% Internals
@ -86,7 +58,7 @@ decode_token_payload(#{<<"version">> := 1} = Payload) ->
token_payload().
encode_quote(Quote) ->
Type = {struct, struct, {ff_proto_withdrawal_thrift, 'Quote'}},
Bin = ff_proto_utils:serialize(Type, ff_withdrawal_codec:marshal(quote, Quote)),
Bin = wapi_thrift_utils:serialize(Type, Quote),
base64:encode(Bin).
-spec decode_quote(token_payload()) ->
@ -94,15 +66,7 @@ encode_quote(Quote) ->
decode_quote(Encoded) ->
Type = {struct, struct, {ff_proto_withdrawal_thrift, 'Quote'}},
Bin = base64:decode(Encoded),
Thrift = ff_proto_utils:deserialize(Type, Bin),
ff_withdrawal_codec:unmarshal(quote, Thrift).
decode_legacy_cash(Body) ->
{genlib:to_int(maps:get(<<"amount">>, Body)), maps:get(<<"currency">>, Body)}.
decode_legacy_resource_id(#{<<"bank_card">> := ID}) ->
{bank_card, ID}.
wapi_thrift_utils:deserialize(Type, Bin).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
@ -130,9 +94,10 @@ payload_symmetry_test() ->
domain_revision => 1,
party_revision => 2
},
Payload = create_token_payload(Quote, WalletID, DestinationID, PartyID),
{ok, Decoded, WalletID, DestinationID, PartyID} = decode_token_payload(Payload),
?assertEqual(Quote, Decoded).
ThriftQuote = ff_withdrawal_codec:marshal(quote, Quote),
Payload = create_token_payload(ThriftQuote, WalletID, DestinationID, PartyID),
{ok, {Decoded, WalletID, DestinationID, PartyID}} = decode_token_payload(Payload),
?assertEqual(ThriftQuote, Decoded).
-spec payload_v2_decoding_test() -> _.
payload_v2_decoding_test() ->
@ -156,6 +121,7 @@ payload_v2_decoding_test() ->
domain_revision => 1,
party_revision => 2
},
ExpectedThriftQuote = ff_withdrawal_codec:marshal(quote, ExpectedQuote),
Payload = #{
<<"version">> => 2,
<<"walletID">> => WalletID,
@ -171,7 +137,7 @@ payload_v2_decoding_test() ->
>>
},
?assertEqual(
{ok, ExpectedQuote, WalletID, DestinationID, PartyID},
{ok, {ExpectedThriftQuote, WalletID, DestinationID, PartyID}},
decode_token_payload(Payload)
).
@ -180,18 +146,6 @@ payload_v1_decoding_test() ->
PartyID = <<"party">>,
WalletID = <<"wallet">>,
DestinationID = <<"destination">>,
ExpectedQuote = #{
cash_from => {1000000, <<"RUB">>},
cash_to => {1, <<"USD">>},
created_at => <<"1970-01-01T00:00:00.123Z">>,
expires_on => <<"1970-01-01T00:00:00.321Z">>,
quote_data => 6,
route => ff_withdrawal_routing:make_route(1000, 2),
operation_timestamp => 234,
resource_descriptor => {bank_card, 5},
domain_revision => 1,
party_revision => 2
},
Payload = #{
<<"version">> => 1,
<<"walletID">> => WalletID,
@ -213,7 +167,7 @@ payload_v1_decoding_test() ->
}
},
?assertEqual(
{ok, ExpectedQuote, WalletID, DestinationID, PartyID},
{error, token_expired},
decode_token_payload(Payload)
).

View File

@ -4,10 +4,17 @@
-include_lib("wapi_wallet_dummy_data.hrl").
-include_lib("damsel/include/dmsl_domain_config_thrift.hrl").
-export([cfg/2]).
-export([cfg/3]).
-export([makeup_cfg/2]).
-export([woody_ctx/0]).
-export([get_woody_ctx/1]).
-export([test_case_name/1]).
-export([get_test_case_name/1]).
-export([init_suite/2]).
-export([start_app/1]).
-export([start_app/2]).
-export([start_wapi/1]).
-export([issue_token/4]).
-export([get_context/1]).
-export([get_keysource/2]).
@ -25,58 +32,120 @@
-define(DOMAIN, <<"wallet-api">>).
%%
-type config() :: [{atom(), any()}].
-type config() :: [{atom(), any()}].
-type test_case_name() :: atom().
-type app_name() :: atom().
-type app_env() :: [{atom(), term()}].
% -type app_with_env() :: {app_name(), app_env()}.
% -type startup_ctx() :: #{atom() => _}.
-define(SIGNEE, wapi).
-spec cfg(atom(), config()) -> term().
cfg(Key, Config) ->
case lists:keyfind(Key, 1, Config) of
{Key, V} -> V;
_ -> error({'ct config entry missing', Key})
end.
-spec cfg(atom(), _, config()) -> config().
cfg(Key, Value, Config) ->
lists:keystore(Key, 1, Config, {Key, Value}).
-type config_mut_fun() :: fun((config()) -> config()).
-spec makeup_cfg([config_mut_fun()], config()) -> config().
makeup_cfg(CMFs, C0) ->
lists:foldl(fun (CMF, C) -> CMF(C) end, C0, CMFs).
-spec woody_ctx() -> config_mut_fun().
woody_ctx() ->
fun (C) -> cfg('$woody_ctx', construct_woody_ctx(C), C) end.
construct_woody_ctx(C) ->
woody_context:new(construct_rpc_id(get_test_case_name(C))).
construct_rpc_id(TestCaseName) ->
woody_context:new_rpc_id(
<<"undefined">>,
list_to_binary(lists:sublist(atom_to_list(TestCaseName), 32)),
woody_context:new_req_id()
).
-spec get_woody_ctx(config()) -> woody_context:ctx().
get_woody_ctx(C) ->
cfg('$woody_ctx', C).
%%
-spec test_case_name(test_case_name()) -> config_mut_fun().
test_case_name(TestCaseName) ->
fun (C) -> cfg('$test_case_name', TestCaseName, C) end.
-spec get_test_case_name(config()) -> test_case_name().
get_test_case_name(C) ->
cfg('$test_case_name', C).
%
-spec init_suite(module(), config()) ->
config().
init_suite(Module, Config) ->
SupPid = start_mocked_service_sup(Module),
Apps1 =
start_app(scoper) ++
start_app(woody),
ServiceURLs = mock_services_([
{
'Repository',
{dmsl_domain_config_thrift, 'Repository'},
fun('Checkout', _) -> {ok, ?SNAPSHOT} end
}
], SupPid),
Apps2 =
start_app(dmt_client, [{max_cache_size, #{}}, {service_urls, ServiceURLs}, {cache_update_interval, 50000}]) ++
start_wapi(Config),
[{apps, lists:reverse(Apps2 ++ Apps1)}, {suite_test_sup, SupPid} | Config].
start_app(woody) ++
start_app({wapi, Config}),
[{apps, lists:reverse(Apps1)}, {suite_test_sup, SupPid} | Config].
-spec start_app(app_name()) ->
[app_name()].
start_app(scoper = AppName) ->
start_app(AppName, []);
start_app_with(AppName, [
{storage, scoper_storage_logger}
]);
start_app(woody = AppName) ->
start_app(AppName, [
start_app_with(AppName, [
{acceptors_pool_size, 4}
]);
start_app(wapi_woody_client = AppName) ->
start_app(AppName, [
{service_urls, #{
cds_storage => "http://cds:8022/v2/storage",
identdoc_storage => "http://cds:8022/v1/identity_document_storage",
fistful_stat => "http://fistful-magista:8022/stat"
start_app({wapi = AppName, Config}) ->
JwkPath = get_keysource("jwk.json", Config),
start_app_with(AppName, [
{ip, "::"},
{port, 8080},
{realm, <<"external">>},
{public_endpoint, <<"localhost:8080">>},
{access_conf, #{
jwt => #{
keyset => #{
wapi => {pem_file, get_keysource("private.pem", Config)}
}
}
}},
{service_retries, #{
fistful_stat => #{
'GetWallets' => {linear, 3, 1000},
'_' => finish
{signee, wapi},
{lechiffre_opts, #{
encryption_key_path => JwkPath,
decryption_key_paths => [JwkPath]
}},
{swagger_handler_opts, #{
validation_opts => #{
custom_validator => wapi_swagger_validator
}
}}
]);
start_app(AppName) ->
genlib_app:start_application(AppName).
[genlib_app:start_application(AppName)].
-spec start_app(app_name(), list()) ->
[app_name()].
@ -84,23 +153,25 @@ start_app(AppName) ->
start_app(AppName, Env) ->
genlib_app:start_application_with(AppName, Env).
-spec start_wapi(config()) ->
[app_name()].
start_wapi(Config) ->
start_app(wapi, [
{ip, ?WAPI_IP},
{port, ?WAPI_PORT},
{realm, <<"external">>},
{public_endpoint, <<"localhost:8080">>},
{access_conf, #{
jwt => #{
keyset => #{
wapi => {pem_file, get_keysource("keys/local/private.pem", Config)}
}
}
}},
{signee, ?SIGNEE}
]).
-spec start_app_with(app_name(), app_env()) -> [app_name()].
start_app_with(AppName, Env) ->
_ = application:load(AppName),
_ = set_app_env(AppName, Env),
case application:ensure_all_started(AppName) of
{ok, Apps} ->
Apps;
{error, Reason} ->
exit({start_app_failed, AppName, Reason})
end.
set_app_env(AppName, Env) ->
lists:foreach(
fun ({K, V}) ->
ok = application:set_env(AppName, K, V)
end,
Env
).
-spec get_keysource(_, config()) ->
_.

View File

@ -51,14 +51,14 @@ init([]) ->
[test_case_name()].
all() ->
[
{group, default}
{group, base}
].
-spec groups() ->
[{group_name(), list(), [test_case_name()]}].
groups() ->
[
{default, [], [
{base, [], [
bank_card_resource_test,
bitcoin_resource_test,
litecoin_resource_test,
@ -73,38 +73,27 @@ groups() ->
%%
%% starting/stopping
%%
-spec init_per_suite(config()) ->
config().
init_per_suite(Config0) ->
%% 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
]
})
], Config0).
-spec init_per_suite(config()) -> config().
init_per_suite(C) ->
wapi_ct_helper:init_suite(?MODULE, C).
-spec end_per_suite(config()) -> _.
-spec end_per_suite(config()) ->
_.
end_per_suite(C) ->
%% TODO remove this after cut off wapi
ok = application:unset_env(wapi, transport),
ok = ct_payment_system:shutdown(C).
_ = wapi_ct_helper:stop_mocked_service_sup(?config(suite_test_sup, C)),
_ = [application:stop(App) || App <- ?config(apps, C)],
ok.
-spec init_per_group(group_name(), config()) ->
config().
init_per_group(default = Group, Config) ->
ok = ff_context:save(ff_context:create(#{
init_per_group(Group, Config) when Group =:= base ->
ok = wapi_context:save(wapi_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),
{ok, Token} = wapi_ct_helper:issue_token(Party, [{[party], write}], {deadline, 10}, ?DOMAIN),
Party = genlib:bsuuid(),
{ok, Token} = wapi_ct_helper:issue_token(Party, [{[party], write}], unlimited, ?DOMAIN),
Config1 = [{party, Party} | Config],
[{context, wapi_ct_helper:get_context(Token)} | Config1];
init_per_group(_, Config) ->
@ -118,14 +107,14 @@ end_per_group(_Group, _C) ->
-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),
C1 = wapi_ct_helper:makeup_cfg([wapi_ct_helper:test_case_name(Name), wapi_ct_helper:woody_ctx()], C),
ok = wapi_context:save(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(),
ok = wapi_context:cleanup(),
wapi_ct_helper:stop_mocked_service_sup(?config(test_sup, C)),
ok.
@ -205,12 +194,18 @@ zcash_resource_test(C) ->
%%
do_destination_lifecycle(ResourceType, C) ->
PartyID = ?config(party, C),
PartyID = wapi_ct_helper:cfg(party, C),
Identity = generate_identity(PartyID),
Resource = generate_resource(ResourceType),
Context = generate_context(PartyID),
Destination = generate_destination(Identity#idnt_IdentityState.id, Resource, Context),
wapi_ct_helper:mock_services([
{bender_thrift,
fun
('GenerateID', _) -> {ok, ?GENERATE_ID_RESULT};
('GetInternalID', _) -> {ok, ?GET_INTERNAL_ID_RESULT}
end
},
{fistful_identity, fun('GetContext', _) -> {ok, ?DEFAULT_CONTEXT(PartyID)} end},
{fistful_destination,
fun
@ -224,7 +219,7 @@ do_destination_lifecycle(ResourceType, C) ->
#{
body => build_destination_spec(Destination)
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
),
{ok, GetResult} = call_api(
fun swag_client_wallet_withdrawals_api:get_destination/3,
@ -233,7 +228,7 @@ do_destination_lifecycle(ResourceType, C) ->
<<"destinationID">> => ?STRING
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
),
?assertEqual(CreateResult, GetResult),
{ok, GetByIDResult} = call_api(
@ -243,7 +238,7 @@ do_destination_lifecycle(ResourceType, C) ->
<<"externalID">> => Destination#dst_DestinationState.external_id
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
),
?assertEqual(GetResult, GetByIDResult),
?assertEqual(Destination#dst_DestinationState.id, maps:get(<<"id">>, CreateResult)),
@ -266,11 +261,6 @@ call_api(F, Params, Context) ->
Response = F(Url, PreparedParams, Opts),
wapi_client_lib:handle_response(Response).
create_party(_C) ->
ID = genlib:bsuuid(),
_ = ff_party:create(ID),
ID.
build_destination_spec(D) ->
#{
<<"name">> => D#dst_DestinationState.name,

View File

@ -0,0 +1,7 @@
{
"use": "enc",
"kty": "oct",
"kid": "1",
"alg": "dir",
"k": "M3VKOExvQVdhWUtXekduVGt1eDdrUmtwTTNBSko1a2M"
}

View File

@ -0,0 +1,9 @@
-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBAK9fx7qOJT7Aoseu7KKgaLagBh3wvDzg7F/ZMtGbPFikJnnvRWvF
B5oEGbMPblvtF0/fjqfu+eqjP3Z1tUSn7TkCAwEAAQJABUY5KIgr4JZEjwLYxQ9T
9uIbLP1Xe/E7yqoqmBk2GGhSrPY0OeRkYnUVLcP96UPQhF63iuG8VF6uZ7oAPsq+
gQIhANZy3jSCzPjXYHRU1kRqQzpt2S+OqoEiqQ6YG1HrC/VxAiEA0Vq6JlQK2tOX
37SS00dK0Qog4Qi8dN73GliFQNP18EkCIQC4epSA48zkfJMzQBAbRraSuxDNApPX
BzQbo+pMrEDbYQIgY4AncQgIkLB4Qk5kah48JNYXglzQlQtTjiX8Ty9ueGECIQCM
GD3UbQKiA0gf5plBA24I4wFVKxxa4wXbW/7SfP6XmQ==
-----END RSA PRIVATE KEY-----

View File

@ -75,37 +75,26 @@ groups() ->
%%
%% 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,
wapi_woody_client
]
})
], Config).
-spec init_per_suite(config()) -> config().
init_per_suite(C) ->
wapi_ct_helper:init_suite(?MODULE, C).
-spec end_per_suite(config()) -> _.
-spec end_per_suite(config()) ->
_.
end_per_suite(C) ->
%% TODO remove this after cut off wapi
ok = application:unset_env(wapi, transport),
ok = ct_payment_system:shutdown(C).
_ = wapi_ct_helper:stop_mocked_service_sup(?config(suite_test_sup, C)),
_ = [application:stop(App) || App <- ?config(apps, C)],
ok.
-spec init_per_group(group_name(), config()) ->
config().
init_per_group(Group, Config) when Group =:= base ->
ok = ff_context:save(ff_context:create(#{
ok = wapi_context:save(wapi_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),
Party = genlib:bsuuid(),
{ok, Token} = wapi_ct_helper:issue_token(Party, [{[party], write}], unlimited, ?DOMAIN),
Config1 = [{party, Party} | Config],
[{context, wapi_ct_helper:get_context(Token)} | Config1];
@ -120,14 +109,14 @@ end_per_group(_Group, _C) ->
-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),
C1 = wapi_ct_helper:makeup_cfg([wapi_ct_helper:test_case_name(Name), wapi_ct_helper:woody_ctx()], C),
ok = wapi_context:save(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(),
ok = wapi_context:cleanup(),
wapi_ct_helper:stop_mocked_service_sup(?config(test_sup, C)),
ok.
@ -137,6 +126,7 @@ end_per_testcase(_Name, C) ->
create_identity(C) ->
PartyID = ?config(party, C),
wapi_ct_helper:mock_services([
{bender_thrift, fun('GenerateID', _) -> {ok, ?GENERATE_ID_RESULT} end},
{fistful_identity, fun('Create', _) -> {ok, ?IDENTITY(PartyID)} end}
], C),
{ok, _} = call_api(
@ -151,7 +141,7 @@ create_identity(C) ->
}
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec get_identity(config()) ->
@ -168,7 +158,7 @@ get_identity(C) ->
<<"identityID">> => ?STRING
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec create_identity_challenge(config()) ->
@ -180,6 +170,7 @@ create_identity_challenge(C) ->
('GetContext', _) -> {ok, ?DEFAULT_CONTEXT(PartyID)};
('StartChallenge', _) -> {ok, ?IDENTITY_CHALLENGE(?IDENTITY_CHALLENGE_STATUS_COMPLETED)}
end},
{bender_thrift, fun('GenerateID', _) -> {ok, ?GENERATE_ID_RESULT} end},
{identdoc_storage, fun('Get', _) -> {ok, ?IDENT_DOC} end}
], C),
{ok, _} = call_api(
@ -206,7 +197,7 @@ create_identity_challenge(C) ->
]
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec get_identity_challenge(config()) ->
@ -228,7 +219,7 @@ get_identity_challenge(C) ->
<<"challengeID">> => ?STRING
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec list_identity_challenges(config()) ->
@ -252,7 +243,7 @@ list_identity_challenges(C) ->
<<"status">> => <<"Completed">>
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec get_identity_challenge_event(config()) ->
@ -274,7 +265,7 @@ get_identity_challenge_event(C) ->
<<"eventID">> => ?INTEGER
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec poll_identity_challenge_events(config()) ->
@ -299,7 +290,7 @@ poll_identity_challenge_events(C) ->
<<"eventCursor">> => ?INTEGER
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
%%
@ -310,8 +301,3 @@ 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.

View File

@ -0,0 +1,7 @@
{
"use": "enc",
"kty": "oct",
"kid": "1",
"alg": "dir",
"k": "M3VKOExvQVdhWUtXekduVGt1eDdrUmtwTTNBSko1a2M"
}

View File

@ -0,0 +1,9 @@
-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBAK9fx7qOJT7Aoseu7KKgaLagBh3wvDzg7F/ZMtGbPFikJnnvRWvF
B5oEGbMPblvtF0/fjqfu+eqjP3Z1tUSn7TkCAwEAAQJABUY5KIgr4JZEjwLYxQ9T
9uIbLP1Xe/E7yqoqmBk2GGhSrPY0OeRkYnUVLcP96UPQhF63iuG8VF6uZ7oAPsq+
gQIhANZy3jSCzPjXYHRU1kRqQzpt2S+OqoEiqQ6YG1HrC/VxAiEA0Vq6JlQK2tOX
37SS00dK0Qog4Qi8dN73GliFQNP18EkCIQC4epSA48zkfJMzQBAbRraSuxDNApPX
BzQbo+pMrEDbYQIgY4AncQgIkLB4Qk5kah48JNYXglzQlQtTjiX8Ty9ueGECIQCM
GD3UbQKiA0gf5plBA24I4wFVKxxa4wXbW/7SfP6XmQ==
-----END RSA PRIVATE KEY-----

View File

@ -70,37 +70,26 @@ groups() ->
%%
%% 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,
wapi_woody_client
]
})
], Config).
-spec init_per_suite(config()) -> config().
init_per_suite(C) ->
wapi_ct_helper:init_suite(?MODULE, C).
-spec end_per_suite(config()) -> _.
-spec end_per_suite(config()) ->
_.
end_per_suite(C) ->
%% TODO remove this after cut off wapi
ok = application:unset_env(wapi, transport),
ok = ct_payment_system:shutdown(C).
_ = wapi_ct_helper:stop_mocked_service_sup(?config(suite_test_sup, C)),
_ = [application:stop(App) || App <- ?config(apps, C)],
ok.
-spec init_per_group(group_name(), config()) ->
config().
init_per_group(Group, Config) when Group =:= base ->
ok = ff_context:save(ff_context:create(#{
ok = wapi_context:save(wapi_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),
Party = genlib:bsuuid(),
{ok, Token} = wapi_ct_helper:issue_token(Party, [{[party], write}], unlimited, ?DOMAIN),
Config1 = [{party, Party} | Config],
[{context, wapi_ct_helper:get_context(Token)} | Config1];
@ -115,14 +104,14 @@ end_per_group(_Group, _C) ->
-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),
C1 = wapi_ct_helper:makeup_cfg([wapi_ct_helper:test_case_name(Name), wapi_ct_helper:woody_ctx()], C),
ok = wapi_context:save(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(),
ok = wapi_context:cleanup(),
wapi_ct_helper:stop_mocked_service_sup(?config(test_sup, C)),
ok.
@ -148,7 +137,7 @@ create_report_ok_test(C) ->
<<"toTime">> => ?TIMESTAMP
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec get_report_ok_test(config()) ->
@ -167,7 +156,7 @@ get_report_ok_test(C) ->
<<"reportID">> => ?INTEGER
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec get_reports_ok_test(config()) ->
@ -193,7 +182,7 @@ get_reports_ok_test(C) ->
<<"type">> => <<"withdrawalRegistry">>
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec reports_with_wrong_identity_ok_test(config()) ->
@ -217,7 +206,7 @@ reports_with_wrong_identity_ok_test(C) ->
<<"toTime">> => ?TIMESTAMP
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
),
?emptyresp(400) = call_api(
fun swag_client_wallet_reports_api:get_report/3,
@ -227,7 +216,7 @@ reports_with_wrong_identity_ok_test(C) ->
<<"reportID">> => ?INTEGER
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
),
?emptyresp(400) = call_api(
fun swag_client_wallet_reports_api:get_reports/3,
@ -241,7 +230,7 @@ reports_with_wrong_identity_ok_test(C) ->
<<"type">> => <<"withdrawalRegistry">>
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec download_file_ok_test(config()) ->
@ -260,7 +249,7 @@ download_file_ok_test(C) ->
<<"expiresAt">> => ?TIMESTAMP
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
%%
@ -272,11 +261,6 @@ call_api(F, Params, Context) ->
Response = F(Url, PreparedParams, Opts),
wapi_client_lib:handle_response(Response).
create_party(_C) ->
ID = genlib:bsuuid(),
_ = ff_party:create(ID),
ID.
create_identity(C) ->
PartyID = ?config(party, C),
Params = #{
@ -291,6 +275,6 @@ create_context(PartyID, C) ->
create_woody_ctx(C) ->
#{
woody_context => ct_helper:get_woody_ctx(C)
woody_context => wapi_ct_helper:get_woody_ctx(C)
}.

View File

@ -0,0 +1,7 @@
{
"use": "enc",
"kty": "oct",
"kid": "1",
"alg": "dir",
"k": "M3VKOExvQVdhWUtXekduVGt1eDdrUmtwTTNBSko1a2M"
}

View File

@ -0,0 +1,9 @@
-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBAK9fx7qOJT7Aoseu7KKgaLagBh3wvDzg7F/ZMtGbPFikJnnvRWvF
B5oEGbMPblvtF0/fjqfu+eqjP3Z1tUSn7TkCAwEAAQJABUY5KIgr4JZEjwLYxQ9T
9uIbLP1Xe/E7yqoqmBk2GGhSrPY0OeRkYnUVLcP96UPQhF63iuG8VF6uZ7oAPsq+
gQIhANZy3jSCzPjXYHRU1kRqQzpt2S+OqoEiqQ6YG1HrC/VxAiEA0Vq6JlQK2tOX
37SS00dK0Qog4Qi8dN73GliFQNP18EkCIQC4epSA48zkfJMzQBAbRraSuxDNApPX
BzQbo+pMrEDbYQIgY4AncQgIkLB4Qk5kah48JNYXglzQlQtTjiX8Ty9ueGECIQCM
GD3UbQKiA0gf5plBA24I4wFVKxxa4wXbW/7SfP6XmQ==
-----END RSA PRIVATE KEY-----

View File

@ -87,42 +87,27 @@ groups() ->
%%
%% 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 init_per_suite(config()) -> config().
init_per_suite(C) ->
wapi_ct_helper:init_suite(?MODULE, C).
-spec end_per_suite(config()) -> _.
-spec end_per_suite(config()) ->
_.
end_per_suite(C) ->
%% TODO remove this after cut off wapi
ok = application:unset_env(wapi, transport),
ok = ct_payment_system:shutdown(C).
_ = wapi_ct_helper:stop_mocked_service_sup(?config(suite_test_sup, C)),
_ = [application:stop(App) || App <- ?config(apps, C)],
ok.
-spec init_per_group(group_name(), config()) ->
config().
init_per_group(Group, Config) when Group =:= base ->
ok = ff_context:save(ff_context:create(#{
ok = wapi_context:save(wapi_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),
Party = genlib:bsuuid(),
{ok, Token} = wapi_ct_helper:issue_token(Party, [{[party], write}], unlimited, ?DOMAIN),
Config1 = [{party, Party} | Config],
[{context, wapi_ct_helper:get_context(Token)} | Config1];
init_per_group(_, Config) ->
@ -136,14 +121,14 @@ end_per_group(_Group, _C) ->
-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),
C1 = wapi_ct_helper:makeup_cfg([wapi_ct_helper:test_case_name(Name), wapi_ct_helper:woody_ctx()], C),
ok = wapi_context:save(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(),
ok = wapi_context:cleanup(),
wapi_ct_helper:stop_mocked_service_sup(?config(test_sup, C)),
ok.
@ -162,7 +147,7 @@ list_wallets(C) ->
<<"limit">> => <<"123">>
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec list_wallets_invalid_error(config()) ->
@ -194,7 +179,7 @@ list_withdrawals(C) ->
<<"limit">> => <<"123">>
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec list_withdrawals_invalid_error(config()) ->
@ -226,7 +211,7 @@ list_deposits(C) ->
<<"limit">> => <<"123">>
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec list_deposits_invalid_error(config()) ->
@ -258,7 +243,7 @@ list_destinations(C) ->
<<"limit">> => <<"123">>
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec list_destinations_invalid_error(config()) ->
@ -290,7 +275,7 @@ list_identities(C) ->
<<"limit">> => <<"123">>
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec list_identities_invalid_error(config()) ->
@ -328,7 +313,7 @@ check_error(Error, MockFunc, SwagFunc, C) ->
<<"limit">> => <<"123">>
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec call_api(function(), map(), wapi_client_lib:context()) ->
@ -337,8 +322,3 @@ 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.

View File

@ -0,0 +1,7 @@
{
"use": "enc",
"kty": "oct",
"kid": "1",
"alg": "dir",
"k": "M3VKOExvQVdhWUtXekduVGt1eDdrUmtwTTNBSko1a2M"
}

View File

@ -0,0 +1,9 @@
-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBAK9fx7qOJT7Aoseu7KKgaLagBh3wvDzg7F/ZMtGbPFikJnnvRWvF
B5oEGbMPblvtF0/fjqfu+eqjP3Z1tUSn7TkCAwEAAQJABUY5KIgr4JZEjwLYxQ9T
9uIbLP1Xe/E7yqoqmBk2GGhSrPY0OeRkYnUVLcP96UPQhF63iuG8VF6uZ7oAPsq+
gQIhANZy3jSCzPjXYHRU1kRqQzpt2S+OqoEiqQ6YG1HrC/VxAiEA0Vq6JlQK2tOX
37SS00dK0Qog4Qi8dN73GliFQNP18EkCIQC4epSA48zkfJMzQBAbRraSuxDNApPX
BzQbo+pMrEDbYQIgY4AncQgIkLB4Qk5kah48JNYXglzQlQtTjiX8Ty9ueGECIQCM
GD3UbQKiA0gf5plBA24I4wFVKxxa4wXbW/7SfP6XmQ==
-----END RSA PRIVATE KEY-----

View File

@ -59,6 +59,29 @@
context = ?DEFAULT_CONTEXT(PartyID)
}).
-define(WITHDRAWAL_QUOTE, #wthd_Quote{
cash_from = ?CASH,
cash_to = ?CASH,
created_at = ?TIMESTAMP,
expires_on = ?TIMESTAMP,
operation_timestamp = ?TIMESTAMP,
domain_revision = 123,
party_revision = 123,
route = #wthd_Route{
provider_id = 123,
terminal_id = 123
},
quote_data = {str, ?STRING}
}).
-define(WITHDRAWAL_EVENT(Change), #wthd_Event{
change = Change,
occured_at = ?TIMESTAMP,
event_id = ?INTEGER
}).
-define(WITHDRAWAL_STATUS_CHANGE, {status_changed, #wthd_StatusChange{status = {pending, #wthd_status_Pending{}}}}).
-define(BLOCKING, unblocked).
-define(ACCOUNT, #account_Account{
@ -205,7 +228,7 @@
id = ?STRING,
name = ?STRING,
created_at = ?TIMESTAMP,
provider = ?STRING,
provider = ?INTEGER,
identity_class = ?STRING,
identity_level = ?STRING,
effective_challenge = ?STRING,

View File

@ -68,42 +68,27 @@ groups() ->
%%
%% 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 init_per_suite(config()) -> config().
init_per_suite(C) ->
wapi_ct_helper:init_suite(?MODULE, C).
-spec end_per_suite(config()) -> _.
-spec end_per_suite(config()) ->
_.
end_per_suite(C) ->
%% TODO remove this after cut off wapi
ok = application:unset_env(wapi, transport),
ok = ct_payment_system:shutdown(C).
_ = wapi_ct_helper:stop_mocked_service_sup(?config(suite_test_sup, C)),
_ = [application:stop(App) || App <- ?config(apps, C)],
ok.
-spec init_per_group(group_name(), config()) ->
config().
init_per_group(Group, Config) when Group =:= base ->
ok = ff_context:save(ff_context:create(#{
ok = wapi_context:save(wapi_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),
Party = genlib:bsuuid(),
{ok, Token} = wapi_ct_helper:issue_token(Party, [{[party], write}], unlimited, ?DOMAIN),
Config1 = [{party, Party} | Config],
[{context, wapi_ct_helper:get_context(Token)} | Config1];
init_per_group(_, Config) ->
@ -117,14 +102,14 @@ end_per_group(_Group, _C) ->
-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),
C1 = wapi_ct_helper:makeup_cfg([wapi_ct_helper:test_case_name(Name), wapi_ct_helper:woody_ctx()], C),
ok = wapi_context:save(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(),
ok = wapi_context:cleanup(),
wapi_ct_helper:stop_mocked_service_sup(?config(test_sup, C)),
ok.
@ -135,6 +120,7 @@ end_per_testcase(_Name, C) ->
create(C) ->
PartyID = ?config(party, C),
wapi_ct_helper:mock_services([
{bender_thrift, fun('GenerateID', _) -> {ok, ?GENERATE_ID_RESULT} end},
{fistful_identity, fun('GetContext', _) -> {ok, ?DEFAULT_CONTEXT(PartyID)} end},
{fistful_wallet, fun('Create', _) -> {ok, ?WALLET(PartyID)} end}
], C),
@ -150,7 +136,7 @@ create(C) ->
}
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec get(config()) ->
@ -167,7 +153,7 @@ get(C) ->
<<"walletID">> => ?STRING
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec get_by_external_id(config()) ->
@ -185,7 +171,7 @@ get_by_external_id(C) ->
<<"externalID">> => ?STRING
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec get_account(config()) ->
@ -207,7 +193,7 @@ get_account(C) ->
<<"walletID">> => ?STRING
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
%%
@ -218,8 +204,3 @@ 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.

View File

@ -0,0 +1,7 @@
{
"use": "enc",
"kty": "oct",
"kid": "1",
"alg": "dir",
"k": "M3VKOExvQVdhWUtXekduVGt1eDdrUmtwTTNBSko1a2M"
}

View File

@ -0,0 +1,9 @@
-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBAK9fx7qOJT7Aoseu7KKgaLagBh3wvDzg7F/ZMtGbPFikJnnvRWvF
B5oEGbMPblvtF0/fjqfu+eqjP3Z1tUSn7TkCAwEAAQJABUY5KIgr4JZEjwLYxQ9T
9uIbLP1Xe/E7yqoqmBk2GGhSrPY0OeRkYnUVLcP96UPQhF63iuG8VF6uZ7oAPsq+
gQIhANZy3jSCzPjXYHRU1kRqQzpt2S+OqoEiqQ6YG1HrC/VxAiEA0Vq6JlQK2tOX
37SS00dK0Qog4Qi8dN73GliFQNP18EkCIQC4epSA48zkfJMzQBAbRraSuxDNApPX
BzQbo+pMrEDbYQIgY4AncQgIkLB4Qk5kah48JNYXglzQlQtTjiX8Ty9ueGECIQCM
GD3UbQKiA0gf5plBA24I4wFVKxxa4wXbW/7SfP6XmQ==
-----END RSA PRIVATE KEY-----

View File

@ -69,37 +69,26 @@ groups() ->
%%
%% 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 init_per_suite(config()) -> config().
init_per_suite(C) ->
wapi_ct_helper:init_suite(?MODULE, C).
-spec end_per_suite(config()) -> _.
-spec end_per_suite(config()) ->
_.
end_per_suite(C) ->
%% TODO remove this after cut off wapi
ok = application:unset_env(wapi, transport),
ok = ct_payment_system:shutdown(C).
_ = wapi_ct_helper:stop_mocked_service_sup(?config(suite_test_sup, C)),
_ = [application:stop(App) || App <- ?config(apps, C)],
ok.
-spec init_per_group(group_name(), config()) ->
config().
init_per_group(Group, Config) when Group =:= base ->
ok = ff_context:save(ff_context:create(#{
ok = wapi_context:save(wapi_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),
Party = genlib:bsuuid(),
{ok, Token} = wapi_ct_helper:issue_token(Party, [{[party], write}], unlimited, ?DOMAIN),
Config1 = [{party, Party} | Config],
[{context, wapi_ct_helper:get_context(Token)} | Config1];
@ -114,14 +103,14 @@ end_per_group(_Group, _C) ->
-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),
C1 = wapi_ct_helper:makeup_cfg([wapi_ct_helper:test_case_name(Name), wapi_ct_helper:woody_ctx()], C),
ok = wapi_context:save(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(),
ok = wapi_context:cleanup(),
wapi_ct_helper:stop_mocked_service_sup(?config(test_sup, C)),
ok.
@ -147,7 +136,7 @@ create_webhook_ok_test(C) ->
}
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec get_webhooks_ok_test(config()) ->
@ -165,7 +154,7 @@ get_webhooks_ok_test(C) ->
<<"identityID">> => IdentityID
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec get_webhook_ok_test(config()) ->
@ -186,7 +175,7 @@ get_webhook_ok_test(C) ->
<<"identityID">> => IdentityID
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec delete_webhook_ok_test(config()) ->
@ -207,7 +196,7 @@ delete_webhook_ok_test(C) ->
<<"identityID">> => IdentityID
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
%%
@ -219,11 +208,6 @@ call_api(F, Params, Context) ->
Response = F(Url, PreparedParams, Opts),
wapi_client_lib:handle_response(Response).
create_party(_C) ->
ID = genlib:bsuuid(),
_ = ff_party:create(ID),
ID.
create_identity(C) ->
PartyID = ?config(party, C),
Params = #{
@ -238,5 +222,5 @@ create_context(PartyID, C) ->
create_woody_ctx(C) ->
#{
woody_context => ct_helper:get_woody_ctx(C)
woody_context => wapi_ct_helper:get_woody_ctx(C)
}.

View File

@ -0,0 +1,7 @@
{
"use": "enc",
"kty": "oct",
"kid": "1",
"alg": "dir",
"k": "M3VKOExvQVdhWUtXekduVGt1eDdrUmtwTTNBSko1a2M"
}

View File

@ -0,0 +1,9 @@
-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBAK9fx7qOJT7Aoseu7KKgaLagBh3wvDzg7F/ZMtGbPFikJnnvRWvF
B5oEGbMPblvtF0/fjqfu+eqjP3Z1tUSn7TkCAwEAAQJABUY5KIgr4JZEjwLYxQ9T
9uIbLP1Xe/E7yqoqmBk2GGhSrPY0OeRkYnUVLcP96UPQhF63iuG8VF6uZ7oAPsq+
gQIhANZy3jSCzPjXYHRU1kRqQzpt2S+OqoEiqQ6YG1HrC/VxAiEA0Vq6JlQK2tOX
37SS00dK0Qog4Qi8dN73GliFQNP18EkCIQC4epSA48zkfJMzQBAbRraSuxDNApPX
BzQbo+pMrEDbYQIgY4AncQgIkLB4Qk5kah48JNYXglzQlQtTjiX8Ty9ueGECIQCM
GD3UbQKiA0gf5plBA24I4wFVKxxa4wXbW/7SfP6XmQ==
-----END RSA PRIVATE KEY-----

View File

@ -23,7 +23,10 @@
-export([
create/1,
get/1,
get_by_external_id/1
get_by_external_id/1,
create_quote/1,
get_events/1,
get_event/1
]).
% common-api is used since it is the domain used in production RN
@ -58,7 +61,10 @@ groups() ->
[
create,
get,
get_by_external_id
get_by_external_id,
create_quote,
get_events,
get_event
]
}
].
@ -66,42 +72,27 @@ groups() ->
%%
%% 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 init_per_suite(config()) -> config().
init_per_suite(C) ->
wapi_ct_helper:init_suite(?MODULE, C).
-spec end_per_suite(config()) -> _.
-spec end_per_suite(config()) ->
_.
end_per_suite(C) ->
%% TODO remove this after cut off wapi
ok = application:unset_env(wapi, transport),
ok = ct_payment_system:shutdown(C).
_ = wapi_ct_helper:stop_mocked_service_sup(?config(suite_test_sup, C)),
_ = [application:stop(App) || App <- ?config(apps, C)],
ok.
-spec init_per_group(group_name(), config()) ->
config().
init_per_group(Group, Config) when Group =:= base ->
ok = ff_context:save(ff_context:create(#{
ok = wapi_context:save(wapi_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),
Party = genlib:bsuuid(),
{ok, Token} = wapi_ct_helper:issue_token(Party, [{[party], write}], unlimited, ?DOMAIN),
Config1 = [{party, Party} | Config],
[{context, wapi_ct_helper:get_context(Token)} | Config1];
init_per_group(_, Config) ->
@ -115,14 +106,14 @@ end_per_group(_Group, _C) ->
-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),
C1 = wapi_ct_helper:makeup_cfg([wapi_ct_helper:test_case_name(Name), wapi_ct_helper:woody_ctx()], C),
ok = wapi_context:save(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(),
ok = wapi_context:cleanup(),
wapi_ct_helper:stop_mocked_service_sup(?config(test_sup, C)),
ok.
@ -133,6 +124,7 @@ end_per_testcase(_Name, C) ->
create(C) ->
PartyID = ?config(party, C),
wapi_ct_helper:mock_services([
{bender_thrift, fun('GenerateID', _) -> {ok, ?GENERATE_ID_RESULT} end},
{fistful_wallet, fun('GetContext', _) -> {ok, ?DEFAULT_CONTEXT(PartyID)} end},
{fistful_destination, fun('GetContext', _) -> {ok, ?DEFAULT_CONTEXT(PartyID)} end},
{fistful_withdrawal, fun('Create', _) -> {ok, ?WITHDRAWAL(PartyID)} end}
@ -148,7 +140,7 @@ create(C) ->
<<"currency">> => <<"RUB">>
}
})},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec get(config()) ->
@ -165,7 +157,7 @@ get(C) ->
<<"withdrawalID">> => ?STRING
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec get_by_external_id(config()) ->
@ -183,7 +175,82 @@ get_by_external_id(C) ->
<<"externalID">> => ?STRING
}
},
ct_helper:cfg(context, C)
wapi_ct_helper:cfg(context, C)
).
-spec create_quote(config()) ->
_.
create_quote(C) ->
PartyID = ?config(party, C),
wapi_ct_helper:mock_services([
{fistful_wallet, fun('GetContext', _) -> {ok, ?DEFAULT_CONTEXT(PartyID)} end},
{fistful_destination, fun('GetContext', _) -> {ok, ?DEFAULT_CONTEXT(PartyID)} end},
{fistful_withdrawal, fun('GetQuote', _) -> {ok, ?WITHDRAWAL_QUOTE} end}
], C),
{ok, _} = call_api(
fun swag_client_wallet_withdrawals_api:create_quote/3,
#{
body => genlib_map:compact(#{
<<"walletID">> => ?STRING,
<<"destinationID">> => ?STRING,
<<"currencyFrom">> => <<"RUB">>,
<<"currencyTo">> => <<"USD">>,
<<"cash">> => #{
<<"amount">> => 100,
<<"currency">> => <<"RUB">>
}
})},
wapi_ct_helper:cfg(context, C)
).
-spec get_events(config()) ->
_.
get_events(C) ->
PartyID = ?config(party, C),
wapi_ct_helper:mock_services([
{fistful_withdrawal,
fun
('GetContext', _) -> {ok, ?DEFAULT_CONTEXT(PartyID)};
('GetEvents', [_, #'EventRange'{limit = 0}]) -> {ok, []};
('GetEvents', _) -> {ok, [?WITHDRAWAL_EVENT(?WITHDRAWAL_STATUS_CHANGE)]}
end
}
], C),
{ok, _} = call_api(
fun swag_client_wallet_withdrawals_api:poll_withdrawal_events/3,
#{
binding => #{
<<"withdrawalID">> => ?STRING
},
qs_val => #{
<<"limit">> => 10
}
},
wapi_ct_helper:cfg(context, C)
).
-spec get_event(config()) ->
_.
get_event(C) ->
PartyID = ?config(party, C),
wapi_ct_helper:mock_services([
{fistful_withdrawal,
fun
('GetContext', _) -> {ok, ?DEFAULT_CONTEXT(PartyID)};
('GetEvents', [_, #'EventRange'{limit = 0}]) -> {ok, []};
('GetEvents', _) -> {ok, [?WITHDRAWAL_EVENT(?WITHDRAWAL_STATUS_CHANGE)]}
end
}
], C),
{ok, _} = call_api(
fun swag_client_wallet_withdrawals_api:get_withdrawal_events/3,
#{
binding => #{
<<"withdrawalID">> => ?STRING,
<<"eventID">> => ?INTEGER
}
},
wapi_ct_helper:cfg(context, C)
).
%%
@ -194,8 +261,3 @@ 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.

View File

@ -0,0 +1,7 @@
{
"use": "enc",
"kty": "oct",
"kid": "1",
"alg": "dir",
"k": "M3VKOExvQVdhWUtXekduVGt1eDdrUmtwTTNBSko1a2M"
}

View File

@ -0,0 +1,9 @@
-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBAK9fx7qOJT7Aoseu7KKgaLagBh3wvDzg7F/ZMtGbPFikJnnvRWvF
B5oEGbMPblvtF0/fjqfu+eqjP3Z1tUSn7TkCAwEAAQJABUY5KIgr4JZEjwLYxQ9T
9uIbLP1Xe/E7yqoqmBk2GGhSrPY0OeRkYnUVLcP96UPQhF63iuG8VF6uZ7oAPsq+
gQIhANZy3jSCzPjXYHRU1kRqQzpt2S+OqoEiqQ6YG1HrC/VxAiEA0Vq6JlQK2tOX
37SS00dK0Qog4Qi8dN73GliFQNP18EkCIQC4epSA48zkfJMzQBAbRraSuxDNApPX
BzQbo+pMrEDbYQIgY4AncQgIkLB4Qk5kah48JNYXglzQlQtTjiX8Ty9ueGECIQCM
GD3UbQKiA0gf5plBA24I4wFVKxxa4wXbW/7SfP6XmQ==
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,141 @@
-module(wapi_context).
-export([create/0]).
-export([create/1]).
-export([save/1]).
-export([load/0]).
-export([cleanup/0]).
-export([get_woody_context/1]).
-export([set_woody_context/2]).
-export([get_user_identity/1]).
-export([set_user_identity/2]).
-export([get_party_client_context/1]).
-export([set_party_client_context/2]).
-export([get_party_client/1]).
-export([set_party_client/2]).
-opaque context() :: #{
woody_context := woody_context(),
party_client_context := party_client_context(),
party_client => party_client(),
user_identity => user_identity()
}.
-type options() :: #{
party_client => party_client(),
user_identity => user_identity(),
woody_context => woody_context(),
party_client_context => party_client_context()
}.
-export_type([context/0]).
-export_type([options/0]).
%% Internal types
-type user_identity() :: woody_user_identity:user_identity().
-type woody_context() :: woody_context:ctx().
-type party_client() :: party_client:client().
-type party_client_context() :: party_client:context().
-define(REGISTRY_KEY, {p, l, {?MODULE, stored_context}}).
%% API
-spec create() -> context().
create() ->
create(#{}).
-spec create(options()) -> context().
create(Options0) ->
Options1 = ensure_woody_context_exists(Options0),
ensure_party_context_exists(Options1).
-spec save(context()) -> ok.
save(Context) ->
true = try gproc:reg(?REGISTRY_KEY, Context)
catch
error:badarg ->
gproc:set_value(?REGISTRY_KEY, Context)
end,
ok.
-spec load() -> context() | no_return().
load() ->
gproc:get_value(?REGISTRY_KEY).
-spec cleanup() -> ok.
cleanup() ->
true = gproc:unreg(?REGISTRY_KEY),
ok.
-spec get_woody_context(context()) -> woody_context().
get_woody_context(Context) ->
#{woody_context := WoodyContext} = ensure_woody_user_info_set(Context),
WoodyContext.
-spec set_woody_context(woody_context(), context()) -> context().
set_woody_context(WoodyContext, #{party_client_context := PartyContext0} = Context) ->
PartyContext1 = party_client_context:set_woody_context(WoodyContext, PartyContext0),
Context#{
woody_context => WoodyContext,
party_client_context => PartyContext1
}.
-spec get_party_client(context()) -> party_client().
get_party_client(#{party_client := PartyClient}) ->
PartyClient;
get_party_client(Context) ->
error(no_party_client, [Context]).
-spec set_party_client(party_client(), context()) -> context().
set_party_client(PartyClient, Context) ->
Context#{party_client => PartyClient}.
-spec get_party_client_context(context()) -> party_client_context().
get_party_client_context(Context) ->
#{party_client_context := PartyContext} = ensure_party_user_info_set(Context),
PartyContext.
-spec set_party_client_context(party_client_context(), context()) -> context().
set_party_client_context(PartyContext, Context) ->
Context#{party_client_context := PartyContext}.
-spec get_user_identity(context()) -> user_identity() | no_return().
get_user_identity(#{user_identity := Identity}) ->
Identity;
get_user_identity(Context) ->
WoodyContext = get_woody_context(Context),
woody_user_identity:get(WoodyContext).
-spec set_user_identity(user_identity(), context()) -> context().
set_user_identity(Identity, Context) ->
Context#{user_identity => Identity}.
%% Internal functions
-spec ensure_woody_context_exists(options()) -> options().
ensure_woody_context_exists(#{woody_context := _WoodyContext} = Options) ->
Options;
ensure_woody_context_exists(Options) ->
Options#{woody_context => woody_context:new()}.
-spec ensure_party_context_exists(options()) -> options().
ensure_party_context_exists(#{party_client_context := _PartyContext} = Options) ->
Options;
ensure_party_context_exists(#{woody_context := WoodyContext} = Options) ->
Options#{party_client_context => party_client:create_context(#{woody_context => WoodyContext})}.
-spec ensure_woody_user_info_set(context()) -> context().
ensure_woody_user_info_set(#{user_identity := Identity, woody_context := WoodyContext} = Context) ->
NewWoodyContext = woody_user_identity:put(Identity, WoodyContext),
Context#{woody_context := NewWoodyContext};
ensure_woody_user_info_set(Context) ->
Context.
-spec ensure_party_user_info_set(context()) -> context().
ensure_party_user_info_set(#{user_identity := Identity, party_client_context := PartyContext} = Context) ->
NewPartyContext = party_client_context:set_user_info(Identity, PartyContext),
Context#{party_client_context := NewPartyContext};
ensure_party_user_info_set(Context) ->
Context.

View File

@ -0,0 +1,19 @@
{application, wapi_core, [
{description,
"Core types and facilities"
},
{vsn, "1"},
{registered, []},
{applications, [
kernel,
stdlib,
genlib
]},
{env, []},
{modules, []},
{maintainers, [
"Andrey Mayorov <a.mayorov@rbkmoney.com>"
]},
{licenses, []},
{links, ["https://github.com/rbkmoney/wapi-v2"]}
]}.

View File

@ -0,0 +1,41 @@
%%%
%%% Errors
-module(wapi_failure).
-type code() :: binary().
-type reason() :: binary().
-type failure() :: #{
code := code(),
reason => reason(),
sub => sub_failure()
}.
-type sub_failure() :: #{
code := code(),
sub => sub_failure()
}.
-export_type([code/0]).
-export_type([reason/0]).
-export_type([failure/0]).
-export_type([sub_failure/0]).
-export([code/1]).
-export([reason/1]).
-export([sub_failure/1]).
%% API
-spec code(failure() | sub_failure()) -> code().
code(#{code := Code}) ->
Code.
-spec reason(failure()) -> reason() | undefined.
reason(Failure) ->
maps:get(reason, Failure, undefined).
-spec sub_failure(failure() | sub_failure()) -> sub_failure() | undefined.
sub_failure(Failure) ->
maps:get(sub, Failure, undefined).

View File

@ -0,0 +1,20 @@
%%
%% Identificators-related utils
-module(wapi_id).
-export([generate_snowflake_id/0]).
%% Types
-type binary_id() :: binary().
-export_type([binary_id/0]).
%% API
-spec generate_snowflake_id() ->
binary_id().
generate_snowflake_id() ->
<<ID:64>> = snowflake:new(),
genlib_format:format_int_base(ID, 62).

View File

@ -0,0 +1,119 @@
%%%
%%% Pipeline
%%%
%%% TODO
%%% - A simple `ok` as a possible result only make everything more complex
%%%
-module(wapi_pipeline).
-export([do/1]).
-export([do/2]).
-export([unwrap/1]).
-export([unwrap/2]).
-export([expect/2]).
-export([flip/1]).
-export([valid/2]).
-export([with/3]).
%%
-type thrown(_E) ::
no_return().
-type result(T, E) ::
{ok, T} | {error, E}.
-spec do(fun(() -> ok | T | thrown(E))) ->
ok | result(T, E).
do(Fun) ->
try Fun() of
ok ->
ok;
R ->
{ok, R}
catch
Thrown -> {error, Thrown}
end.
-spec do(Tag, fun(() -> ok | T | thrown(E))) ->
ok | result(T, {Tag, E}).
do(Tag, Fun) ->
do(fun () -> unwrap(Tag, do(Fun)) end).
-spec unwrap
(ok) -> ok;
({ok, V}) -> V;
({error, E}) -> thrown(E).
unwrap(ok) ->
ok;
unwrap({ok, V}) ->
V;
unwrap({error, E}) ->
throw(E).
-spec expect
(_E, ok) -> ok;
(_E, {ok, V}) -> V;
( E, {error, _}) -> thrown(E).
expect(_, ok) ->
ok;
expect(_, {ok, V}) ->
V;
expect(E, {error, _}) ->
throw(E).
-spec flip(result(T, E)) ->
result(E, T).
flip({ok, T}) ->
{error, T};
flip({error, E}) ->
{ok, E}.
-spec unwrap
(_Tag, ok) -> ok;
(_Tag, {ok, V}) -> V;
( Tag, {error, E}) -> thrown({Tag, E}).
unwrap(_, ok) ->
ok;
unwrap(_, {ok, V}) ->
V;
unwrap(Tag, {error, E}) ->
throw({Tag, E}).
-spec valid(T, T) ->
ok | {error, T}.
valid(V, V) ->
ok;
valid(_, V) ->
{error, V}.
%% TODO
%% - Too complex
%% - Not the right place
-type outcome(E, R) ::
{ok, [E]} | {error, R}.
-spec with(Sub, St, fun((SubSt | undefined) -> outcome(SubEv, Reason))) ->
outcome({Sub, SubEv}, {Sub, Reason}) when
Sub :: atom(),
St :: #{Sub => SubSt},
SubSt :: _.
with(Model, St, F) ->
case F(maps:get(Model, St, undefined)) of
{ok, Events0} when is_list(Events0) ->
Events1 = [{Model, Ev} || Ev <- Events0],
{ok, Events1};
{error, Reason} ->
{error, {Model, Reason}}
end.

View File

@ -0,0 +1,71 @@
-module(wapi_thrift_utils).
-export([serialize/2]).
-export([deserialize/2]).
-spec serialize(thrift_type(), term()) -> binary().
-type thrift_type() ::
thrift_base_type() |
thrift_collection_type() |
thrift_enum_type() |
thrift_struct_type().
-type thrift_base_type() ::
bool |
double |
i8 |
i16 |
i32 |
i64 |
string.
-type thrift_collection_type() ::
{list, thrift_type()} |
{set, thrift_type()} |
{map, thrift_type(), thrift_type()}.
-type thrift_enum_type() ::
{enum, thrift_type_ref()}.
-type thrift_struct_type() ::
{struct, thrift_struct_flavor(), thrift_type_ref() | thrift_struct_def()}.
-type thrift_struct_flavor() :: struct | union | exception.
-type thrift_type_ref() :: {module(), Name :: atom()}.
-type thrift_struct_def() :: list({
Tag :: pos_integer(),
Requireness :: required | optional | undefined,
Type :: thrift_struct_type(),
Name :: atom(),
Default :: any()
}).
serialize(Type, Data) ->
{ok, Trans} = thrift_membuffer_transport:new(),
{ok, Proto} = new_protocol(Trans),
case thrift_protocol:write(Proto, {Type, Data}) of
{NewProto, ok} ->
{_, {ok, Result}} = thrift_protocol:close_transport(NewProto),
Result;
{_NewProto, {error, Reason}} ->
erlang:error({thrift, {protocol, Reason}})
end.
-spec deserialize(thrift_type(), binary()) ->
term().
deserialize(Type, Data) ->
{ok, Trans} = thrift_membuffer_transport:new(Data),
{ok, Proto} = new_protocol(Trans),
case thrift_protocol:read(Proto, Type) of
{_NewProto, {ok, Result}} ->
Result;
{_NewProto, {error, Reason}} ->
erlang:error({thrift, {protocol, Reason}})
end.
new_protocol(Trans) ->
thrift_binary_protocol:new(Trans, [{strict_read, true}, {strict_write, true}]).

View File

@ -0,0 +1,84 @@
%%%
%%% A matter of time.
-module(wapi_time).
-export([now/0]).
-export([to_rfc3339/1]).
-export([from_rfc3339/1]).
-export([add_interval/2]).
-export_type([timestamp_ms/0]).
-type timestamp_ms() :: integer().
-type year() :: integer().
-type month() :: integer().
-type day() :: integer().
-type hour() :: integer().
-type minute() :: integer().
-type second() :: integer().
-type date() :: {year(), month(), day()}.
-type time() :: {hour(), minute(), second()}.
-type datetime_interval() :: {date(), time()}.
%% API
-spec now() -> timestamp_ms().
now() ->
erlang:system_time(millisecond).
-spec to_rfc3339(timestamp_ms()) -> binary().
to_rfc3339(Timestamp) ->
genlib_rfc3339:format_relaxed(Timestamp, millisecond).
-spec from_rfc3339(binary()) -> timestamp_ms().
from_rfc3339(BTimestamp) ->
genlib_rfc3339:parse(BTimestamp, millisecond).
-spec add_interval(timestamp_ms(), datetime_interval()) ->
timestamp_ms().
add_interval(Timestamp, {Date, Time}) ->
Ms = Timestamp rem 1000,
TSSeconds = erlang:convert_time_unit(Timestamp, millisecond, second),
{D, T} = genlib_time:unixtime_to_daytime(TSSeconds),
NewDate = genlib_time:daytime_to_unixtime({genlib_time:shift_date(D, Date), T}),
DateTime = genlib_time:add_duration(NewDate, Time),
DateTime*1000 + Ms.
%% TESTS
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-spec test() -> _.
-spec rfc3339_symmetry_test() -> _.
rfc3339_symmetry_test() ->
TimestampStr = <<"2000-01-01T00:00:00Z">>,
?assertEqual(TimestampStr, to_rfc3339(from_rfc3339(TimestampStr))).
-spec add_second_interval_test() -> _.
add_second_interval_test() ->
Timestamp = wapi_time:now(),
NewTimestamp = add_interval(Timestamp, {{0, 0, 0}, {0, 0, 1}}),
?assertEqual(Timestamp + 1000, NewTimestamp).
-spec add_minute_interval_test() -> _.
add_minute_interval_test() ->
Timestamp = wapi_time:now(),
NewTimestamp = add_interval(Timestamp, {{0, 0, 0}, {0, 1, 0}}),
?assertEqual(Timestamp + 60*1000, NewTimestamp).
-spec add_hour_interval_test() -> _.
add_hour_interval_test() ->
Timestamp = wapi_time:now(),
NewTimestamp = add_interval(Timestamp, {{0, 0, 0}, {1, 0, 0}}),
?assertEqual(Timestamp + 60*60*1000, NewTimestamp).
-spec add_day_interval_test() -> _.
add_day_interval_test() ->
Timestamp = wapi_time:now(),
NewTimestamp = add_interval(Timestamp, {{0, 0, 1}, {0, 0, 0}}),
?assertEqual(Timestamp + 24*60*60*1000, NewTimestamp).
-endif.