fix: Revert everything & add erts (#335)

* Revert everything back to prometheus_metrics

* Include erts
This commit is contained in:
Toporkov Igor 2020-11-12 17:36:13 +03:00 committed by GitHub
parent a64579afc3
commit 01fb6d846d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 1277 additions and 2391 deletions

View File

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

View File

@ -41,17 +41,17 @@ marshal_destination_state(DestinationState, Context) ->
#dst_DestinationState{
id = marshal(id, ff_destination:id(DestinationState)),
name = marshal(string, ff_destination:name(DestinationState)),
resource = maybe_marshal(resource, ff_destination:resource(DestinationState)),
external_id = maybe_marshal(id, ff_destination:external_id(DestinationState)),
account = maybe_marshal(account, ff_destination:account(DestinationState)),
status = maybe_marshal(status, ff_destination:status(DestinationState)),
created_at = maybe_marshal(timestamp_ms, ff_destination:created_at(DestinationState)),
resource = marshal(resource, ff_destination:resource(DestinationState)),
external_id = marshal(id, ff_destination:external_id(DestinationState)),
account = marshal(account, ff_destination:account(DestinationState)),
status = marshal(status, ff_destination:status(DestinationState)),
created_at = marshal(timestamp_ms, ff_destination:created_at(DestinationState)),
blocking = Blocking,
metadata = maybe_marshal(ctx, ff_destination:metadata(DestinationState)),
context = maybe_marshal(ctx, Context)
metadata = marshal(ctx, ff_destination:metadata(DestinationState)),
context = marshal(ctx, Context)
}.
-spec marshal_event(ff_destination_machine:event()) ->
-spec marshal_event(ff_destination:timestamped_event()) ->
ff_proto_destination_thrift:'Event'().
marshal_event({EventID, {ev, Timestamp, Change}}) ->

View File

@ -23,7 +23,7 @@ handle_function(Func, Args, Opts) ->
%%
handle_function_('Create', [Params, Ctx], Opts) ->
ID = Params#dst_DestinationParams.id,
case ff_destination_machine:create(
case ff_destination:create(
ff_destination_codec:unmarshal_destination_params(Params),
ff_destination_codec:unmarshal(ctx, Ctx))
of
@ -41,10 +41,10 @@ handle_function_('Create', [Params, Ctx], Opts) ->
woody_error:raise(system, {internal, result_unexpected, woody_error:format_details(Error)})
end;
handle_function_('Get', [ID, EventRange], _Opts) ->
case ff_destination_machine:get(ID, ff_codec:unmarshal(event_range, EventRange)) of
case ff_destination:get_machine(ID, ff_codec:unmarshal(event_range, EventRange)) of
{ok, Machine} ->
Destination = ff_destination_machine:destination(Machine),
Context = ff_destination_machine:ctx(Machine),
Destination = ff_destination:get(Machine),
Context = ff_destination:ctx(Machine),
Response = ff_destination_codec:marshal_destination_state(Destination, Context),
{ok, Response};
{error, notfound} ->
@ -52,16 +52,16 @@ handle_function_('Get', [ID, EventRange], _Opts) ->
end;
handle_function_('GetContext', [ID], _Opts) ->
ok = scoper:add_meta(#{id => ID}),
case ff_destination_machine:get(ID, {undefined, 0}) of
case ff_destination:get_machine(ID, {undefined, 0}) of
{ok, Machine} ->
Context = ff_destination_machine:ctx(Machine),
Context = ff_destination:ctx(Machine),
{ok, ff_codec:marshal(context, Context)};
{error, notfound} ->
woody_error:raise(business, #fistful_DestinationNotFound{})
end;
handle_function_('GetEvents', [ID, EventRange], _Opts) ->
ok = scoper:add_meta(#{id => ID}),
case ff_destination_machine:events(ID, ff_codec:unmarshal(event_range, EventRange)) of
case ff_destination:events(ID, ff_codec:unmarshal(event_range, EventRange)) of
{ok, Events} ->
{ok, lists:map(fun ff_destination_codec:marshal_event/1, Events)};
{error, notfound} ->

View File

@ -99,7 +99,7 @@ unmarshal_event(undefined = Version, EncodedChange, Context0) ->
-spec maybe_migrate(any()) ->
event().
maybe_migrate({ev, Timestamp, Change}) ->
{ev, Timestamp, ff_destination:maybe_migrate(Change, #{timestamp => Timestamp})}.
{ev, Timestamp, ff_instrument:maybe_migrate(Change, #{timestamp => Timestamp})}.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").

View File

@ -30,8 +30,6 @@ handle_function_('Create', [IdentityParams, Context], Opts) ->
handle_function_('Get', [IdentityID, #'EventRange'{}], Opts);
{error, {provider, notfound}} ->
woody_error:raise(business, #fistful_ProviderNotFound{});
{error, {party, notfound}} ->
woody_error:raise(business, #fistful_PartyNotFound{});
{error, {identity_class, notfound}} ->
woody_error:raise(business, #fistful_IdentityClassNotFound{});
{error, {inaccessible, _}} ->

View File

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

View File

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

View File

@ -78,8 +78,8 @@ init([]) ->
{Backends, MachineHandlers, ModernizerHandlers} = lists:unzip3([
contruct_backend_childspec('ff/identity' , ff_identity_machine , PartyClient),
contruct_backend_childspec('ff/wallet_v2' , ff_wallet_machine , PartyClient),
contruct_backend_childspec('ff/source_v1' , ff_source_machine , PartyClient),
contruct_backend_childspec('ff/destination_v2' , ff_destination_machine , PartyClient),
contruct_backend_childspec('ff/source_v1' , ff_instrument_machine , PartyClient),
contruct_backend_childspec('ff/destination_v2' , ff_instrument_machine , PartyClient),
contruct_backend_childspec('ff/deposit_v1' , ff_deposit_machine , PartyClient),
contruct_backend_childspec('ff/withdrawal_v2' , ff_withdrawal_machine , PartyClient),
contruct_backend_childspec('ff/withdrawal/session_v2' , ff_withdrawal_session_machine , PartyClient),

View File

@ -25,7 +25,7 @@ handle_function(Func, Args, Opts) ->
handle_function_('CreateSource', [Params], Opts) ->
SourceID = Params#ff_admin_SourceParams.id,
case ff_source_machine:create(#{
case ff_source:create(#{
id => SourceID,
identity => Params#ff_admin_SourceParams.identity_id,
name => Params#ff_admin_SourceParams.name,
@ -43,9 +43,9 @@ handle_function_('CreateSource', [Params], Opts) ->
woody_error:raise(system, {internal, result_unexpected, woody_error:format_details(Error)})
end;
handle_function_('GetSource', [ID], _Opts) ->
case ff_source_machine:get(ID) of
case ff_source:get_machine(ID) of
{ok, Machine} ->
Source = ff_source_machine:source(Machine),
Source = ff_source:get(Machine),
{ok, ff_source_codec:marshal(source, Source)};
{error, notfound} ->
woody_error:raise(business, #fistful_SourceNotFound{})

View File

@ -38,19 +38,19 @@ marshal_source_state(SourceState, Context) ->
blocked
end,
#src_SourceState{
id = maybe_marshal(id, ff_source:id(SourceState)),
id = marshal(id, ff_source:id(SourceState)),
name = marshal(string, ff_source:name(SourceState)),
resource = marshal(resource, ff_source:resource(SourceState)),
external_id = maybe_marshal(id, ff_source:external_id(SourceState)),
account = maybe_marshal(account, ff_source:account(SourceState)),
status = maybe_marshal(status, ff_source:status(SourceState)),
created_at = maybe_marshal(timestamp_ms, ff_source:created_at(SourceState)),
external_id = marshal(id, ff_source:external_id(SourceState)),
account = marshal(account, ff_source:account(SourceState)),
status = marshal(status, ff_source:status(SourceState)),
created_at = marshal(timestamp_ms, ff_source:created_at(SourceState)),
blocking = Blocking,
metadata = maybe_marshal(ctx, ff_source:metadata(SourceState)),
context = maybe_marshal(ctx, Context)
metadata = marshal(ctx, ff_source:metadata(SourceState)),
context = marshal(ctx, Context)
}.
-spec marshal_event(ff_source_machine:event()) ->
-spec marshal_event(ff_source:timestamped_event()) ->
ff_proto_source_thrift:'Event'().
marshal_event({EventID, {ev, Timestamp, Change}}) ->

View File

@ -24,7 +24,7 @@ handle_function(Func, Args, Opts) ->
handle_function_('Create', [Params, Ctx], Opts) ->
ID = Params#src_SourceParams.id,
ok = scoper:add_meta(#{id => ID}),
case ff_source_machine:create(
case ff_source:create(
ff_source_codec:unmarshal_source_params(Params),
ff_source_codec:unmarshal(ctx, Ctx))
of
@ -43,10 +43,10 @@ handle_function_('Create', [Params, Ctx], Opts) ->
end;
handle_function_('Get', [ID, EventRange], _Opts) ->
ok = scoper:add_meta(#{id => ID}),
case ff_source_machine:get(ID, ff_codec:unmarshal(event_range, EventRange)) of
case ff_source:get_machine(ID, ff_codec:unmarshal(event_range, EventRange)) of
{ok, Machine} ->
Source = ff_source_machine:source(Machine),
Context = ff_source_machine:ctx(Machine),
Source = ff_source:get(Machine),
Context = ff_source:ctx(Machine),
Response = ff_source_codec:marshal_source_state(Source, Context),
{ok, Response};
{error, notfound} ->
@ -54,16 +54,16 @@ handle_function_('Get', [ID, EventRange], _Opts) ->
end;
handle_function_('GetContext', [ID], _Opts) ->
ok = scoper:add_meta(#{id => ID}),
case ff_source_machine:get(ID, {undefined, 0}) of
case ff_source:get_machine(ID, {undefined, 0}) of
{ok, Machine} ->
Context = ff_source_machine:ctx(Machine),
Context = ff_source:ctx(Machine),
{ok, ff_codec:marshal(context, Context)};
{error, notfound} ->
woody_error:raise(business, #fistful_SourceNotFound{})
end;
handle_function_('GetEvents', [ID, EventRange], _Opts) ->
ok = scoper:add_meta(#{id => ID}),
case ff_source_machine:events(ID, ff_codec:unmarshal(event_range, EventRange)) of
case ff_source:events(ID, ff_codec:unmarshal(event_range, EventRange)) of
{ok, Events} ->
{ok, lists:map(fun ff_source_codec:marshal_event/1, Events)};
{error, notfound} ->

View File

@ -97,7 +97,7 @@ unmarshal_event(undefined = Version, EncodedChange, Context0) ->
-spec maybe_migrate(any()) ->
event().
maybe_migrate({ev, Timestamp, Change0}) ->
Change = ff_source:maybe_migrate(Change0, #{timestamp => Timestamp}),
Change = ff_instrument:maybe_migrate(Change0, #{timestamp => Timestamp}),
{ev, Timestamp, Change}.
-ifdef(TEST).

View File

@ -109,7 +109,7 @@ maybe_migrate({p_transfer, PEvent}, _MigrateParams) ->
maybe_migrate({adjustment, _Payload} = Event, _MigrateParams) ->
ff_adjustment_utils:maybe_migrate(Event);
maybe_migrate({resource_got, Resource}, _MigrateParams) ->
{resource_got, ff_destination:maybe_migrate_resource(Resource)};
{resource_got, ff_instrument:maybe_migrate_resource(Resource)};
% Old events
maybe_migrate({limit_check, {wallet, Details}}, MigrateParams) ->

View File

@ -184,7 +184,7 @@ maybe_migrate({created, Session = #{
destination := #{resource := OldResource}
}
}}, Context) ->
NewResource = ff_destination:maybe_migrate_resource(OldResource),
NewResource = ff_instrument:maybe_migrate_resource(OldResource),
FullResource = try_get_full_resource(NewResource, Context),
NewWithdrawal0 = maps:without([destination], Withdrawal),
NewWithdrawal1 = NewWithdrawal0#{resource => FullResource},
@ -194,7 +194,7 @@ maybe_migrate({created, Session = #{
resource := Resource
}
}}, Context) ->
NewResource = ff_destination:maybe_migrate_resource(Resource),
NewResource = ff_instrument:maybe_migrate_resource(Resource),
maybe_migrate({created, Session#{
version => 1,
withdrawal => Withdrawal#{

View File

@ -740,13 +740,12 @@ create_source(IID, _C) ->
currency => <<"RUB">>,
resource => SrcResource
},
ok = ff_source_machine:create(Params, ff_entity_context:new()),
ok = ff_source:create(Params, ff_entity_context:new()),
authorized = ct_helper:await(
authorized,
fun () ->
{ok, SrcM} = ff_source_machine:get(ID),
Source = ff_source_machine:source(SrcM),
ff_source:status(Source)
{ok, SrcM} = ff_source:get_machine(ID),
ff_source:status(ff_source:get(SrcM))
end
),
ID.

View File

@ -213,7 +213,7 @@ get_create_destination_events_ok(C) ->
IID = create_person_identity(Party, C),
DestID = create_destination(IID, C),
{ok, RawEvents} = ff_destination_machine:events(DestID, {undefined, 1000}),
{ok, RawEvents} = ff_destination:events(DestID, {undefined, 1000}),
{_Events, MaxID} = ct_eventsink:events(LastEvent, 1000, Sink),
MaxID = LastEvent + length(RawEvents).
@ -227,7 +227,7 @@ get_create_source_events_ok(C) ->
IID = create_person_identity(Party, C),
SrcID = create_source(IID, C),
{ok, RawEvents} = ff_source_machine:events(SrcID, {undefined, 1000}),
{ok, RawEvents} = ff_source:events(SrcID, {undefined, 1000}),
{_Events, MaxID} = ct_eventsink:events(LastEvent, 1000, Sink),
MaxID = LastEvent + length(RawEvents).
@ -412,34 +412,32 @@ create_wallet(IdentityID, Name, Currency, _C) ->
),
ID.
create_source(IdentityID, Name, Currency, Resource) ->
create_instrument(Type, IdentityID, Name, Currency, Resource, C) ->
ID = genlib:unique(),
ok = ff_source_machine:create(
ok = create_instrument(
Type,
#{id => ID, identity => IdentityID, name => Name, currency => Currency, resource => Resource},
ff_entity_context:new()
ff_entity_context:new(),
C
),
ID.
create_destination(IdentityID, Name, Currency, Resource) ->
ID = genlib:unique(),
ok = ff_destination_machine:create(
#{id => ID, identity => IdentityID, name => Name, currency => Currency, resource => Resource},
ff_entity_context:new()
),
ID.
create_instrument(destination, Params, Ctx, _C) ->
ff_destination:create(Params, Ctx);
create_instrument(source, Params, Ctx, _C) ->
ff_source:create(Params, Ctx).
generate_id() ->
genlib:to_binary(genlib_time:ticks()).
create_source(IID, _C) ->
create_source(IID, C) ->
SrcResource = #{type => internal, details => <<"Infinite source of cash">>},
SrcID = create_source(IID, <<"XSource">>, <<"RUB">>, SrcResource),
SrcID = create_instrument(source, IID, <<"XSource">>, <<"RUB">>, SrcResource, C),
authorized = ct_helper:await(
authorized,
fun () ->
{ok, SrcM} = ff_source_machine:get(SrcID),
Source = ff_source_machine:source(SrcM),
ff_source:status(Source)
{ok, SrcM} = ff_source:get_machine(SrcID),
ff_source:status(ff_source:get(SrcM))
end
),
SrcID.
@ -455,13 +453,12 @@ process_deposit(SrcID, WalID) ->
create_destination(IID, C) ->
DestResource = {bank_card, #{bank_card => ct_cardstore:bank_card(<<"4150399999000900">>, {12, 2025}, C)}},
DestID = create_destination(IID, <<"XDesination">>, <<"RUB">>, DestResource),
DestID = create_instrument(destination, IID, <<"XDesination">>, <<"RUB">>, DestResource, C),
authorized = ct_helper:await(
authorized,
fun () ->
{ok, DestM} = ff_destination_machine:get(DestID),
Destination = ff_destination_machine:destination(DestM),
ff_destination:status(Destination)
{ok, DestM} = ff_destination:get_machine(DestID),
ff_destination:status(ff_destination:get(DestM))
end
),
DestID.

View File

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

View File

@ -619,13 +619,12 @@ create_destination(IID, Currency, Token, C) ->
end,
Resource = {bank_card, #{bank_card => NewStoreResource}},
Params = #{id => ID, identity => IID, name => <<"XDesination">>, currency => Currency, resource => Resource},
ok = ff_destination_machine:create(Params, ff_entity_context:new()),
ok = ff_destination:create(Params, ff_entity_context:new()),
authorized = ct_helper:await(
authorized,
fun () ->
{ok, Machine} = ff_destination_machine:get(ID),
Destination = ff_destination_machine:destination(Machine),
ff_destination:status(Destination)
{ok, Machine} = ff_destination:get_machine(ID),
ff_destination:status(ff_destination:get(Machine))
end
),
ID.

View File

@ -139,25 +139,31 @@ create_identity(Party, Name, ProviderID, ClassID, _C) ->
create_destination(IID, C) ->
DestResource = {bank_card, #{bank_card => ct_cardstore:bank_card(<<"4150399999000900">>, {12, 2025}, C)}},
DestID = create_destination(IID, <<"XDesination">>, <<"RUB">>, DestResource),
DestID = create_instrument(destination, IID, <<"XDesination">>, <<"RUB">>, DestResource, C),
authorized = ct_helper:await(
authorized,
fun () ->
{ok, DestM} = ff_destination_machine:get(DestID),
Destination = ff_destination_machine:destination(DestM),
ff_destination:status(Destination)
{ok, DestM} = ff_destination:get_machine(DestID),
ff_destination:status(ff_destination:get(DestM))
end
),
DestID.
create_destination(IdentityID, Name, Currency, Resource) ->
create_instrument(Type, IdentityID, Name, Currency, Resource, C) ->
ID = genlib:unique(),
ok = ff_destination_machine:create(
ok = create_instrument(
Type,
#{id => ID, identity => IdentityID, name => Name, currency => Currency, resource => Resource},
ff_entity_context:new()
ff_entity_context:new(),
C
),
ID.
create_instrument(destination, Params, Ctx, _C) ->
ff_destination:create(Params, Ctx);
create_instrument(source, Params, Ctx, _C) ->
ff_source:create(Params, Ctx).
create_failed_session(IdentityID, DestinationID, _C) ->
ID = genlib:unique(),
{ok, IdentityMachine} = ff_identity_machine:get(IdentityID),
@ -167,8 +173,8 @@ create_failed_session(IdentityID, DestinationID, _C) ->
sender => ff_identity_machine:identity(IdentityMachine),
receiver => ff_identity_machine:identity(IdentityMachine)
},
{ok, DestinationMachine} = ff_destination_machine:get(DestinationID),
Destination = ff_destination_machine:destination(DestinationMachine),
{ok, DestinationMachine} = ff_destination:get_machine(DestinationID),
Destination = ff_destination:get(DestinationMachine),
{ok, DestinationResource} = ff_destination:resource_full(Destination),
SessionParams = #{
withdrawal_id => ID,

View File

@ -4,10 +4,6 @@
-include_lib("damsel/include/dmsl_domain_thrift.hrl").
-include_lib("damsel/include/dmsl_withdrawals_provider_adapter_thrift.hrl").
%% Accessors
-export([id/1]).
%% API
-export([process_withdrawal/4]).
@ -117,15 +113,6 @@
-export_type([quote_data/0]).
-export_type([identity/0]).
%%
%% Accessors
%%
-spec id(withdrawal()) ->
binary().
id(Withdrawal) ->
maps:get(id, Withdrawal).
%%
%% API
%%

View File

@ -285,8 +285,7 @@ metadata(T) ->
create(Params) ->
do(fun() ->
#{id := ID, source_id := SourceID, wallet_id := WalletID, body := Body} = Params,
Machine = unwrap(source, ff_source_machine:get(SourceID)),
Source = ff_source_machine:source(Machine),
Source = ff_source:get(unwrap(source, ff_source:get_machine(SourceID))),
CreatedAt = ff_time:now(),
DomainRevision = ff_domain_config:head(),
Wallet = unwrap(wallet, get_wallet(WalletID)),
@ -594,9 +593,8 @@ process_transfer_fail(limit_check, Deposit) ->
make_final_cash_flow(WalletID, SourceID, Body) ->
{ok, WalletMachine} = ff_wallet_machine:get(WalletID),
WalletAccount = ff_wallet:account(ff_wallet_machine:wallet(WalletMachine)),
{ok, SourceMachine} = ff_source_machine:get(SourceID),
Source = ff_source_machine:source(SourceMachine),
SourceAccount = ff_source:account(Source),
{ok, SourceMachine} = ff_source:get_machine(SourceID),
SourceAccount = ff_source:account(ff_source:get(SourceMachine)),
Constants = #{
operation_amount => Body
},

View File

@ -461,9 +461,8 @@ process_transfer_fail(limit_check, Revert) ->
make_final_cash_flow(WalletID, SourceID, Body) ->
{ok, WalletMachine} = ff_wallet_machine:get(WalletID),
WalletAccount = ff_wallet:account(ff_wallet_machine:wallet(WalletMachine)),
{ok, SourceMachine} = ff_source_machine:get(SourceID),
Source = ff_source_machine:source(SourceMachine),
SourceAccount = ff_source:account(Source),
{ok, SourceMachine} = ff_source:get_machine(SourceID),
SourceAccount = ff_source:account(ff_source:get(SourceMachine)),
Constants = #{
operation_amount => Body
},

View File

@ -5,17 +5,17 @@
%%%
%%% - We must consider withdrawal provider terms ensure that the provided
%%% Resource is ok to withdraw to.
%%%
-module(ff_destination).
-type id() :: binary().
-type name() :: binary().
-type ctx() :: ff_entity_context:context().
-type id() :: ff_instrument:id().
-type name() :: ff_instrument:name().
-type account() :: ff_account:account().
-type identity() :: ff_identity:id().
-type currency() :: ff_currency:id().
-type status() :: unauthorized | authorized.
-type metadata() :: ff_entity_context:md().
-type timestamp() :: ff_time:timestamp_ms().
-type status() :: ff_instrument:status().
-type resource_type() :: bank_card | crypto_wallet.
-type resource() ::
@ -89,50 +89,18 @@
| {ripple, #{tag => binary()}}
.
-define(ACTUAL_FORMAT_VERSION, 4).
-type destination() :: ff_instrument:instrument(resource()).
-type destination_state() :: ff_instrument:instrument_state(resource()).
-type params() :: ff_instrument_machine:params(resource()).
-type machine() :: ff_instrument_machine:st(resource()).
-type event_range() :: ff_instrument_machine:event_range().
-type event() :: ff_instrument:event(resource()).
-type destination() :: #{
version := ?ACTUAL_FORMAT_VERSION,
resource := resource(),
name := name(),
created_at => timestamp(),
external_id => id(),
metadata => metadata()
}.
-type destination_state() :: #{
account := account() | undefined,
resource := resource(),
name := name(),
status => status(),
created_at => timestamp(),
external_id => id(),
metadata => metadata()
}.
-type params() :: #{
id := id(),
identity := ff_identity:id(),
name := name(),
currency := ff_currency:id(),
resource := resource(),
external_id => id(),
metadata => metadata()
}.
-type event() ::
{created, destination_state()} |
{account, ff_account:event()} |
{status_changed, status()}.
-type legacy_event() :: any().
-type create_error() ::
{identity, notfound} |
{currency, notfound} |
ff_account:create_error() |
{identity, ff_party:inaccessibility()}.
-type events() :: ff_instrument_machine:events(resource()).
-type timestamped_event() :: ff_instrument_machine:timestamped_event(resource()).
-export_type([id/0]).
-export_type([machine/0]).
-export_type([destination/0]).
-export_type([destination_state/0]).
-export_type([status/0]).
@ -140,23 +108,23 @@
-export_type([resource_id/0]).
-export_type([resource_type/0]).
-export_type([resource_full/0]).
-export_type([params/0]).
-export_type([event/0]).
-export_type([create_error/0]).
-export_type([timestamped_event/0]).
-export_type([params/0]).
-export_type([exp_date/0]).
%% Accessors
-export([account/1]).
-export([id/1]).
-export([name/1]).
-export([account/1]).
-export([identity/1]).
-export([currency/1]).
-export([resource/1]).
-export([status/1]).
-export([external_id/1]).
-export([created_at/1]).
-export([metadata/1]).
-export([external_id/1]).
-export([resource_full/1]).
-export([resource_full/2]).
-export([process_resource_full/2]).
@ -164,78 +132,51 @@
%% API
-export([create/1]).
-export([create/2]).
-export([get_machine/1]).
-export([get_machine/2]).
-export([get/1]).
-export([ctx/1]).
-export([is_accessible/1]).
-export([authorize/1]).
-export([apply_event/2]).
-export([maybe_migrate/2]).
-export([maybe_migrate_resource/1]).
-export([events/2]).
%% Pipeline
-import(ff_pipeline, [do/1, unwrap/1, unwrap/2]).
-import(ff_pipeline, [do/1, unwrap/2]).
%% Accessors
-spec id(destination_state()) ->
id() | undefined.
-spec name(destination_state()) ->
name().
-spec account(destination_state()) ->
account() | undefined.
-spec identity(destination_state()) ->
identity().
-spec currency(destination_state()) ->
currency().
-spec resource(destination_state()) ->
resource().
-spec status(destination_state()) ->
status() | undefined.
-spec id(destination_state()) -> id().
-spec name(destination_state()) -> name().
-spec account(destination_state()) -> account().
-spec identity(destination_state()) -> identity().
-spec currency(destination_state()) -> currency().
-spec resource(destination_state()) -> resource().
-spec status(destination_state()) -> status().
id(Destination) ->
case account(Destination) of
undefined ->
undefined;
Account ->
ff_account:id(Account)
end.
name(#{name := V}) ->
V.
account(#{account := V}) ->
V;
account(_) ->
undefined.
identity(Destination) ->
ff_account:identity(account(Destination)).
currency(Destination) ->
ff_account:currency(account(Destination)).
resource(#{resource := V}) ->
V.
status(#{status := V}) ->
V;
status(_) ->
undefined.
id(Destination) -> ff_instrument:id(Destination).
name(Destination) -> ff_instrument:name(Destination).
identity(Destination) -> ff_instrument:identity(Destination).
currency(Destination) -> ff_instrument:currency(Destination).
resource(Destination) -> ff_instrument:resource(Destination).
status(Destination) -> ff_instrument:status(Destination).
account(Destination) -> ff_instrument:account(Destination).
-spec external_id(destination_state()) ->
id() | undefined.
external_id(#{external_id := ExternalID}) ->
ExternalID;
external_id(_Destination) ->
undefined.
external_id(T) -> ff_instrument:external_id(T).
-spec created_at(destination_state()) ->
ff_time:timestamp_ms() | undefiend.
created_at(#{created_at := CreatedAt}) ->
CreatedAt;
created_at(_Destination) ->
undefined.
ff_time:timestamp_ms().
created_at(T) -> ff_instrument:created_at(T).
-spec metadata(destination_state()) ->
ff_entity_context:context() | undefined.
metadata(#{metadata := Metadata}) ->
Metadata;
metadata(_Destination) ->
undefined.
ff_entity_context:context().
metadata(T) -> ff_instrument:metadata(T).
-spec resource_full(destination_state()) ->
{ok, resource_full()} |
@ -289,172 +230,54 @@ unwrap_resource_id({bank_card, ID}) ->
%% API
-spec create(params()) ->
{ok, [event()]} |
{error, create_error()}.
create(Params) ->
do(fun () ->
#{
id := ID,
identity := IdentityID,
name := Name,
currency := CurrencyID,
resource := Resource
} = Params,
Identity = ff_identity_machine:identity(unwrap(identity, ff_identity_machine:get(IdentityID))),
Currency = unwrap(currency, ff_currency:get(CurrencyID)),
Events = unwrap(ff_account:create(ID, Identity, Currency)),
accessible = unwrap(identity, ff_identity:is_accessible(Identity)),
CreatedAt = ff_time:now(),
[{created, genlib_map:compact(#{
version => ?ACTUAL_FORMAT_VERSION,
name => Name,
resource => Resource,
external_id => maps:get(external_id, Params, undefined),
metadata => maps:get(metadata, Params, undefined),
created_at => CreatedAt
})}] ++
[{account, Ev} || Ev <- Events] ++
[{status_changed, unauthorized}]
end).
-define(NS, 'ff/destination_v2').
-spec create(params(), ctx()) ->
ok |
{error,
_InstrumentCreateError |
exists
}.
create(Params, Ctx) ->
ff_instrument_machine:create(?NS, Params, Ctx).
-spec get_machine(id()) ->
{ok, machine()} |
{error, notfound} .
get_machine(ID) ->
ff_instrument_machine:get(?NS, ID).
-spec get_machine(id(), event_range()) ->
{ok, machine()} |
{error, notfound} .
get_machine(ID, EventRange) ->
ff_instrument_machine:get(?NS, ID, EventRange).
-spec get(machine()) ->
destination_state().
get(Machine) ->
ff_instrument_machine:instrument(Machine).
-spec ctx(machine()) ->
ctx().
ctx(St) ->
ff_machine:ctx(St).
-spec is_accessible(destination_state()) ->
{ok, accessible} |
{error, ff_party:inaccessibility()}.
is_accessible(Destination) ->
ff_account:is_accessible(account(Destination)).
ff_instrument:is_accessible(Destination).
-spec authorize(destination_state()) ->
{ok, [event()]}.
authorize(#{status := unauthorized}) ->
% TODO
% - Do the actual authorization
{ok, [{status_changed, authorized}]};
authorize(#{status := authorized}) ->
{ok, []}.
-spec events(id(), event_range()) ->
{ok, events()} |
{error, notfound}.
-spec apply_event(event(), ff_maybe:maybe(destination_state())) ->
destination_state().
apply_event({created, Destination}, undefined) ->
Destination;
apply_event({status_changed, S}, Destination) ->
Destination#{status => S};
apply_event({account, Ev}, Destination = #{account := Account}) ->
Destination#{account => ff_account:apply_event(Ev, Account)};
apply_event({account, Ev}, Destination) ->
apply_event({account, Ev}, Destination#{account => undefined}).
-spec maybe_migrate(event() | legacy_event(), ff_machine:migrate_params()) ->
event().
maybe_migrate(Event = {created, #{version := ?ACTUAL_FORMAT_VERSION}}, _MigrateParams) ->
Event;
maybe_migrate({created, Destination = #{version := 3, name := Name}}, MigrateParams) ->
maybe_migrate({created, Destination#{
version => 4,
name => maybe_migrate_name(Name)
}}, MigrateParams);
maybe_migrate({created, Destination = #{version := 2}}, MigrateParams) ->
Context = maps:get(ctx, MigrateParams, undefined),
%% TODO add metada migration for eventsink after decouple instruments
Metadata = ff_entity_context:try_get_legacy_metadata(Context),
maybe_migrate({created, genlib_map:compact(Destination#{
version => 3,
metadata => Metadata
})}, MigrateParams);
maybe_migrate({created, Destination = #{version := 1}}, MigrateParams) ->
Timestamp = maps:get(timestamp, MigrateParams),
CreatedAt = ff_codec:unmarshal(timestamp_ms, ff_codec:marshal(timestamp, Timestamp)),
maybe_migrate({created, Destination#{
version => 2,
created_at => CreatedAt
}}, MigrateParams);
maybe_migrate({created, Destination = #{
resource := Resource,
name := Name
}}, MigrateParams) ->
NewDestination = genlib_map:compact(#{
version => 1,
resource => maybe_migrate_resource(Resource),
name => Name,
external_id => maps:get(external_id, Destination, undefined)
}),
maybe_migrate({created, NewDestination}, MigrateParams);
%% Other events
maybe_migrate(Event, _MigrateParams) ->
Event.
-spec maybe_migrate_resource(any()) ->
any().
maybe_migrate_resource({crypto_wallet, #{id := ID, currency := ripple, tag := Tag}}) ->
maybe_migrate_resource({crypto_wallet, #{id => ID, currency => {ripple, #{tag => Tag}}}});
maybe_migrate_resource({crypto_wallet, #{id := ID, currency := Currency}}) when is_atom(Currency) ->
maybe_migrate_resource({crypto_wallet, #{id => ID, currency => {Currency, #{}}}});
maybe_migrate_resource({crypto_wallet, #{id := _ID} = CryptoWallet}) ->
maybe_migrate_resource({crypto_wallet, #{crypto_wallet => CryptoWallet}});
maybe_migrate_resource({bank_card, #{token := _Token} = BankCard}) ->
maybe_migrate_resource({bank_card, #{bank_card => BankCard}});
maybe_migrate_resource(Resource) ->
Resource.
maybe_migrate_name(Name) ->
re:replace(Name, "\\d{12,19}", <<"">>, [global, {return, binary}]).
%% Tests
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-spec test() -> _.
-spec v1_created_migration_test() -> _.
v1_created_migration_test() ->
CreatedAt = ff_time:now(),
LegacyEvent = {created, #{
version => 1,
resource => {crypto_wallet, #{crypto_wallet => #{}}},
name => <<"some name">>,
external_id => genlib:unique()
}},
{created, #{version := Version}} = maybe_migrate(LegacyEvent, #{
timestamp => ff_codec:unmarshal(timestamp, ff_codec:marshal(timestamp_ms, CreatedAt))
}),
?assertEqual(4, Version).
-spec v2_created_migration_test() -> _.
v2_created_migration_test() ->
CreatedAt = ff_time:now(),
LegacyEvent = {created, #{
version => 2,
resource => {crypto_wallet, #{crypto_wallet => #{}}},
name => <<"some name">>,
external_id => genlib:unique(),
created_at => CreatedAt
}},
{created, #{version := Version, metadata := Metadata}} = maybe_migrate(LegacyEvent, #{
ctx => #{
<<"com.rbkmoney.wapi">> => #{
<<"metadata">> => #{
<<"some key">> => <<"some val">>
}
}
}
}),
?assertEqual(4, Version),
?assertEqual(#{<<"some key">> => <<"some val">>}, Metadata).
-spec name_migration_test() -> _.
name_migration_test() ->
?assertEqual(<<"sd">>, maybe_migrate_name(<<"sd123123123123123">>)),
?assertEqual(<<"sd1231231231sd23123">>, maybe_migrate_name(<<"sd1231231231sd23123">>)),
?assertEqual(<<"sdds123sd">>, maybe_migrate_name(<<"sd123123123123ds123sd">>)),
?assertEqual(<<"sdsd">>, maybe_migrate_name(<<"sd123123123123123sd">>)),
?assertEqual(<<"sd">>, maybe_migrate_name(<<"123123123123123sd">>)).
-endif.
events(ID, Range) ->
ff_instrument_machine:events(?NS, ID, Range).

View File

@ -1,164 +0,0 @@
%%%
%%% Destination machine
%%%
-module(ff_destination_machine).
%% API
-type id() :: machinery:id().
-type ctx() :: ff_entity_context:context().
-type destination() :: ff_destination:destination_state().
-type change() :: ff_destination:event().
-type event() :: {integer(), ff_machine:timestamped_event(change())}.
-type events() :: [event()].
-type event_range() :: {After :: non_neg_integer() | undefined, Limit :: non_neg_integer() | undefined}.
-type params() :: ff_destination:params().
-type st() :: ff_machine:st(destination()).
-type repair_error() :: ff_repair:repair_error().
-type repair_response() :: ff_repair:repair_response().
-export_type([id/0]).
-export_type([st/0]).
-export_type([event/0]).
-export_type([repair_error/0]).
-export_type([repair_response/0]).
-export_type([params/0]).
-export_type([event_range/0]).
%% API
-export([create/2]).
-export([get/1]).
-export([get/2]).
-export([events/2]).
%% Accessors
-export([destination/1]).
-export([ctx/1]).
%% Machinery
-behaviour(machinery).
-export([init/4]).
-export([process_timeout/3]).
-export([process_repair/4]).
-export([process_call/4]).
%% Pipeline
-import(ff_pipeline, [do/1, unwrap/1]).
%%
-define(NS, 'ff/destination_v2').
-spec create(params(), ctx()) ->
ok |
{error, ff_destination:create_error() | exists}.
create(#{id := ID} = Params, Ctx) ->
do(fun () ->
Events = unwrap(ff_destination:create(Params)),
unwrap(machinery:start(?NS, ID, {Events, Ctx}, backend()))
end).
-spec get(id()) ->
{ok, st()} |
{error, notfound}.
get(ID) ->
ff_machine:get(ff_destination, ?NS, ID).
-spec get(id(), event_range()) ->
{ok, st()} |
{error, notfound} .
get(ID, {After, Limit}) ->
ff_machine:get(ff_destination, ?NS, ID, {After, Limit, forward}).
-spec events(id(), event_range()) ->
{ok, events()} |
{error, notfound}.
events(ID, {After, Limit}) ->
do(fun () ->
History = unwrap(ff_machine:history(ff_destination, ?NS, ID, {After, Limit, forward})),
[{EventID, TsEv} || {EventID, _, TsEv} <- History]
end).
%% Accessors
-spec destination(st()) ->
destination().
destination(St) ->
ff_machine:model(St).
-spec ctx(st()) ->
ctx().
ctx(St) ->
ff_machine:ctx(St).
%% Machinery
-type machine() :: ff_machine:machine(change()).
-type result() :: ff_machine:result(change()).
-type handler_opts() :: machinery:handler_opts(_).
-type handler_args() :: machinery:handler_args(_).
-spec init({[change()], ctx()}, machine(), _, handler_opts()) ->
result().
init({Events, Ctx}, #{}, _, _Opts) ->
#{
events => ff_machine:emit_events(Events),
action => continue,
aux_state => #{ctx => Ctx}
}.
%%
-spec process_timeout(machine(), handler_args(), handler_opts()) ->
result().
process_timeout(Machine, _, _Opts) ->
St = ff_machine:collapse(ff_destination, Machine),
process_timeout(deduce_activity(ff_machine:model(St)), St).
process_timeout(authorize, St) ->
D0 = destination(St),
case ff_destination:authorize(D0) of
{ok, Events} ->
#{
events => ff_machine:emit_events(Events)
}
end.
deduce_activity(#{status := unauthorized}) ->
authorize;
deduce_activity(#{}) ->
undefined.
%%
-spec process_call(_CallArgs, machine(), handler_args(), handler_opts()) ->
{ok, result()}.
process_call(_CallArgs, #{}, _, _Opts) ->
{ok, #{}}.
-spec process_repair(ff_repair:scenario(), machine(), handler_args(), handler_opts()) ->
{ok, {repair_response(), result()}} | {error, repair_error()}.
process_repair(Scenario, Machine, _Args, _Opts) ->
ff_repair:apply_scenario(ff_destination, Machine, Scenario).
%% Internals
backend() ->
fistful:backend(?NS).

View File

@ -0,0 +1,327 @@
%%%
%%% Instrument
%%%
%%% TODOs
%%%
%%% - We must consider withdrawal provider terms ensure that the provided
%%% resource is ok to withdraw to.
%%%
-module(ff_instrument).
-type id() :: binary().
-type external_id() :: id() | undefined.
-type name() :: binary().
-type metadata() :: ff_entity_context:md().
-type resource(T) :: T.
-type account() :: ff_account:account().
-type identity() :: ff_identity:id().
-type currency() :: ff_currency:id().
-type timestamp() :: ff_time:timestamp_ms().
-type status() :: unauthorized | authorized.
-define(ACTUAL_FORMAT_VERSION, 4).
-type instrument_state(T) :: #{
account := account() | undefined,
resource := resource(T),
name := name(),
status := status() | undefined,
created_at => timestamp(),
external_id => id(),
metadata => metadata()
}.
-type instrument(T) :: #{
version := ?ACTUAL_FORMAT_VERSION,
resource := resource(T),
name := name(),
created_at => timestamp(),
external_id => id(),
metadata => metadata()
}.
-type event(T) ::
{created, instrument_state(T)} |
{account, ff_account:event()} |
{status_changed, status()}.
-type legacy_event() :: any().
-type create_error() ::
{identity, notfound} |
{currecy, notfoud} |
ff_account:create_error() |
{identity, ff_party:inaccessibility()}.
-export_type([id/0]).
-export_type([instrument/1]).
-export_type([instrument_state/1]).
-export_type([status/0]).
-export_type([resource/1]).
-export_type([event/1]).
-export_type([name/0]).
-export_type([metadata/0]).
-export([account/1]).
-export([id/1]).
-export([name/1]).
-export([identity/1]).
-export([currency/1]).
-export([resource/1]).
-export([status/1]).
-export([external_id/1]).
-export([created_at/1]).
-export([metadata/1]).
-export([create/1]).
-export([authorize/1]).
-export([is_accessible/1]).
-export([apply_event/2]).
-export([maybe_migrate/2]).
-export([maybe_migrate_resource/1]).
%% Pipeline
-import(ff_pipeline, [do/1, unwrap/1, unwrap/2]).
%% Accessors
-spec account(instrument_state(_)) ->
account() | undefined.
account(#{account := V}) ->
V;
account(_) ->
undefined.
-spec id(instrument_state(_)) ->
id().
-spec name(instrument_state(_)) ->
binary().
-spec identity(instrument_state(_)) ->
identity().
-spec currency(instrument_state(_)) ->
currency().
-spec resource(instrument_state(T)) ->
resource(T).
-spec status(instrument_state(_)) ->
status() | undefined.
id(Instrument) ->
case account(Instrument) of
undefined ->
undefined;
Account ->
ff_account:id(Account)
end.
name(#{name := V}) ->
V.
identity(Instrument) ->
ff_account:identity(account(Instrument)).
currency(Instrument) ->
ff_account:currency(account(Instrument)).
resource(#{resource := V}) ->
V.
status(#{status := V}) ->
V;
status(_) ->
undefined.
-spec external_id(instrument_state(_)) ->
external_id().
external_id(#{external_id := ExternalID}) ->
ExternalID;
external_id(_Instrument) ->
undefined.
-spec created_at(instrument_state(_)) ->
timestamp().
created_at(#{created_at := CreatedAt}) ->
CreatedAt.
-spec metadata(instrument_state(_)) ->
metadata().
metadata(#{metadata := Metadata}) ->
Metadata;
metadata(_Instrument) ->
undefined.
%%
-spec create(ff_instrument_machine:params(T)) ->
{ok, [event(T)]} |
{error, create_error()}.
create(Params = #{
id := ID,
identity := IdentityID,
name := Name,
currency := CurrencyID,
resource := Resource
}) ->
do(fun () ->
Identity = ff_identity_machine:identity(unwrap(identity, ff_identity_machine:get(IdentityID))),
Currency = unwrap(currency, ff_currency:get(CurrencyID)),
Events = unwrap(ff_account:create(ID, Identity, Currency)),
accessible = unwrap(identity, ff_identity:is_accessible(Identity)),
CreatedAt = ff_time:now(),
[{created, genlib_map:compact(#{
version => ?ACTUAL_FORMAT_VERSION,
name => Name,
resource => Resource,
external_id => maps:get(external_id, Params, undefined),
metadata => maps:get(metadata, Params, undefined),
created_at => CreatedAt
})}] ++
[{account, Ev} || Ev <- Events] ++
[{status_changed, unauthorized}]
end).
-spec authorize(instrument_state(T)) ->
{ok, [event(T)]}.
authorize(#{status := unauthorized}) ->
% TODO
% - Do the actual authorization
{ok, [{status_changed, authorized}]};
authorize(#{status := authorized}) ->
{ok, []}.
-spec is_accessible(instrument_state(_)) ->
{ok, accessible} |
{error, ff_party:inaccessibility()}.
is_accessible(Instrument) ->
ff_account:is_accessible(account(Instrument)).
%%
-spec apply_event(event(T), ff_maybe:maybe(instrument_state(T))) ->
instrument_state(T).
apply_event({created, Instrument}, undefined) ->
Instrument;
apply_event({status_changed, S}, Instrument) ->
Instrument#{status => S};
apply_event({account, Ev}, Instrument = #{account := Account}) ->
Instrument#{account => ff_account:apply_event(Ev, Account)};
apply_event({account, Ev}, Instrument) ->
apply_event({account, Ev}, Instrument#{account => undefined}).
-spec maybe_migrate(event(T) | legacy_event(), ff_machine:migrate_params()) ->
event(T).
maybe_migrate(Event = {created, #{version := ?ACTUAL_FORMAT_VERSION}}, _MigrateParams) ->
Event;
maybe_migrate({created, Instrument = #{version := 3, name := Name}}, MigrateParams) ->
maybe_migrate({created, Instrument#{
version => 4,
name => maybe_migrate_name(Name)
}}, MigrateParams);
maybe_migrate({created, Instrument = #{version := 2}}, MigrateParams) ->
Context = maps:get(ctx, MigrateParams, undefined),
%% TODO add metada migration for eventsink after decouple instruments
Metadata = ff_entity_context:try_get_legacy_metadata(Context),
maybe_migrate({created, genlib_map:compact(Instrument#{
version => 3,
metadata => Metadata
})}, MigrateParams);
maybe_migrate({created, Instrument = #{version := 1}}, MigrateParams) ->
Timestamp = maps:get(timestamp, MigrateParams),
CreatedAt = ff_codec:unmarshal(timestamp_ms, ff_codec:marshal(timestamp, Timestamp)),
maybe_migrate({created, Instrument#{
version => 2,
created_at => CreatedAt
}}, MigrateParams);
maybe_migrate({created, Instrument = #{
resource := Resource,
name := Name
}}, MigrateParams) ->
NewInstrument = genlib_map:compact(#{
version => 1,
resource => maybe_migrate_resource(Resource),
name => Name,
external_id => maps:get(external_id, Instrument, undefined)
}),
maybe_migrate({created, NewInstrument}, MigrateParams);
%% Other events
maybe_migrate(Event, _MigrateParams) ->
Event.
-spec maybe_migrate_resource(any()) ->
any().
maybe_migrate_resource({crypto_wallet, #{id := ID, currency := ripple, tag := Tag}}) ->
maybe_migrate_resource({crypto_wallet, #{id => ID, currency => {ripple, #{tag => Tag}}}});
maybe_migrate_resource({crypto_wallet, #{id := ID, currency := Currency}}) when is_atom(Currency) ->
maybe_migrate_resource({crypto_wallet, #{id => ID, currency => {Currency, #{}}}});
maybe_migrate_resource({crypto_wallet, #{id := _ID} = CryptoWallet}) ->
maybe_migrate_resource({crypto_wallet, #{crypto_wallet => CryptoWallet}});
maybe_migrate_resource({bank_card, #{token := _Token} = BankCard}) ->
maybe_migrate_resource({bank_card, #{bank_card => BankCard}});
maybe_migrate_resource(Resource) ->
Resource.
maybe_migrate_name(Name) ->
re:replace(Name, "\\d{12,19}", <<"">>, [global, {return, binary}]).
%% Tests
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-spec test() -> _.
-spec v1_created_migration_test() -> _.
v1_created_migration_test() ->
CreatedAt = ff_time:now(),
LegacyEvent = {created, #{
version => 1,
resource => {crypto_wallet, #{crypto_wallet => #{}}},
name => <<"some name">>,
external_id => genlib:unique()
}},
{created, #{version := Version}} = maybe_migrate(LegacyEvent, #{
timestamp => ff_codec:unmarshal(timestamp, ff_codec:marshal(timestamp_ms, CreatedAt))
}),
?assertEqual(4, Version).
-spec v2_created_migration_test() -> _.
v2_created_migration_test() ->
CreatedAt = ff_time:now(),
LegacyEvent = {created, #{
version => 2,
resource => {crypto_wallet, #{crypto_wallet => #{}}},
name => <<"some name">>,
external_id => genlib:unique(),
created_at => CreatedAt
}},
{created, #{version := Version, metadata := Metadata}} = maybe_migrate(LegacyEvent, #{
ctx => #{
<<"com.rbkmoney.wapi">> => #{
<<"metadata">> => #{
<<"some key">> => <<"some val">>
}
}
}
}),
?assertEqual(4, Version),
?assertEqual(#{<<"some key">> => <<"some val">>}, Metadata).
-spec name_migration_test() -> _.
name_migration_test() ->
?assertEqual(<<"sd">>, maybe_migrate_name(<<"sd123123123123123">>)),
?assertEqual(<<"sd1231231231sd23123">>, maybe_migrate_name(<<"sd1231231231sd23123">>)),
?assertEqual(<<"sdds123sd">>, maybe_migrate_name(<<"sd123123123123ds123sd">>)),
?assertEqual(<<"sdsd">>, maybe_migrate_name(<<"sd123123123123123sd">>)),
?assertEqual(<<"sd">>, maybe_migrate_name(<<"123123123123123sd">>)).
-endif.

View File

@ -0,0 +1,166 @@
%%%
%%% Instrument machine
%%%
-module(ff_instrument_machine).
%% API
-type id() :: machinery:id().
-type ns() :: machinery:namespace().
-type ctx() :: ff_entity_context:context().
-type instrument(T) :: ff_instrument:instrument_state(T).
-type metadata() :: ff_instrument:metadata().
-type event_range() :: {After :: non_neg_integer() | undefined, Limit :: non_neg_integer() | undefined}.
-type st(T) ::
ff_machine:st(instrument(T)).
-type repair_error() :: ff_repair:repair_error().
-type repair_response() :: ff_repair:repair_response().
-export_type([id/0]).
-export_type([st/1]).
-export_type([repair_error/0]).
-export_type([repair_response/0]).
-export_type([events/1]).
-export_type([timestamped_event/1]).
-export_type([params/1]).
-export_type([event_range/0]).
-export([create/3]).
-export([get/2]).
-export([get/3]).
-export([events/3]).
%% Accessors
-export([instrument/1]).
%% Machinery
-behaviour(machinery).
-export([init/4]).
-export([process_timeout/3]).
-export([process_repair/4]).
-export([process_call/4]).
%% Pipeline
-import(ff_pipeline, [do/1, unwrap/1]).
%%
-type params(T) :: #{
id := id(),
identity := ff_identity:id(),
name := binary(),
currency := ff_currency:id(),
resource := ff_instrument:resource(T),
external_id => id(),
metadata => metadata()
}.
-spec create(ns(), params(_), ctx()) ->
ok |
{error,
_InstrumentCreateError |
exists
}.
create(NS, Params = #{id := ID}, Ctx) ->
do(fun () ->
Events = unwrap(ff_instrument:create(Params)),
unwrap(machinery:start(NS, ID, {Events, Ctx}, fistful:backend(NS)))
end).
-spec get(ns(), id()) ->
{ok, st(_)} |
{error, notfound} .
get(NS, ID) ->
ff_machine:get(ff_instrument, NS, ID).
-spec get(ns(), id(), event_range()) ->
{ok, st(_)} |
{error, notfound} .
get(NS, ID, {After, Limit}) ->
ff_machine:get(ff_instrument, NS, ID, {After, Limit, forward}).
%% Accessors
-spec instrument(st(T)) ->
instrument(T).
instrument(St) ->
ff_machine:model(St).
%% Machinery
-type event(T) :: ff_instrument:event(T).
-type events(T) :: [{integer(), ff_machine:timestamped_event(event(T))}].
-type timestamped_event(T) :: {integer(), ff_machine:timestamped_event(event(T))}.
-type machine() :: ff_machine:machine(event(_)).
-type result() :: ff_machine:result(event(_)).
-type handler_opts() :: machinery:handler_opts(_).
-type handler_args() :: machinery:handler_args(_).
-spec init({[event(_)], ctx()}, machine(), _, handler_opts()) ->
result().
init({Events, Ctx}, #{}, _, _Opts) ->
#{
events => ff_machine:emit_events(Events),
action => continue,
aux_state => #{ctx => Ctx}
}.
%%
-spec process_timeout(machine(), handler_args(), handler_opts()) ->
result().
process_timeout(Machine, _, _Opts) ->
St = ff_machine:collapse(ff_instrument, Machine),
process_timeout(deduce_activity(ff_machine:model(St)), St).
process_timeout(authorize, St) ->
D0 = instrument(St),
case ff_instrument:authorize(D0) of
{ok, Events} ->
#{
events => ff_machine:emit_events(Events)
}
end.
deduce_activity(#{status := unauthorized}) ->
authorize;
deduce_activity(#{}) ->
undefined.
%%
-spec process_call(_CallArgs, machine(), handler_args(), handler_opts()) ->
{ok, result()}.
process_call(_CallArgs, #{}, _, _Opts) ->
{ok, #{}}.
-spec process_repair(ff_repair:scenario(), machine(), handler_args(), handler_opts()) ->
{ok, {repair_response(), result()}} | {error, repair_error()}.
process_repair(Scenario, Machine, _Args, _Opts) ->
ff_repair:apply_scenario(ff_instrument, Machine, Scenario).
-spec events(ns(), id(), event_range()) ->
{ok, events(_)} |
{error, notfound}.
events(NS, ID, {After, Limit}) ->
do(fun () ->
History = unwrap(ff_machine:history(ff_instrument, NS, ID, {After, Limit, forward})),
[{EventID, TsEv} || {EventID, _, TsEv} <- History]
end).

View File

@ -4,65 +4,31 @@
%%% TODOs
%%%
%%% - Implement a generic source instead of a current dummy one.
%%%
-module(ff_source).
-type id() :: binary().
-type name() :: binary().
-type ctx() :: ff_entity_context:context().
-type id() :: ff_instrument:id().
-type name() :: ff_instrument:name().
-type account() :: ff_account:account().
-type identity() :: ff_identity:id().
-type currency() :: ff_currency:id().
-type status() :: unauthorized | authorized.
-type metadata() :: ff_entity_context:md().
-type timestamp() :: ff_time:timestamp_ms().
-type status() :: ff_instrument:status().
-type resource() :: #{
type := internal,
details => binary()
}.
-define(ACTUAL_FORMAT_VERSION, 4).
-type source() :: ff_instrument:instrument(resource()).
-type source_state() :: ff_instrument:instrument_state(resource()).
-type params() :: ff_instrument_machine:params(resource()).
-type machine() :: ff_instrument_machine:st(resource()).
-type event_range() :: ff_instrument_machine:event_range().
-type source() :: #{
version := ?ACTUAL_FORMAT_VERSION,
resource := resource(),
name := name(),
created_at => timestamp(),
external_id => id(),
metadata => metadata()
}.
-type source_state() :: #{
account := account() | undefined,
resource := resource(),
name := name(),
status => status(),
created_at => timestamp(),
external_id => id(),
metadata => metadata()
}.
-type params() :: #{
id := id(),
identity := ff_identity:id(),
name := name(),
currency := ff_currency:id(),
resource := resource(),
external_id => id(),
metadata => metadata()
}.
-type event() ::
{created, source_state()} |
{account, ff_account:event()} |
{status_changed, status()}.
-type legacy_event() :: any().
-type create_error() ::
{identity, notfound} |
{currency, notfound} |
ff_account:create_error() |
{identity, ff_party:inaccessibility()}.
-type event() :: ff_instrument:event(resource()).
-type events() :: ff_instrument_machine:events(resource()).
-type timestamped_event() :: ff_instrument_machine:timestamped_event(resource()).
-export_type([id/0]).
-export_type([source/0]).
@ -71,12 +37,12 @@
-export_type([resource/0]).
-export_type([params/0]).
-export_type([event/0]).
-export_type([create_error/0]).
-export_type([timestamped_event/0]).
%% Accessors
-export([id/1]).
-export([account/1]).
-export([id/1]).
-export([name/1]).
-export([identity/1]).
-export([currency/1]).
@ -88,172 +54,94 @@
%% API
-export([create/1]).
-export([create/2]).
-export([get_machine/1]).
-export([get_machine/2]).
-export([ctx/1]).
-export([get/1]).
-export([is_accessible/1]).
-export([authorize/1]).
-export([apply_event/2]).
-export([maybe_migrate/2]).
%% Pipeline
-import(ff_pipeline, [do/1, unwrap/1, unwrap/2]).
-export([events/2]).
%% Accessors
-spec id(source_state()) ->
id() | undefined.
-spec name(source_state()) ->
name().
-spec account(source_state()) ->
account() | undefined.
-spec identity(source_state()) ->
identity().
-spec currency(source_state()) ->
currency().
-spec resource(source_state()) ->
resource().
-spec status(source_state()) ->
status() | undefined.
-spec id(source_state()) -> id().
-spec name(source_state()) -> name().
-spec account(source_state()) -> account().
-spec identity(source_state()) -> identity().
-spec currency(source_state()) -> currency().
-spec resource(source_state()) -> resource().
-spec status(source_state()) -> status() | undefined.
id(Source) ->
case account(Source) of
undefined ->
undefined;
Account ->
ff_account:id(Account)
end.
name(#{name := V}) ->
V.
account(#{account := V}) ->
V;
account(_) ->
undefined.
identity(Source) ->
ff_account:identity(account(Source)).
currency(Source) ->
ff_account:currency(account(Source)).
resource(#{resource := V}) ->
V.
status(#{status := V}) ->
V;
status(_) ->
undefined.
id(Source) -> ff_instrument:id(Source).
name(Source) -> ff_instrument:name(Source).
identity(Source) -> ff_instrument:identity(Source).
currency(Source) -> ff_instrument:currency(Source).
resource(Source) -> ff_instrument:resource(Source).
status(Source) -> ff_instrument:status(Source).
account(Source) -> ff_instrument:account(Source).
-spec external_id(source_state()) ->
id() | undefined.
external_id(#{external_id := ExternalID}) ->
ExternalID;
external_id(_Source) ->
undefined.
external_id(T) -> ff_instrument:external_id(T).
-spec created_at(source_state()) ->
ff_time:timestamp_ms() | undefiend.
created_at(#{created_at := CreatedAt}) ->
CreatedAt;
created_at(_Source) ->
undefined.
ff_time:timestamp_ms().
created_at(T) -> ff_instrument:created_at(T).
-spec metadata(source_state()) ->
ff_entity_context:context() | undefined.
metadata(#{metadata := Metadata}) ->
Metadata;
metadata(_Source) ->
undefined.
ff_entity_context:context().
metadata(T) -> ff_instrument:metadata(T).
%% API
-spec create(params()) ->
{ok, [event()]} |
{error, create_error()}.
create(Params) ->
do(fun () ->
#{
id := ID,
identity := IdentityID,
name := Name,
currency := CurrencyID,
resource := Resource
} = Params,
Identity = ff_identity_machine:identity(unwrap(identity, ff_identity_machine:get(IdentityID))),
Currency = unwrap(currency, ff_currency:get(CurrencyID)),
Events = unwrap(ff_account:create(ID, Identity, Currency)),
accessible = unwrap(identity, ff_identity:is_accessible(Identity)),
CreatedAt = ff_time:now(),
[{created, genlib_map:compact(#{
version => ?ACTUAL_FORMAT_VERSION,
name => Name,
resource => Resource,
external_id => maps:get(external_id, Params, undefined),
metadata => maps:get(metadata, Params, undefined),
created_at => CreatedAt
})}] ++
[{account, Ev} || Ev <- Events] ++
[{status_changed, unauthorized}]
end).
-define(NS, 'ff/source_v1').
-spec create(params(), ctx()) ->
ok |
{error,
_InstrumentCreateError |
exists
}.
create(Params, Ctx) ->
ff_instrument_machine:create(?NS, Params, Ctx).
-spec get_machine(id()) ->
{ok, machine()} |
{error, notfound} .
get_machine(ID) ->
ff_instrument_machine:get(?NS, ID).
-spec get(machine()) ->
source_state().
get(Machine) ->
ff_instrument_machine:instrument(Machine).
-spec get_machine(id(), event_range()) ->
{ok, machine()} |
{error, notfound} .
get_machine(ID, EventRange) ->
ff_instrument_machine:get(?NS, ID, EventRange).
-spec ctx(machine()) ->
ctx().
ctx(St) ->
ff_machine:ctx(St).
-spec is_accessible(source_state()) ->
{ok, accessible} |
{error, ff_party:inaccessibility()}.
is_accessible(Source) ->
ff_account:is_accessible(account(Source)).
ff_instrument:is_accessible(Source).
-spec authorize(source_state()) ->
{ok, [event()]}.
authorize(#{status := unauthorized}) ->
% TODO
% - Do the actual authorization
{ok, [{status_changed, authorized}]};
authorize(#{status := authorized}) ->
{ok, []}.
-spec events(id(), event_range()) ->
{ok, events()} |
{error, notfound}.
-spec apply_event(event(), ff_maybe:maybe(source_state())) ->
source_state().
apply_event({created, Source}, undefined) ->
Source;
apply_event({status_changed, S}, Source) ->
Source#{status => S};
apply_event({account, Ev}, Source = #{account := Account}) ->
Source#{account => ff_account:apply_event(Ev, Account)};
apply_event({account, Ev}, Source) ->
apply_event({account, Ev}, Source#{account => undefined}).
-spec maybe_migrate(event() | legacy_event(), ff_machine:migrate_params()) ->
event().
maybe_migrate(Event = {created, #{version := ?ACTUAL_FORMAT_VERSION}}, _MigrateParams) ->
Event;
maybe_migrate({created, Source = #{version := 3}}, MigrateParams) ->
maybe_migrate({created, Source#{
version => 4
}}, MigrateParams);
maybe_migrate({created, Source = #{version := 2}}, MigrateParams) ->
Context = maps:get(ctx, MigrateParams, undefined),
%% TODO add metada migration for eventsink after decouple instruments
Metadata = ff_entity_context:try_get_legacy_metadata(Context),
maybe_migrate({created, genlib_map:compact(Source#{
version => 3,
metadata => Metadata
})}, MigrateParams);
maybe_migrate({created, Source = #{version := 1}}, MigrateParams) ->
Timestamp = maps:get(timestamp, MigrateParams),
CreatedAt = ff_codec:unmarshal(timestamp_ms, ff_codec:marshal(timestamp, Timestamp)),
maybe_migrate({created, Source#{
version => 2,
created_at => CreatedAt
}}, MigrateParams);
maybe_migrate({created, Source = #{
resource := Resource,
name := Name
}}, MigrateParams) ->
maybe_migrate({created, genlib_map:compact(#{
version => 1,
resource => Resource,
name => Name,
external_id => maps:get(external_id, Source, undefined)
})}, MigrateParams);
%% Other events
maybe_migrate(Event, _MigrateParams) ->
Event.
events(ID, Range) ->
ff_instrument_machine:events(?NS, ID, Range).

View File

@ -1,162 +0,0 @@
%%%
%%% Source machine
%%%
-module(ff_source_machine).
%% API
-type id() :: machinery:id().
-type ctx() :: ff_entity_context:context().
-type source() :: ff_source:source_state().
-type change() :: ff_source:event().
-type event() :: {integer(), ff_machine:timestamped_event(change())}.
-type events() :: [event()].
-type event_range() :: {After :: non_neg_integer() | undefined, Limit :: non_neg_integer() | undefined}.
-type params() :: ff_source:params().
-type st() :: ff_machine:st(source()).
-type repair_error() :: ff_repair:repair_error().
-type repair_response() :: ff_repair:repair_response().
-export_type([id/0]).
-export_type([st/0]).
-export_type([event/0]).
-export_type([repair_error/0]).
-export_type([repair_response/0]).
-export_type([params/0]).
-export_type([event_range/0]).
%% API
-export([create/2]).
-export([get/1]).
-export([get/2]).
-export([events/2]).
%% Accessors
-export([source/1]).
-export([ctx/1]).
%% Machinery
-behaviour(machinery).
-export([init/4]).
-export([process_timeout/3]).
-export([process_repair/4]).
-export([process_call/4]).
%% Pipeline
-import(ff_pipeline, [do/1, unwrap/1]).
%%
-define(NS, 'ff/source_v1').
-spec create(params(), ctx()) ->
ok |
{error, ff_source:create_error() | exists}.
create(#{id := ID} = Params, Ctx) ->
do(fun () ->
Events = unwrap(ff_source:create(Params)),
unwrap(machinery:start(?NS, ID, {Events, Ctx}, backend()))
end).
-spec get(id()) ->
{ok, st()} |
{error, notfound}.
get(ID) ->
ff_machine:get(ff_source, ?NS, ID).
-spec get(id(), event_range()) ->
{ok, st()} |
{error, notfound} .
get(ID, {After, Limit}) ->
ff_machine:get(ff_source, ?NS, ID, {After, Limit, forward}).
-spec events(id(), event_range()) ->
{ok, events()} |
{error, notfound}.
events(ID, {After, Limit}) ->
do(fun () ->
History = unwrap(ff_machine:history(ff_source, ?NS, ID, {After, Limit, forward})),
[{EventID, TsEv} || {EventID, _, TsEv} <- History]
end).
%% Accessors
-spec source(st()) ->
source().
source(St) ->
ff_machine:model(St).
-spec ctx(st()) ->
ctx().
ctx(St) ->
ff_machine:ctx(St).
%% Machinery
-type machine() :: ff_machine:machine(change()).
-type result() :: ff_machine:result(change()).
-type handler_opts() :: machinery:handler_opts(_).
-type handler_args() :: machinery:handler_args(_).
-spec init({[change()], ctx()}, machine(), _, handler_opts()) ->
result().
init({Events, Ctx}, #{}, _, _Opts) ->
#{
events => ff_machine:emit_events(Events),
action => continue,
aux_state => #{ctx => Ctx}
}.
%%
-spec process_timeout(machine(), handler_args(), handler_opts()) ->
result().
process_timeout(Machine, _, _Opts) ->
St = ff_machine:collapse(ff_source, Machine),
process_timeout(deduce_activity(ff_machine:model(St)), St).
process_timeout(authorize, St) ->
D0 = source(St),
case ff_source:authorize(D0) of
{ok, Events} ->
#{
events => ff_machine:emit_events(Events)
}
end.
deduce_activity(#{status := unauthorized}) ->
authorize;
deduce_activity(#{}) ->
undefined.
%%
-spec process_call(_CallArgs, machine(), handler_args(), handler_opts()) ->
{ok, result()}.
process_call(_CallArgs, #{}, _, _Opts) ->
{ok, #{}}.
-spec process_repair(ff_repair:scenario(), machine(), handler_args(), handler_opts()) ->
{ok, {repair_response(), result()}} | {error, repair_error()}.
process_repair(Scenario, Machine, _Args, _Opts) ->
ff_repair:apply_scenario(ff_source, Machine, Scenario).
%% Internals
backend() ->
fistful:backend(?NS).

View File

@ -176,7 +176,7 @@
-type invalid_withdrawal_status_error() ::
{invalid_withdrawal_status, status()}.
-type action() :: sleep | continue | undefined.
-type action() :: poll | continue | undefined.
-export_type([withdrawal/0]).
-export_type([withdrawal_state/0]).
@ -199,10 +199,6 @@
-export([process_transfer/1]).
%%
-export([process_session_finished/3]).
%% Accessors
-export([wallet_id/1]).
@ -299,7 +295,7 @@
p_transfer_start |
p_transfer_prepare |
session_starting |
session_sleeping |
session_polling |
p_transfer_commit |
p_transfer_cancel |
limit_check |
@ -332,8 +328,8 @@ destination_resource(#{resource := Resource}) ->
Resource;
destination_resource(Withdrawal) ->
DestinationID = destination_id(Withdrawal),
{ok, DestinationMachine} = ff_destination_machine:get(DestinationID),
Destination = ff_destination_machine:destination(DestinationMachine),
{ok, DestinationMachine} = ff_destination:get_machine(DestinationID),
Destination = ff_destination:get(DestinationMachine),
{ok, Resource} = ff_destination:resource_full(Destination),
Resource.
@ -511,51 +507,6 @@ process_transfer(Withdrawal) ->
Activity = deduce_activity(Withdrawal),
do_process_transfer(Activity, Withdrawal).
%%
-spec process_session_finished(session_id(), session_result(), withdrawal_state()) ->
{ok, process_result()} | {error, session_not_found | old_session | result_mismatch}.
process_session_finished(SessionID, SessionResult, Withdrawal) ->
case get_session_by_id(SessionID, Withdrawal) of
#{id := SessionID, result := SessionResult} ->
{ok, {undefined, []}};
#{id := SessionID, result := _OtherSessionResult} ->
{error, result_mismatch};
#{id := SessionID} ->
try_finish_session(SessionID, SessionResult, Withdrawal);
undefined ->
{error, session_not_found}
end.
-spec get_session_by_id(session_id(), withdrawal_state()) ->
session() | undefined.
get_session_by_id(SessionID, Withdrawal) ->
Sessions = ff_withdrawal_route_attempt_utils:get_sessions(attempts(Withdrawal)),
case lists:filter(fun(#{id := SessionID0}) -> SessionID0 =:= SessionID end, Sessions) of
[Session] -> Session;
[] -> undefined
end.
-spec try_finish_session(session_id(), session_result(), withdrawal_state()) ->
{ok, process_result()} | {error, old_session}.
try_finish_session(SessionID, SessionResult, Withdrawal) ->
case is_current_session(SessionID, Withdrawal) of
true ->
{ok, {continue, [{session_finished, {SessionID, SessionResult}}]}};
false ->
{error, old_session}
end.
-spec is_current_session(session_id(), withdrawal_state()) ->
boolean().
is_current_session(SessionID, Withdrawal) ->
case session_id(Withdrawal) of
SessionID ->
true;
_ ->
false
end.
%% Internals
-spec do_start_adjustment(adjustment_params(), withdrawal_state()) ->
@ -688,7 +639,7 @@ do_pending_activity(#{p_transfer := prepared, limit_check := failed}) ->
do_pending_activity(#{p_transfer := cancelled, limit_check := failed}) ->
{fail, limit_check};
do_pending_activity(#{p_transfer := prepared, session := pending}) ->
session_sleeping;
session_polling;
do_pending_activity(#{p_transfer := prepared, session := succeeded}) ->
p_transfer_commit;
do_pending_activity(#{p_transfer := committed, session := succeeded}) ->
@ -732,8 +683,8 @@ do_process_transfer(limit_check, Withdrawal) ->
process_limit_check(Withdrawal);
do_process_transfer(session_starting, Withdrawal) ->
process_session_creation(Withdrawal);
do_process_transfer(session_sleeping, Withdrawal) ->
process_session_sleep(Withdrawal);
do_process_transfer(session_polling, Withdrawal) ->
process_session_poll(Withdrawal);
do_process_transfer({fail, Reason}, Withdrawal) ->
{ok, Providers} = do_process_routing(Withdrawal),
process_route_change(Providers, Withdrawal, Reason);
@ -873,8 +824,8 @@ process_session_creation(Withdrawal) ->
Wallet = ff_wallet_machine:wallet(WalletMachine),
WalletAccount = ff_wallet:account(Wallet),
{ok, DestinationMachine} = ff_destination_machine:get(DestinationID),
Destination = ff_destination_machine:destination(DestinationMachine),
{ok, DestinationMachine} = ff_destination:get_machine(DestinationID),
Destination = ff_destination:get(DestinationMachine),
DestinationAccount = ff_destination:account(Destination),
Route = route(Withdrawal),
@ -918,15 +869,15 @@ create_session(ID, TransferData, SessionParams) ->
ok
end.
-spec process_session_sleep(withdrawal_state()) ->
-spec process_session_poll(withdrawal_state()) ->
process_result().
process_session_sleep(Withdrawal) ->
process_session_poll(Withdrawal) ->
SessionID = session_id(Withdrawal),
{ok, SessionMachine} = ff_withdrawal_session_machine:get(SessionID),
Session = ff_withdrawal_session_machine:session(SessionMachine),
case ff_withdrawal_session:status(Session) of
active ->
{sleep, []};
{poll, []};
{finished, _} ->
Result = ff_withdrawal_session:result(Session),
{continue, [{session_finished, {SessionID, Result}}]}
@ -1047,8 +998,8 @@ get_wallet(WalletID) ->
{ok, destination()} | {error, notfound}.
get_destination(DestinationID) ->
do(fun() ->
DestinationMachine = unwrap(ff_destination_machine:get(DestinationID)),
ff_destination_machine:destination(DestinationMachine)
DestinationMachine = unwrap(ff_destination:get_machine(DestinationID)),
ff_destination:get(DestinationMachine)
end).
-spec get_wallet_identity(wallet()) ->

View File

@ -55,7 +55,6 @@
-export([repair/2]).
-export([start_adjustment/2]).
-export([notify_session_finished/3]).
%% Accessors
@ -79,12 +78,8 @@
-type adjustment_params() :: ff_withdrawal:adjustment_params().
-type session_id() :: ff_withdrawal_session:id().
-type session_result() :: ff_withdrawal_session:session_result().
-type call() ::
{start_adjustment, adjustment_params()} |
{session_finished, session_id(), session_result()}.
{start_adjustment, adjustment_params()}.
-define(NS, 'ff/withdrawal_v2').
@ -144,11 +139,6 @@ repair(ID, Scenario) ->
start_adjustment(WithdrawalID, Params) ->
call(WithdrawalID, {start_adjustment, Params}).
-spec notify_session_finished(id(), session_id(), session_result()) ->
ok | {error, session_not_found | old_session | result_mismatch}.
notify_session_finished(WithdrawalID, SessionID, SessionResult) ->
call(WithdrawalID, {session_finished, SessionID, SessionResult}).
%% Accessors
-spec withdrawal(st()) ->
@ -170,6 +160,8 @@ ctx(St) ->
-type handler_opts() :: machinery:handler_opts(_).
-type handler_args() :: machinery:handler_args(_).
-define(MAX_SESSION_POLL_TIMEOUT, 4 * 60 * 60).
backend() ->
fistful:backend(?NS).
@ -196,8 +188,6 @@ process_timeout(Machine, _, _Opts) ->
process_call({start_adjustment, Params}, Machine, _, _Opts) ->
do_start_adjustment(Params, Machine);
process_call({session_finished, SessionID, SessionResult}, Machine, _, _Opts) ->
do_process_session_finished(SessionID, SessionResult, Machine);
process_call(CallArgs, _Machine, _, _Opts) ->
erlang:error({unexpected_call, CallArgs}).
@ -219,17 +209,6 @@ do_start_adjustment(Params, Machine) ->
{Error, #{}}
end.
-spec do_process_session_finished(session_id(), session_result(), machine()) -> {Response, result()} when
Response :: ok | {error, session_not_found | old_session | result_mismatch}.
do_process_session_finished(SessionID, SessionResult, Machine) ->
St = ff_machine:collapse(ff_withdrawal, Machine),
case ff_withdrawal:process_session_finished(SessionID, SessionResult, withdrawal(St)) of
{ok, Result} ->
{ok, process_result(Result, St)};
{error, _Reason} = Error ->
{Error, #{}}
end.
process_result({Action, Events}, St) ->
genlib_map:compact(#{
events => set_events(Events),
@ -245,8 +224,14 @@ set_action(continue, _St) ->
continue;
set_action(undefined, _St) ->
undefined;
set_action(sleep, _St) ->
unset_timer.
set_action(poll, St) ->
Now = machinery_time:now(),
{set_timer, {timeout, compute_poll_timeout(Now, St)}}.
compute_poll_timeout(Now, St) ->
MaxTimeout = genlib_app:env(ff_transfer, max_session_poll_timeout, ?MAX_SESSION_POLL_TIMEOUT),
Timeout0 = machinery_time:interval(Now, ff_machine:updated(St)) div 1000,
erlang:min(MaxTimeout, erlang:max(1, Timeout0)).
call(ID, Call) ->
case machinery:call(?NS, ID, Call, backend()) of

View File

@ -102,18 +102,6 @@
opts := ff_withdrawal_provider:adapter_opts()
}.
-type id() :: machinery:id().
-type action() ::
undefined |
continue |
{setup_callback, machinery:tag(), machinery:timer()} |
{setup_timer, machinery:timer()} |
retry |
finish.
-type process_result() :: {action(), [event()]}.
-export_type([id/0]).
-export_type([data/0]).
-export_type([event/0]).
@ -126,12 +114,15 @@
-export_type([callback_params/0]).
-export_type([process_callback_response/0]).
-export_type([process_callback_error/0]).
-export_type([process_result/0]).
-export_type([action/0]).
%%
%% Internal types
%%
-type id() :: machinery:id().
-type auxst() :: undefined.
-type result() :: machinery:result(event(), auxst()).
-type withdrawal() :: ff_adapter_withdrawal:withdrawal().
-type callbacks_index() :: ff_withdrawal_callback_utils:index().
-type adapter_with_opts() :: {ff_withdrawal_provider:adapter(), ff_withdrawal_provider:adapter_opts()}.
@ -224,18 +215,7 @@ apply_event({callback, _Ev} = WrappedEvent, Session) ->
Callbacks1 = ff_withdrawal_callback_utils:apply_event(WrappedEvent, Callbacks0),
set_callbacks_index(Callbacks1, Session).
-spec process_session(session_state()) -> process_result().
process_session(#{status := {finished, _}, id := ID, result := Result, withdrawal := Withdrawal}) ->
% Session has finished, it should notify the withdrawal machine about the fact
WithdrawalID = ff_adapter_withdrawal:id(Withdrawal),
case ff_withdrawal_machine:notify_session_finished(WithdrawalID, ID, Result) of
ok ->
{finish, []};
{error, session_not_found} ->
{retry, []};
{error, _} = Error ->
erlang:error({unable_to_finish_session, Error})
end;
-spec process_session(session_state()) -> result().
process_session(#{status := active, withdrawal := Withdrawal, route := Route} = SessionState) ->
{Adapter, AdapterOpts} = get_adapter_with_opts(Route),
ASt = maps:get(adapter_state, SessionState, undefined),
@ -243,7 +223,7 @@ process_session(#{status := active, withdrawal := Withdrawal, route := Route} =
#{intent := Intent} = ProcessResult,
Events0 = process_next_state(ProcessResult, []),
Events1 = process_transaction_info(ProcessResult, Events0, SessionState),
process_adapter_intent(Intent, SessionState, Events1).
process_intent(Intent, SessionState, Events1).
process_transaction_info(#{transaction_info := TrxInfo}, Events, SessionState) ->
ok = assert_transaction_info(TrxInfo, transaction_info(SessionState)),
@ -261,24 +241,27 @@ assert_transaction_info(NewTrxInfo, _TrxInfo) ->
erlang:error({transaction_info_is_different, NewTrxInfo}).
-spec set_session_result(session_result(), session_state()) ->
process_result().
set_session_result(Result, Session = #{status := active}) ->
process_adapter_intent({finish, Result}, Session).
result().
set_session_result(Result, #{status := active}) ->
#{
events => [{finished, Result}],
action => unset_timer
}.
-spec process_callback(callback_params(), session_state()) ->
{ok, {process_callback_response(), process_result()}} |
{error, {process_callback_error(), process_result()}}.
{ok, {process_callback_response(), result()}} |
{error, {process_callback_error(), result()}}.
process_callback(#{tag := CallbackTag} = Params, Session) ->
{ok, Callback} = find_callback(CallbackTag, Session),
case ff_withdrawal_callback:status(Callback) of
succeeded ->
{ok, {ff_withdrawal_callback:response(Callback), {undefined, []}}};
{ok, {ff_withdrawal_callback:response(Callback), #{}}};
pending ->
case status(Session) of
active ->
do_process_callback(Params, Callback, Session);
{finished, _} ->
{error, {{session_already_finished, make_session_finish_params(Session)}, {undefined, []}}}
{error, {{session_already_finished, make_session_finish_params(Session)}, #{}}}
end
end.
@ -300,7 +283,7 @@ do_process_callback(CallbackParams, Callback, Session) ->
Events0 = ff_withdrawal_callback_utils:process_response(Response, Callback),
Events1 = process_next_state(HandleCallbackResult, Events0),
Events2 = process_transaction_info(HandleCallbackResult, Events1, Session),
{ok, {Response, process_adapter_intent(Intent, Session, Events2)}}.
{ok, {Response, process_intent(Intent, Session, Events2)}}.
make_session_finish_params(Session) ->
{_Adapter, AdapterOpts} = get_adapter_with_opts(route(Session)),
@ -315,21 +298,28 @@ process_next_state(#{next_state := NextState}, Events) ->
process_next_state(_, Events) ->
Events.
process_adapter_intent(Intent, Session, Events0) ->
{Action, Events1} = process_adapter_intent(Intent, Session),
{Action, Events0 ++ Events1}.
process_intent(Intent, Session, Events) ->
#{events := Events0} = Result = process_intent(Intent, Session),
Result#{events => Events ++ Events0}.
process_adapter_intent({finish, {success, _TransactionInfo}}, _Session) ->
process_intent({finish, {success, _TransactionInfo}}, _Session) ->
%% we ignore TransactionInfo here
%% @see ff_adapter_withdrawal:rebind_transaction_info/1
{continue, [{finished, success}]};
process_adapter_intent({finish, Result}, _Session) ->
{continue, [{finished, Result}]};
process_adapter_intent({sleep, #{timer := Timer, tag := Tag}}, Session) ->
Events = create_callback(Tag, Session),
{{setup_callback, Tag, Timer}, Events};
process_adapter_intent({sleep, #{timer := Timer}}, _Session) ->
{{setup_timer, Timer}, []}.
#{
events => [{finished, success}],
action => unset_timer
};
process_intent({finish, Result}, _Session) ->
#{
events => [{finished, Result}],
action => unset_timer
};
process_intent({sleep, #{timer := Timer} = Params}, Session) ->
CallbackEvents = create_callback(Params, Session),
#{
events => CallbackEvents,
action => maybe_add_tag_action(Params, [timer_action(Timer)])
}.
%%
@ -344,14 +334,16 @@ create_session(ID, Data, #{withdrawal_id := WdthID, resource := Res, route := Ro
status => active
}.
create_callback(Tag, Session) ->
create_callback(#{tag := Tag}, Session) ->
case ff_withdrawal_callback_utils:get_by_tag(Tag, callbacks_index(Session)) of
{error, {unknown_callback, Tag}} ->
{ok, CallbackEvents} = ff_withdrawal_callback:create(#{tag => Tag}),
ff_withdrawal_callback_utils:wrap_events(Tag, CallbackEvents);
{ok, Callback} ->
erlang:error({callback_already_exists, Callback})
end.
end;
create_callback(_, _) ->
[].
-spec convert_identity_state_to_adapter_identity(ff_identity:identity_state()) ->
ff_adapter_withdrawal:identity().
@ -409,3 +401,13 @@ create_adapter_withdrawal(#{id := SesID, sender := Sender, receiver := Receiver}
-spec set_callbacks_index(callbacks_index(), session_state()) -> session_state().
set_callbacks_index(Callbacks, Session) ->
Session#{callbacks => Callbacks}.
-spec timer_action({deadline, binary()} | {timeout, non_neg_integer()}) -> machinery:action().
timer_action(Timer) ->
{set_timer, Timer}.
-spec maybe_add_tag_action(SleepIntentParams :: map(), [machinery:action()]) -> [machinery:action()].
maybe_add_tag_action(#{tag := Tag}, Actions) ->
[{tag, Tag} | Actions];
maybe_add_tag_action(_, Actions) ->
Actions.

View File

@ -58,7 +58,6 @@
-type st() :: ff_machine:st(session()).
-type session() :: ff_withdrawal_session:session_state().
-type event() :: ff_withdrawal_session:event().
-type action() :: ff_withdrawal_session:action().
-type event_range() :: {After :: non_neg_integer() | undefined, Limit :: non_neg_integer() | undefined}.
-type callback_params() :: ff_withdrawal_session:callback_params().
@ -67,8 +66,6 @@
{unknown_session, {tag, id()}} |
ff_withdrawal_session:process_callback_error().
-type process_result() :: ff_withdrawal_session:process_result().
-type ctx() :: ff_entity_context:context().
%% Pipeline
@ -79,9 +76,6 @@
%% API
%%
-define(SESSION_RETRY_TIME_LIMIT, 24 * 60 * 60).
-define(MAX_SESSION_RETRY_TIMEOUT, 4 * 60 * 60).
-spec session(st()) -> session().
session(St) ->
@ -153,11 +147,14 @@ init(Events, #{}, _, _Opts) ->
result().
process_timeout(Machine, _, _Opts) ->
State = ff_machine:collapse(ff_withdrawal_session, Machine),
Session = session(State),
process_result(ff_withdrawal_session:process_session(Session), State).
#{events := Events} = Result = ff_withdrawal_session:process_session(session(State)),
Result#{
events => ff_machine:emit_events(Events)
}.
-spec process_call(any(), machine(), handler_args(), handler_opts()) ->
{ok, result()}.
process_call({process_callback, Params}, Machine, _, _Opts) ->
do_process_callback(Params, Machine);
process_call(_CallArgs, #{}, _, _Opts) ->
@ -169,8 +166,7 @@ process_repair(Scenario, Machine, _Args, _Opts) ->
ScenarioProcessors = #{
set_session_result => fun(Args, RMachine) ->
State = ff_machine:collapse(ff_withdrawal_session, RMachine),
{Action, Events} = ff_withdrawal_session:set_session_result(Args, session(State)),
{ok, {ok, #{action => set_action(Action, State), events => Events}}}
{ok, {ok, ff_withdrawal_session:set_session_result(Args, session(State))}}
end
},
ff_repair:apply_scenario(ff_withdrawal_session, Machine, Scenario, ScenarioProcessors).
@ -179,102 +175,6 @@ process_repair(Scenario, Machine, _Args, _Opts) ->
%% Internals
%%
-spec process_result(process_result(), st()) ->
result().
process_result({Action, Events}, St) ->
genlib_map:compact(#{
events => set_events(Events),
action => set_action(Action, St)
}).
-spec set_events([event()]) ->
undefined | ff_machine:timestamped_event(event()).
set_events([]) ->
undefined;
set_events(Events) ->
ff_machine:emit_events(Events).
-spec set_action(action(), st()) ->
undefined | machinery:action() | [machinery:action()].
set_action(continue, _St) ->
continue;
set_action(undefined, _St) ->
undefined;
set_action({setup_callback, Tag, Timer}, _St) ->
[tag_action(Tag), timer_action(Timer)];
set_action({setup_timer, Timer}, _St) ->
timer_action(Timer);
set_action(retry, St) ->
case compute_retry_timer(St) of
{ok, Timer} ->
timer_action(Timer);
{error, deadline_reached} = Error ->
erlang:error(Error)
end;
set_action(finish, _St) ->
unset_timer.
%%
-spec compute_retry_timer(st()) ->
{ok, machinery:timer()} | {error, deadline_reached}.
compute_retry_timer(St) ->
Now = machinery_time:now(),
Updated = ff_machine:updated(St),
Deadline = compute_retry_deadline(Updated),
Timeout = compute_next_timeout(Now, Updated),
check_next_timeout(Timeout, Now, Deadline).
-spec compute_retry_deadline(machinery:timestamp()) ->
machinery:timestamp().
compute_retry_deadline(Updated) ->
RetryTimeLimit = genlib_app:env(ff_transfer, session_retry_time_limit, ?SESSION_RETRY_TIME_LIMIT),
machinery_time:add_seconds(RetryTimeLimit, Updated).
-spec compute_next_timeout(machinery:timestamp(), machinery:timestamp()) ->
timeout().
compute_next_timeout(Now, Updated) ->
MaxTimeout = genlib_app:env(ff_transfer, max_session_retry_timeout, ?MAX_SESSION_RETRY_TIMEOUT),
Timeout0 = machinery_time:interval(Now, Updated) div 1000,
erlang:min(MaxTimeout, erlang:max(1, Timeout0)).
-spec check_next_timeout(timeout(), machinery:timestamp(), machinery:timestamp()) ->
{ok, machinery:timer()} | {error, deadline_reached}.
check_next_timeout(Timeout, Now, Deadline) ->
case check_deadline(machinery_time:add_seconds(Timeout, Now), Deadline) of
ok ->
{ok, {timeout, Timeout}};
{error, _} = Error ->
Error
end.
-spec check_deadline(machinery:timestamp(), machinery:timestamp()) ->
ok | {error, deadline_reached}.
check_deadline({Now, _}, {Deadline, _}) ->
check_deadline_(
calendar:datetime_to_gregorian_seconds(Now),
calendar:datetime_to_gregorian_seconds(Deadline)
).
-spec check_deadline_(integer(), integer()) ->
ok | {error, deadline_reached}.
check_deadline_(Now, Deadline) when Now < Deadline ->
ok;
check_deadline_(Now, Deadline) when Now >= Deadline ->
{error, deadline_reached}.
%%
-spec timer_action(machinery:timer()) ->
machinery:action().
timer_action(Timer) ->
{set_timer, Timer}.
-spec tag_action(machinery:tag()) ->
machinery:action().
tag_action(Tag) ->
{tag, Tag}.
backend() ->
fistful:backend(?NS).
@ -292,10 +192,12 @@ call(Ref, Call) ->
{error, ff_withdrawal_session:process_callback_error()}.
do_process_callback(Params, Machine) ->
St= ff_machine:collapse(ff_withdrawal_session, Machine),
St = ff_machine:collapse(ff_withdrawal_session, Machine),
case ff_withdrawal_session:process_callback(Params, session(St)) of
{ok, {Response, #{events := Events} = Result}} ->
{{ok, Response}, Result#{events => ff_machine:emit_events(Events)}};
{ok, {Response, Result}} ->
{{ok, Response}, process_result(Result, St)};
{error, {Reason, _Result}} ->
{{error, Reason}, #{}}
{{ok, Response}, Result};
{error, {Reason, Result}} ->
{{error, Reason}, Result}
end.

View File

@ -341,13 +341,12 @@ create_source(IID, _C) ->
ID = generate_id(),
SrcResource = #{type => internal, details => <<"Infinite source of cash">>},
Params = #{id => ID, identity => IID, name => <<"XSource">>, currency => <<"RUB">>, resource => SrcResource},
ok = ff_source_machine:create(Params, ff_entity_context:new()),
ok = ff_source:create(Params, ff_entity_context:new()),
authorized = ct_helper:await(
authorized,
fun () ->
{ok, SrcM} = ff_source_machine:get(ID),
Source = ff_source_machine:source(SrcM),
ff_source:status(Source)
{ok, SrcM} = ff_source:get_machine(ID),
ff_source:status(ff_source:get(SrcM))
end
),
ID.

View File

@ -426,9 +426,8 @@ get_wallet_balance(ID) ->
get_account_balance(ff_wallet:account(ff_wallet_machine:wallet(Machine))).
get_source_balance(ID) ->
{ok, Machine} = ff_source_machine:get(ID),
Source = ff_source_machine:source(Machine),
get_account_balance(ff_source:account(Source)).
{ok, Machine} = ff_source:get_machine(ID),
get_account_balance(ff_source:account(ff_source:get(Machine))).
get_account_balance(Account) ->
{ok, {Amounts, Currency}} = ff_transaction:balance(
@ -444,13 +443,12 @@ create_source(IID, _C) ->
ID = generate_id(),
SrcResource = #{type => internal, details => <<"Infinite source of cash">>},
Params = #{id => ID, identity => IID, name => <<"XSource">>, currency => <<"RUB">>, resource => SrcResource},
ok = ff_source_machine:create(Params, ff_entity_context:new()),
ok = ff_source:create(Params, ff_entity_context:new()),
authorized = ct_helper:await(
authorized,
fun () ->
{ok, SrcM} = ff_source_machine:get(ID),
Source = ff_source_machine:source(SrcM),
ff_source:status(Source)
{ok, SrcM} = ff_source:get_machine(ID),
ff_source:status(ff_source:get(SrcM))
end
),
ID.

View File

@ -423,9 +423,8 @@ get_wallet_balance(ID) ->
get_account_balance(ff_wallet:account(ff_wallet_machine:wallet(Machine))).
get_source_balance(ID) ->
{ok, Machine} = ff_source_machine:get(ID),
Source = ff_source_machine:source(Machine),
get_account_balance(ff_source:account(Source)).
{ok, Machine} = ff_source:get_machine(ID),
get_account_balance(ff_source:account(ff_source:get(Machine))).
set_wallet_balance({Amount, Currency}, ID) ->
TransactionID = generate_id(),
@ -453,13 +452,12 @@ create_source(IID, _C) ->
ID = generate_id(),
SrcResource = #{type => internal, details => <<"Infinite source of cash">>},
Params = #{id => ID, identity => IID, name => <<"XSource">>, currency => <<"RUB">>, resource => SrcResource},
ok = ff_source_machine:create(Params, ff_entity_context:new()),
ok = ff_source:create(Params, ff_entity_context:new()),
authorized = ct_helper:await(
authorized,
fun () ->
{ok, SrcM} = ff_source_machine:get(ID),
Source = ff_source_machine:source(SrcM),
ff_source:status(Source)
{ok, SrcM} = ff_source:get_machine(ID),
ff_source:status(ff_source:get(SrcM))
end
),
ID.

View File

@ -473,9 +473,8 @@ get_wallet_balance(ID) ->
get_account_balance(ff_wallet:account(ff_wallet_machine:wallet(Machine))).
get_source_balance(ID) ->
{ok, Machine} = ff_source_machine:get(ID),
Source = ff_source_machine:source(Machine),
get_account_balance(ff_source:account(Source)).
{ok, Machine} = ff_source:get_machine(ID),
get_account_balance(ff_source:account(ff_source:get(Machine))).
set_wallet_balance({Amount, Currency}, ID) ->
TransactionID = generate_id(),
@ -503,13 +502,12 @@ create_source(IID, _C) ->
ID = generate_id(),
SrcResource = #{type => internal, details => <<"Infinite source of cash">>},
Params = #{id => ID, identity => IID, name => <<"XSource">>, currency => <<"RUB">>, resource => SrcResource},
ok = ff_source_machine:create(Params, ff_entity_context:new()),
ok = ff_source:create(Params, ff_entity_context:new()),
authorized = ct_helper:await(
authorized,
fun () ->
{ok, SrcM} = ff_source_machine:get(ID),
Source = ff_source_machine:source(SrcM),
ff_source:status(Source)
{ok, SrcM} = ff_source:get_machine(ID),
ff_source:status(ff_source:get(SrcM))
end
),
ID.

View File

@ -1,6 +1,7 @@
-module(ff_destination_SUITE).
-module(ff_instrument_SUITE).
-include_lib("fistful_proto/include/ff_proto_destination_thrift.hrl").
-include_lib("fistful_proto/include/ff_proto_source_thrift.hrl").
-include_lib("damsel/include/dmsl_domain_thrift.hrl").
-include_lib("stdlib/include/assert.hrl").
@ -15,10 +16,15 @@
-export([end_per_testcase/2]).
% Tests
-export([create_source_ok_test/1]).
-export([create_destination_ok_test/1]).
-export([create_source_identity_notfound_fail_test/1]).
-export([create_destination_identity_notfound_fail_test/1]).
-export([create_source_currency_notfound_fail_test/1]).
-export([create_destination_currency_notfound_fail_test/1]).
-export([get_source_ok_test/1]).
-export([get_destination_ok_test/1]).
-export([get_source_notfound_fail_test/1]).
-export([get_destination_notfound_fail_test/1]).
-type config() :: ct_helper:config().
@ -36,10 +42,15 @@ all() ->
groups() ->
[
{default, [parallel], [
create_source_ok_test,
create_destination_ok_test,
create_source_identity_notfound_fail_test,
create_destination_identity_notfound_fail_test,
create_source_currency_notfound_fail_test,
create_destination_currency_notfound_fail_test,
get_source_ok_test,
get_destination_ok_test,
get_source_notfound_fail_test,
get_destination_notfound_fail_test
]}
].
@ -78,6 +89,13 @@ end_per_testcase(_Name, _C) ->
%% Default group test cases
-spec create_source_ok_test(config()) -> test_return().
create_source_ok_test(C) ->
Party = create_party(C),
IID = create_person_identity(Party, C),
_SourceID = create_source(IID, C),
ok.
-spec create_destination_ok_test(config()) -> test_return().
create_destination_ok_test(C) ->
Party = create_party(C),
@ -85,6 +103,20 @@ create_destination_ok_test(C) ->
_DestinationID = create_destination(IID, C),
ok.
-spec create_source_identity_notfound_fail_test(config()) -> test_return().
create_source_identity_notfound_fail_test(_C) ->
IID = <<"BadIdentityID">>,
SrcResource = #{type => internal, details => <<"Infinite source of cash">>},
Params = #{
id => genlib:unique(),
identity => IID,
name => <<"XSource">>,
currency => <<"RUB">>,
resource => SrcResource
},
CreateResult = ff_source:create(Params, ff_entity_context:new()),
?assertEqual({error, {identity, notfound}}, CreateResult).
-spec create_destination_identity_notfound_fail_test(config()) -> test_return().
create_destination_identity_notfound_fail_test(C) ->
IID = <<"BadIdentityID">>,
@ -104,9 +136,24 @@ create_destination_identity_notfound_fail_test(C) ->
currency => <<"RUB">>,
resource => DestResource
},
CreateResult = ff_destination_machine:create(Params, ff_entity_context:new()),
CreateResult = ff_destination:create(Params, ff_entity_context:new()),
?assertEqual({error, {identity, notfound}}, CreateResult).
-spec create_source_currency_notfound_fail_test(config()) -> test_return().
create_source_currency_notfound_fail_test(C) ->
Party = create_party(C),
IID = create_person_identity(Party, C),
SrcResource = #{type => internal, details => <<"Infinite source of cash">>},
Params = #{
id => genlib:unique(),
identity => IID,
name => <<"XSource">>,
currency => <<"BadUnknownCurrency">>,
resource => SrcResource
},
CreateResult = ff_source:create(Params, ff_entity_context:new()),
?assertEqual({error, {currency, notfound}}, CreateResult).
-spec create_destination_currency_notfound_fail_test(config()) -> test_return().
create_destination_currency_notfound_fail_test(C) ->
Party = create_party(C),
@ -128,15 +175,31 @@ create_destination_currency_notfound_fail_test(C) ->
currency => <<"BadUnknownCurrency">>,
resource => DestResource
},
CreateResult = ff_destination_machine:create(Params, ff_entity_context:new()),
CreateResult = ff_destination:create(Params, ff_entity_context:new()),
?assertEqual({error, {currency, notfound}}, CreateResult).
-spec get_source_ok_test(config()) -> test_return().
get_source_ok_test(C) ->
Party = create_party(C),
IID = create_person_identity(Party, C),
SourceID = create_source(IID, C),
{ok, SourceMachine} = ff_source:get_machine(SourceID),
?assertMatch(
#{
account := #{currency := <<"RUB">>},
name := <<"XSource">>,
resource := #{details := <<"Infinite source of cash">>, type := internal},
status := authorized
},
ff_destination:get(SourceMachine)
).
-spec get_destination_ok_test(config()) -> test_return().
get_destination_ok_test(C) ->
Party = create_party(C),
IID = create_person_identity(Party, C),
DestinationID = create_destination(IID, C),
{ok, DestinationMachine} = ff_destination_machine:get(DestinationID),
{ok, DestinationMachine} = ff_destination:get_machine(DestinationID),
?assertMatch(
#{
account := #{currency := <<"RUB">>},
@ -153,12 +216,16 @@ get_destination_ok_test(C) ->
},
status := authorized
},
ff_destination_machine:destination(DestinationMachine)
ff_destination:get(DestinationMachine)
).
-spec get_source_notfound_fail_test(config()) -> test_return().
get_source_notfound_fail_test(_C) ->
?assertEqual({error, notfound}, ff_source:get_machine(<<"BadID">>)).
-spec get_destination_notfound_fail_test(config()) -> test_return().
get_destination_notfound_fail_test(_C) ->
?assertEqual({error, notfound}, ff_destination_machine:get(<<"BadID">>)).
?assertEqual({error, notfound}, ff_destination:get_machine(<<"BadID">>)).
%% Common functions
@ -184,23 +251,41 @@ create_identity(Party, Name, ProviderID, ClassID, _C) ->
),
ID.
create_destination(IID, C) ->
DestResource = {bank_card, #{bank_card => ct_cardstore:bank_card(<<"4150399999000900">>, {12, 2025}, C)}},
DestID = create_destination(IID, <<"XDestination">>, <<"RUB">>, DestResource),
create_instrument(Type, IdentityID, Name, Currency, Resource, C) ->
ID = genlib:unique(),
ok = create_instrument(
Type,
#{id => ID, identity => IdentityID, name => Name, currency => Currency, resource => Resource},
ff_entity_context:new(),
C
),
ID.
create_instrument(destination, Params, Ctx, _C) ->
ff_destination:create(Params, Ctx);
create_instrument(source, Params, Ctx, _C) ->
ff_source:create(Params, Ctx).
create_source(IID, C) ->
SrcResource = #{type => internal, details => <<"Infinite source of cash">>},
SrcID = create_instrument(source, IID, <<"XSource">>, <<"RUB">>, SrcResource, C),
authorized = ct_helper:await(
authorized,
fun () ->
{ok, DestM} = ff_destination_machine:get(DestID),
Destination = ff_destination_machine:destination(DestM),
ff_destination:status(Destination)
{ok, SrcM} = ff_source:get_machine(SrcID),
ff_source:status(ff_source:get(SrcM))
end
),
SrcID.
create_destination(IID, C) ->
DestResource = {bank_card, #{bank_card => ct_cardstore:bank_card(<<"4150399999000900">>, {12, 2025}, C)}},
DestID = create_instrument(destination, IID, <<"XDestination">>, <<"RUB">>, DestResource, C),
authorized = ct_helper:await(
authorized,
fun () ->
{ok, DestM} = ff_destination:get_machine(DestID),
ff_destination:status(ff_destination:get(DestM))
end
),
DestID.
create_destination(IdentityID, Name, Currency, Resource) ->
ID = genlib:unique(),
ok = ff_destination_machine:create(
#{id => ID, identity => IdentityID, name => Name, currency => Currency, resource => Resource},
ff_entity_context:new()
),
ID.

View File

@ -1,180 +0,0 @@
-module(ff_source_SUITE).
-include_lib("fistful_proto/include/ff_proto_source_thrift.hrl").
-include_lib("damsel/include/dmsl_domain_thrift.hrl").
-include_lib("stdlib/include/assert.hrl").
% Common test API
-export([all/0]).
-export([groups/0]).
-export([init_per_suite/1]).
-export([end_per_suite/1]).
-export([init_per_group/2]).
-export([end_per_group/2]).
-export([init_per_testcase/2]).
-export([end_per_testcase/2]).
% Tests
-export([create_source_ok_test/1]).
-export([create_source_identity_notfound_fail_test/1]).
-export([create_source_currency_notfound_fail_test/1]).
-export([get_source_ok_test/1]).
-export([get_source_notfound_fail_test/1]).
-type config() :: ct_helper:config().
-type test_case_name() :: ct_helper:test_case_name().
-type group_name() :: ct_helper:group_name().
-type test_return() :: _ | no_return().
-spec all() -> [test_case_name() | {group, group_name()}].
all() ->
[
{group, default}
].
-spec groups() -> [{group_name(), list(), [test_case_name()]}].
groups() ->
[
{default, [parallel], [
create_source_ok_test,
create_source_identity_notfound_fail_test,
create_source_currency_notfound_fail_test,
get_source_ok_test,
get_source_notfound_fail_test
]}
].
-spec init_per_suite(config()) -> config().
init_per_suite(C) ->
ct_helper:makeup_cfg([
ct_helper:test_case_name(init),
ct_payment_system:setup()
], C).
-spec end_per_suite(config()) -> _.
end_per_suite(C) ->
ok = ct_payment_system:shutdown(C).
%%
-spec init_per_group(group_name(), config()) -> config().
init_per_group(_, C) ->
C.
-spec end_per_group(group_name(), config()) -> _.
end_per_group(_, _) ->
ok.
%%
-spec init_per_testcase(test_case_name(), config()) -> config().
init_per_testcase(Name, C) ->
C1 = ct_helper:makeup_cfg([ct_helper:test_case_name(Name), ct_helper:woody_ctx()], C),
ok = ct_helper:set_context(C1),
C1.
-spec end_per_testcase(test_case_name(), config()) -> _.
end_per_testcase(_Name, _C) ->
ok = ct_helper:unset_context().
%% Default group test cases
-spec create_source_ok_test(config()) -> test_return().
create_source_ok_test(C) ->
Party = create_party(C),
IID = create_person_identity(Party, C),
_SourceID = create_source(IID, C),
ok.
-spec create_source_identity_notfound_fail_test(config()) -> test_return().
create_source_identity_notfound_fail_test(_C) ->
IID = <<"BadIdentityID">>,
SrcResource = #{type => internal, details => <<"Infinite source of cash">>},
Params = #{
id => genlib:unique(),
identity => IID,
name => <<"XSource">>,
currency => <<"RUB">>,
resource => SrcResource
},
CreateResult = ff_source_machine:create(Params, ff_entity_context:new()),
?assertEqual({error, {identity, notfound}}, CreateResult).
-spec create_source_currency_notfound_fail_test(config()) -> test_return().
create_source_currency_notfound_fail_test(C) ->
Party = create_party(C),
IID = create_person_identity(Party, C),
SrcResource = #{type => internal, details => <<"Infinite source of cash">>},
Params = #{
id => genlib:unique(),
identity => IID,
name => <<"XSource">>,
currency => <<"BadUnknownCurrency">>,
resource => SrcResource
},
CreateResult = ff_source_machine:create(Params, ff_entity_context:new()),
?assertEqual({error, {currency, notfound}}, CreateResult).
-spec get_source_ok_test(config()) -> test_return().
get_source_ok_test(C) ->
Party = create_party(C),
IID = create_person_identity(Party, C),
SourceID = create_source(IID, C),
{ok, SourceMachine} = ff_source_machine:get(SourceID),
?assertMatch(
#{
account := #{currency := <<"RUB">>},
name := <<"XSource">>,
resource := #{details := <<"Infinite source of cash">>, type := internal},
status := authorized
},
ff_source_machine:source(SourceMachine)
).
-spec get_source_notfound_fail_test(config()) -> test_return().
get_source_notfound_fail_test(_C) ->
?assertEqual({error, notfound}, ff_source_machine:get(<<"BadID">>)).
%% Common functions
create_party(_C) ->
ID = genlib:bsuuid(),
_ = ff_party:create(ID),
ID.
create_person_identity(Party, C) ->
create_person_identity(Party, C, <<"good-one">>).
create_person_identity(Party, C, ProviderID) ->
create_identity(Party, ProviderID, <<"person">>, C).
create_identity(Party, ProviderID, ClassID, C) ->
create_identity(Party, <<"Identity Name">>, ProviderID, ClassID, C).
create_identity(Party, Name, ProviderID, ClassID, _C) ->
ID = genlib:unique(),
ok = ff_identity_machine:create(
#{id => ID, name => Name, party => Party, provider => ProviderID, class => ClassID},
#{<<"com.rbkmoney.wapi">> => #{<<"name">> => Name}}
),
ID.
create_source(IID, _C) ->
SrcResource = #{type => internal, details => <<"Infinite source of cash">>},
SrcID = create_source(IID, <<"XSource">>, <<"RUB">>, SrcResource),
authorized = ct_helper:await(
authorized,
fun () ->
{ok, SrcM} = ff_source_machine:get(SrcID),
Source = ff_source_machine:source(SrcM),
ff_source:status(Source)
end
),
SrcID.
create_source(IdentityID, Name, Currency, Resource) ->
ID = genlib:unique(),
ok = ff_source_machine:create(
#{id => ID, identity => IdentityID, name => Name, currency => Currency, resource => Resource},
ff_entity_context:new()
),
ID.

View File

@ -397,9 +397,8 @@ get_wallet_balance(ID) ->
get_account_balance(ff_wallet:account(ff_wallet_machine:wallet(Machine))).
get_destination_balance(ID) ->
{ok, Machine} = ff_destination_machine:get(ID),
Destination = ff_destination_machine:destination(Machine),
get_account_balance(ff_destination:account(Destination)).
{ok, Machine} = ff_destination:get_machine(ID),
get_account_balance(ff_destination:account(ff_destination:get(Machine))).
get_account_balance(Account) ->
{ok, {Amounts, Currency}} = ff_transaction:balance(
@ -408,21 +407,20 @@ get_account_balance(Account) ->
),
{ff_indef:current(Amounts), ff_indef:to_range(Amounts), Currency}.
create_source(IdentityID, Name, Currency, Resource) ->
create_instrument(Type, IdentityID, Name, Currency, Resource, C) ->
ID = genlib:unique(),
ok = ff_source_machine:create(
ok = create_instrument(
Type,
#{id => ID, identity => IdentityID, name => Name, currency => Currency, resource => Resource},
ff_entity_context:new()
ff_entity_context:new(),
C
),
ID.
create_destination(IdentityID, Name, Currency, Resource) ->
ID = genlib:unique(),
ok = ff_destination_machine:create(
#{id => ID, identity => IdentityID, name => Name, currency => Currency, resource => Resource},
ff_entity_context:new()
),
ID.
create_instrument(destination, Params, Ctx, _C) ->
ff_destination:create(Params, Ctx);
create_instrument(source, Params, Ctx, _C) ->
ff_source:create(Params, Ctx).
generate_id() ->
genlib:to_binary(genlib_time:ticks()).
@ -436,15 +434,15 @@ call_admin(Fun, Args) ->
}),
ff_woody_client:call(Client, Request).
create_source(IID, _C) ->
create_source(IID, C) ->
% Create source
SrcResource = #{type => internal, details => <<"Infinite source of cash">>},
SrcID = create_source(IID, <<"XSource">>, <<"RUB">>, SrcResource),
SrcID = create_instrument(source, IID, <<"XSource">>, <<"RUB">>, SrcResource, C),
authorized = ct_helper:await(
authorized,
fun () ->
{ok, SrcM} = ff_source_machine:get(SrcID),
Source = ff_source_machine:source(SrcM),
ff_source:status(Source)
{ok, SrcM} = ff_source:get_machine(SrcID),
ff_source:status(ff_source:get(SrcM))
end
),
SrcID.
@ -467,29 +465,27 @@ process_deposit(SrcID, WalID) ->
create_destination(IID, C) ->
DestResource = {bank_card, #{bank_card => ct_cardstore:bank_card(<<"4150399999000900">>, {12, 2025}, C)}},
DestID = create_destination(IID, <<"XDesination">>, <<"RUB">>, DestResource),
DestID = create_instrument(destination, IID, <<"XDesination">>, <<"RUB">>, DestResource, C),
authorized = ct_helper:await(
authorized,
fun () ->
{ok, DestM} = ff_destination_machine:get(DestID),
Destination = ff_destination_machine:destination(DestM),
ff_destination:status(Destination)
{ok, DestM} = ff_destination:get_machine(DestID),
ff_destination:status(ff_destination:get(DestM))
end
),
DestID.
create_crypto_destination(IID, _C) ->
create_crypto_destination(IID, C) ->
Resource = {crypto_wallet, #{crypto_wallet => #{
id => <<"a30e277c07400c9940628828949efd48">>,
currency => {litecoin, #{}}
}}},
DestID = create_destination(IID, <<"CryptoDestination">>, <<"RUB">>, Resource),
DestID = create_instrument(destination, IID, <<"CryptoDestination">>, <<"RUB">>, Resource, C),
authorized = ct_helper:await(
authorized,
fun () ->
{ok, DestM} = ff_destination_machine:get(DestID),
Destination = ff_destination_machine:destination(DestM),
ff_destination:status(Destination)
{ok, DestM} = ff_destination:get_machine(DestID),
ff_destination:status(ff_destination:get(DestM))
end
),
DestID.

View File

@ -2,7 +2,6 @@
-include_lib("stdlib/include/assert.hrl").
-include_lib("damsel/include/dmsl_domain_thrift.hrl").
-include_lib("fistful_proto/include/ff_proto_withdrawal_session_thrift.hrl").
-include_lib("shumpune_proto/include/shumpune_shumpune_thrift.hrl").
%% Common test API
@ -18,7 +17,6 @@
%% Tests
-export([session_fail_test/1]).
-export([session_repair_test/1]).
-export([quote_fail_test/1]).
-export([route_not_found_fail_test/1]).
-export([provider_operations_forbidden_fail_test/1]).
@ -72,7 +70,6 @@ groups() ->
[
{default, [parallel], [
session_fail_test,
session_repair_test,
quote_fail_test,
route_not_found_fail_test,
provider_operations_forbidden_fail_test,
@ -585,43 +582,6 @@ provider_callback_test(C) ->
% Check that session is still alive
?assertEqual({ok, #{payload => CallbackPayload}}, call_process_callback(Callback)).
-spec session_repair_test(config()) -> test_return().
session_repair_test(C) ->
Currency = <<"RUB">>,
Cash = {700700, Currency},
#{
wallet_id := WalletID,
destination_id := DestinationID
} = prepare_standard_environment(Cash, C),
WithdrawalID = generate_id(),
WithdrawalParams = #{
id => WithdrawalID,
destination_id => DestinationID,
wallet_id => WalletID,
body => Cash,
quote => #{
cash_from => {700700, <<"RUB">>},
cash_to => {700700, <<"RUB">>},
created_at => <<"2016-03-22T06:12:27Z">>,
expires_on => <<"2016-03-22T06:12:27Z">>,
route => ff_withdrawal_routing:make_route(11, 1),
quote_data => #{<<"test">> => <<"fatal">>}
}
},
Callback = #{
tag => <<"cb_", WithdrawalID/binary>>,
payload => <<"super_secret">>
},
ok = ff_withdrawal_machine:create(WithdrawalParams, ff_entity_context:new()),
?assertEqual(pending, await_session_processing_status(WithdrawalID, pending)),
SessionID = get_session_id(WithdrawalID),
?assertEqual(<<"callback_processing">>, await_session_adapter_state(SessionID, <<"callback_processing">>)),
?assertError({failed, _, _}, call_process_callback(Callback)),
timer:sleep(3000),
?assertEqual(pending, await_session_processing_status(WithdrawalID, pending)),
ok = repair_withdrawal_session(WithdrawalID),
?assertEqual(succeeded, await_final_withdrawal_status(WithdrawalID)).
%% Utils
prepare_standard_environment(WithdrawalCash, C) ->
@ -766,13 +726,12 @@ create_destination(IID, Currency, Token, C) ->
end,
Resource = {bank_card, #{bank_card => NewStoreResource}},
Params = #{id => ID, identity => IID, name => <<"XDesination">>, currency => Currency, resource => Resource},
ok = ff_destination_machine:create(Params, ff_entity_context:new()),
ok = ff_destination:create(Params, ff_entity_context:new()),
authorized = ct_helper:await(
authorized,
fun () ->
{ok, Machine} = ff_destination_machine:get(ID),
Destination = ff_destination_machine:destination(Machine),
ff_destination:status(Destination)
{ok, Machine} = ff_destination:get_machine(ID),
ff_destination:status(ff_destination:get(Machine))
end
),
ID.
@ -784,13 +743,12 @@ create_crypto_destination(IID, _C) ->
currency => {litecoin, #{}}
}}},
Params = #{id => ID, identity => IID, name => <<"CryptoDestination">>, currency => <<"RUB">>, resource => Resource},
ok = ff_destination_machine:create(Params, ff_entity_context:new()),
ok = ff_destination:create(Params, ff_entity_context:new()),
authorized = ct_helper:await(
authorized,
fun () ->
{ok, Machine} = ff_destination_machine:get(ID),
Destination = ff_destination_machine:destination(Machine),
ff_destination:status(Destination)
{ok, DestM} = ff_destination:get_machine(ID),
ff_destination:status(ff_destination:get(DestM))
end
),
ID.
@ -837,24 +795,3 @@ make_dummy_party_change(PartyID) ->
call_process_callback(Callback) ->
ff_withdrawal_session_machine:process_callback(Callback).
repair_withdrawal_session(WithdrawalID) ->
SessionID = get_session_id(WithdrawalID),
{ok, ok} = call_session_repair(SessionID, {set_session_result, #wthd_session_SetResultRepair{
result = {success, #wthd_session_SessionResultSuccess{
trx_info = #'TransactionInfo'{
id = SessionID,
extra = #{}
}
}}
}}),
ok.
call_session_repair(SessionID, Scenario) ->
Service = {ff_proto_withdrawal_session_thrift, 'Repairer'},
Request = {Service, 'Repair', [SessionID, Scenario]},
Client = ff_woody_client:new(#{
url => <<"http://localhost:8022/v1/repair/withdrawal/session">>,
event_handler => scoper_woody_event_handler
}),
ff_woody_client:call(Client, Request).

View File

@ -428,9 +428,8 @@ get_wallet_balance(ID) ->
get_account_balance(ff_wallet:account(ff_wallet_machine:wallet(Machine))).
get_destination_balance(ID) ->
{ok, Machine} = ff_destination_machine:get(ID),
Destination = ff_destination_machine:destination(Machine),
get_account_balance(ff_destination:account(Destination)).
{ok, Machine} = ff_destination:get_machine(ID),
get_account_balance(ff_destination:account(ff_destination:get(Machine))).
get_account_balance(Account) ->
{ok, {Amounts, Currency}} = ff_transaction:balance(
@ -446,13 +445,12 @@ create_destination(IID, C) ->
ID = generate_id(),
Resource = {bank_card, #{bank_card => ct_cardstore:bank_card(<<"4150399999000900">>, {12, 2025}, C)}},
Params = #{id => ID, identity => IID, name => <<"XDesination">>, currency => <<"RUB">>, resource => Resource},
ok = ff_destination_machine:create(Params, ff_entity_context:new()),
ok = ff_destination:create(Params, ff_entity_context:new()),
authorized = ct_helper:await(
authorized,
fun () ->
{ok, Machine} = ff_destination_machine:get(ID),
Destination = ff_destination_machine:destination(Machine),
ff_destination:status(Destination)
{ok, Machine} = ff_destination:get_machine(ID),
ff_destination:status(ff_destination:get(Machine))
end
),
ID.

View File

@ -338,13 +338,12 @@ create_destination(IID, Currency, C) ->
StoreSource = ct_cardstore:bank_card(<<"4150399999000900">>, {12, Y + 1}, C),
Resource = {bank_card, #{bank_card => StoreSource}},
Params = #{id => ID, identity => IID, name => <<"XDesination">>, currency => Currency, resource => Resource},
ok = ff_destination_machine:create(Params, ff_entity_context:new()),
ok = ff_destination:create(Params, ff_entity_context:new()),
authorized = ct_helper:await(
authorized,
fun () ->
{ok, Machine} = ff_destination_machine:get(ID),
Destination = ff_destination_machine:destination(Machine),
ff_destination:status(Destination)
{ok, Machine} = ff_destination:get_machine(ID),
ff_destination:status(ff_destination:get(Machine))
end
),
ID.

View File

@ -82,7 +82,6 @@
-type create_error() ::
{provider, notfound} |
{party, notfound} |
{identity_class, notfound} |
ff_party:inaccessibility() |
invalid.

View File

@ -180,9 +180,7 @@ is_accessible(ID) ->
#domain_Party{suspension = {suspended, _}} ->
{error, {inaccessible, suspended}};
#domain_Party{} ->
{ok, accessible};
#payproc_PartyNotFound{} ->
{error, notfound}
{ok, accessible}
end.
-spec get_revision(id()) ->
@ -442,10 +440,8 @@ do_get_party(ID) ->
case Result of
{ok, Party} ->
Party;
{error, #payproc_PartyNotFound{} = Reason} ->
Reason;
{error, Unexpected} ->
error(Unexpected)
{error, Reason} ->
error(Reason)
end.
do_get_contract(ID, ContractID) ->

View File

@ -294,9 +294,8 @@ maybe_migrate_account({wallet, WalletID}) ->
{ok, Machine} = ff_wallet_machine:get(WalletID),
ff_wallet:account(ff_wallet_machine:wallet(Machine));
maybe_migrate_account({destination, DestinationID}) ->
{ok, Machine} = ff_destination_machine:get(DestinationID),
Destination = ff_destination_machine:destination(Machine),
ff_destination:account(Destination);
{ok, Machine} = ff_destination:get_machine(DestinationID),
ff_destination:account(ff_destination:get(Machine));
maybe_migrate_account(Account) when is_map(Account) ->
Account.

View File

@ -129,10 +129,9 @@ validate_result(Mod, #{history := History} = Machine, #{events := NewEvents}) ->
{ok, valid}
catch
error:Error:Stack ->
Stacktrace = genlib_format:format_stacktrace(Stack),
logger:warning("Invalid repair result: ~p, Stack: ~p", [Error, Stacktrace], #{
logger:warning("Invalid repair result: ~p", [Error], #{
error => genlib:format(Error),
stacktrace => Stacktrace
stacktrace => genlib_format:format_stacktrace(Stack)
}),
{error, unexpected_failure}
end.

View File

@ -55,10 +55,6 @@
-type status() :: {success, transaction_info()} | {failure, failure()}.
-type timer() :: {deadline, binary()} | {timeout, integer()}.
%%
-define(DUMMY_QUOTE_ERROR_FATAL, {obj, #{{str, <<"test">>} => {str, <<"fatal">>}}}).
%%
%% API
%%
@ -106,10 +102,6 @@ get_quote(_Quote, _Options) ->
transaction_info => transaction_info()
}} when
CallbackTag :: binary().
handle_callback(_Callback, #{quote := #wthadpt_Quote{quote_data = QuoteData}}, _State, _Options) when
QuoteData =:= ?DUMMY_QUOTE_ERROR_FATAL
->
erlang:error(spanish_inquisition);
handle_callback(#{payload := Payload}, _Withdrawal, _State, _Options) ->
{ok, #{
intent => {finish, success},

View File

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

View File

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

View File

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

View File

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

View File

@ -155,17 +155,17 @@ get_identity(IdentityId, Context) ->
{identity_class, notfound} |
{inaccessible, ff_party:inaccessibility()} |
{email, notfound} |
{external_id_conflict, id(), external_id()} |
{party, notfound}
{external_id_conflict, id(), external_id()}
).
create_identity(Params, Context) ->
IdentityParams = from_swag(identity_params, Params),
CreateFun = fun(ID, EntityCtx) ->
CreateIdentity = fun(ID, EntityCtx) ->
ff_identity_machine:create(
maps:merge(IdentityParams#{id => ID}, #{party => wapi_handler_utils:get_owner(Context)}),
add_meta_to_ctx([<<"name">>], Params, EntityCtx)
)
end,
CreateFun = fun(ID, EntityCtx) -> with_party(Context, fun() -> CreateIdentity(ID, EntityCtx) end) end,
do(fun() -> unwrap(create_entity(identity, Params, CreateFun, Context)) end).
-spec get_identity_challenges(id(), [binary()], ctx()) -> result(map(),
@ -368,7 +368,7 @@ create_destination(Params = #{<<"identity">> := IdenityId}, Context) ->
_ = check_resource(identity, IdenityId, Context),
DestinationParams = from_swag(destination_params, Params),
Resource = unwrap(construct_resource(maps:get(resource, DestinationParams))),
unwrap(ff_destination_machine:create(
unwrap(ff_destination:create(
DestinationParams#{id => ID, resource => Resource},
add_meta_to_ctx([], Params, EntityCtx)
))
@ -1231,7 +1231,7 @@ get_state(Resource, Id, Context) ->
do_get_state(identity, Id) -> ff_identity_machine:get(Id);
do_get_state(wallet, Id) -> ff_wallet_machine:get(Id);
do_get_state(destination, Id) -> ff_destination_machine:get(Id);
do_get_state(destination, Id) -> ff_destination:get_machine(Id);
do_get_state(withdrawal, Id) -> ff_withdrawal_machine:get(Id);
do_get_state(p2p_transfer, Id) -> p2p_transfer_machine:get(Id);
do_get_state(p2p_template, Id) -> p2p_template_machine:get(Id);
@ -1288,6 +1288,27 @@ handle_create_entity_result(Result, Type, ID, Context) when
handle_create_entity_result({error, E}, _Type, _ID, _Context) ->
throw(E).
with_party(Context, Fun) ->
try Fun()
catch
error:#'payproc_PartyNotFound'{} ->
ok = create_party(Context),
Fun()
end.
create_party(Context) ->
_ = ff_party:create(
wapi_handler_utils:get_owner(Context),
#{email => unwrap(get_email(wapi_handler_utils:get_auth_context(Context)))}
),
ok.
get_email(AuthContext) ->
case uac_authorizer_jwt:get_claim(<<"email">>, AuthContext, undefined) of
undefined -> {error, {email, notfound}};
Email -> {ok, Email}
end.
-spec not_implemented() -> no_return().
not_implemented() ->
wapi_handler_utils:throw_not_implemented().
@ -1908,7 +1929,7 @@ to_swag(wallet_account, {OwnAmount, AvailableAmount, Currency}) ->
}
};
to_swag(destination, State) ->
Destination = ff_destination_machine:destination(State),
Destination = ff_destination:get(State),
to_swag(map, maps:merge(
#{
<<"id">> => ff_destination:id(Destination),

View File

@ -110,10 +110,8 @@ process_request('CreateIdentity', #{'Identity' := Params}, Context, Opts) ->
case wapi_wallet_ff_backend:create_identity(Params, Context) of
{ok, Identity = #{<<"id">> := IdentityId}} ->
wapi_handler_utils:reply_ok(201, Identity, get_location('GetIdentity', [IdentityId], Opts));
{error, {inaccessible, _}} ->
{error, {inaccessible, _}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Party inaccessible">>));
{error, {party, notfound}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Party does not exist">>));
{error, {provider, notfound}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such provider">>));
{error, {identity_class, notfound}} ->

View File

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

View File

@ -275,9 +275,8 @@ wait_for_destination_authorized(DestID) ->
authorized = ct_helper:await(
authorized,
fun () ->
{ok, DestM} = ff_destination_machine:get(DestID),
Destination = ff_destination_machine:destination(DestM),
ff_destination:status(Destination)
{ok, DestM} = ff_destination:get_machine(DestID),
ff_destination:status(ff_destination:get(DestM))
end
).

View File

@ -30,7 +30,6 @@
-export([check_withdrawal_limit_test/1]).
-export([check_withdrawal_limit_exceeded_test/1]).
-export([identity_providers_mismatch_test/1]).
-export([lazy_party_creation_forbidden_test/1]).
-export([consume_eventsinks/1]).
@ -73,8 +72,7 @@ groups() ->
not_allowed_currency_test,
check_withdrawal_limit_test,
check_withdrawal_limit_exceeded_test,
identity_providers_mismatch_test,
lazy_party_creation_forbidden_test
identity_providers_mismatch_test
]},
{eventsink, [], [
consume_eventsinks
@ -136,31 +134,12 @@ end_per_group(_, _) ->
init_per_testcase(Name, C) ->
C1 = ct_helper:makeup_cfg([ct_helper:test_case_name(Name), ct_helper:woody_ctx()], C),
ok = ct_helper:set_context(C1),
case Name of
woody_retry_test ->
Save = application:get_env(wapi_woody_client, service_urls, undefined),
ok = application:set_env(
wapi_woody_client,
service_urls,
Save#{fistful_stat => "http://spanish.inquision/fistful_stat"}
),
lists:keystore(service_urls, 1, C1, {service_urls, Save});
_Other ->
C1
end.
C1.
-spec end_per_testcase(test_case_name(), config()) -> _.
end_per_testcase(_Name, C) ->
ok = ct_helper:unset_context(),
case lists:keysearch(service_urls, 1, C) of
{value, {_, undefined}} ->
application:unset_env(wapi_woody_client, service_urls);
{value, {_, Save}} ->
application:set_env(wapi_woody_client, service_urls, Save);
_ ->
ok
end.
end_per_testcase(_Name, _C) ->
ok = ct_helper:unset_context().
-define(ID_PROVIDER, <<"good-one">>).
-define(ID_PROVIDER2, <<"good-two">>).
@ -404,24 +383,6 @@ identity_providers_mismatch_test(C) ->
})},
cfg(context, C)
).
-spec lazy_party_creation_forbidden_test(config()) -> test_return().
lazy_party_creation_forbidden_test(_) ->
Name = <<"Keyn Fawkes">>,
Provider = ?ID_PROVIDER,
Class = ?ID_CLASS,
{Context, _} = create_context_for_group(group_or_smth, <<"Nonexistent party">>),
{error, {422, #{<<"message">> := <<"Party does not exist">>}}} = call_api(
fun swag_client_wallet_identities_api:create_identity/3,
#{body => #{
<<"name">> => Name,
<<"provider">> => Provider,
<<"class">> => Class,
<<"metadata">> => #{
?STRING => ?STRING
}
}},
Context
).
-spec unknown_withdrawal_test(config()) -> test_return().
@ -577,6 +538,12 @@ quote_withdrawal_test(C) ->
ok = check_withdrawal(WalletID, DestID, WithdrawalID, C).
woody_retry_test(C) ->
Urls = application:get_env(wapi_woody_client, service_urls, #{}),
ok = application:set_env(
wapi_woody_client,
service_urls,
Urls#{fistful_stat => "http://spanish.inquision/fistful_stat"}
),
Params = #{
identityID => <<"12332">>,
currencyID => <<"RUB">>,
@ -595,7 +562,8 @@ woody_retry_test(C) ->
end,
T2 = erlang:monotonic_time(),
Time = erlang:convert_time_unit(T2 - T1, native, micro_seconds),
?assert(Time > 3000000).
?assert(Time > 3000000),
ok = application:set_env(wapi_woody_client, service_urls, Urls).
-spec get_wallet_by_external_id(config()) ->
test_return().
@ -789,9 +757,8 @@ await_destination(DestID) ->
authorized = ct_helper:await(
authorized,
fun () ->
{ok, DestM} = ff_destination_machine:get(DestID),
Destination = ff_destination_machine:destination(DestM),
ff_destination:status(Destination)
{ok, DestM} = ff_destination:get_machine(DestID),
ff_destination:status(ff_destination:get(DestM))
end
).

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -176,7 +176,7 @@
{vm_args , "./config/vm.args"},
{dev_mode , false},
{include_src , false},
{include_erts , false},
{include_erts , true},
{extended_start_script , true},
%% wapi
{overlay, [
@ -189,11 +189,6 @@
]},
{test, [
{deps, [
{meck,
"0.9.0"
}
]},
{cover_enabled, true},
{cover_excl_apps, [
ff_cth,

View File

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