FF-2: add deposit api and refactor ff_withdrawal (#14)

* FF-2: refactor ff_withdrawal
- introduce ff_transfer_machine and new ff_transfer as an underlying process
  for any withdrawal, deposit and transfer between wallets.
- rename apps/ff_withdraw -> apps/ff_transfer and make corresponding updates
  including sys.config.
- rename fistful/src/ff_transfer.erl -> fistful/src/ff_postings_transfer.erl
* Introduce ff_instrument as an underlying abstruction for ff_destination and ff_source.
* Add ff_deposit
* Add ff_source
* Add FistfulAdmin thrift service
* Add transfer migration code
This commit is contained in:
Anton Belyaev 2018-10-05 14:19:46 +03:00 committed by GitHub
parent ddf5630f5b
commit 9f27ef35ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1536 additions and 717 deletions

View File

@ -69,7 +69,7 @@ start_app(lager = AppName) ->
{suppress_application_start_stop, false},
{suppress_supervisor_start_stop, false},
{handlers, [
{lager_common_test_backend, debug}
{lager_common_test_backend, [debug, {lager_logstash_formatter, []}]}
]}
]), #{}};

View File

@ -13,7 +13,8 @@
lager,
scoper,
fistful,
ff_withdraw,
ff_transfer,
fistful_proto,
wapi
]},
{env, []},

View File

@ -60,7 +60,7 @@ init([]) ->
contruct_backend_childspec('ff/identity' , ff_identity_machine),
contruct_backend_childspec('ff/wallet_v2' , ff_wallet_machine),
contruct_backend_childspec('ff/destination_v2' , ff_destination_machine),
contruct_backend_childspec('ff/withdrawal_v2' , ff_withdrawal_machine),
contruct_backend_childspec('ff/withdrawal_v2' , ff_transfer_machine),
contruct_backend_childspec('ff/withdrawal/session_v2' , ff_withdrawal_session_machine)
]),
ok = application:set_env(fistful, backends, maps:from_list(Backends)),
@ -80,15 +80,18 @@ init([]) ->
port => genlib_app:env(?MODULE, port, 8022),
handlers => [],
event_handler => scoper_woody_event_handler,
additional_routes => machinery_mg_backend:get_routes(
Handlers,
maps:merge(
genlib_app:env(?MODULE, route_opts, #{}),
#{
event_handler => scoper_woody_event_handler
}
)
) ++ [erl_health_handle:get_route(HealthCheckers)]
additional_routes =>
machinery_mg_backend:get_routes(
Handlers,
maps:merge(
genlib_app:env(?MODULE, route_opts, #{}),
#{
event_handler => scoper_woody_event_handler
}
)
) ++
get_admin_routes() ++
[erl_health_handle:get_route(HealthCheckers)]
}
)
)
@ -117,3 +120,13 @@ get_service_client(ServiceID) ->
#{} ->
error({'woody service undefined', ServiceID})
end.
get_admin_routes() ->
Opts = genlib_app:env(?MODULE, admin, #{}),
Path = maps:get(path, Opts, <<"/v1/admin">>),
Limits = genlib_map:get(handler_limits, Opts),
woody_server_thrift_http_handler:get_routes(genlib_map:compact(#{
handlers => [{Path, {{ff_proto_fistful_thrift, 'FistfulAdmin'}, {ff_server_handler, []}}}],
event_handler => scoper_woody_event_handler,
handler_limits => Limits
})).

View File

@ -0,0 +1,144 @@
-module(ff_server_handler).
-behaviour(woody_server_thrift_handler).
-include_lib("fistful_proto/include/ff_proto_fistful_thrift.hrl").
%% woody_server_thrift_handler callbacks
-export([handle_function/4]).
%%
%% woody_server_thrift_handler callbacks
%%
-spec handle_function(woody:func(), woody:args(), woody_context:ctx(), woody:options()) ->
{ok, woody:result()} | no_return().
handle_function(Func, Args, Context, Opts) ->
scoper:scope(fistful, #{function => Func},
fun() ->
ok = ff_woody_ctx:set(Context),
try
handle_function_(Func, Args, Context, Opts)
after
ff_woody_ctx:unset()
end
end
).
%%
%% Internals
%%
handle_function_('CreateSource', [Params], Context, Opts) ->
SourceID = next_id('source'),
case ff_source:create(SourceID, #{
identity => Params#fistful_SourceParams.identity_id,
name => Params#fistful_SourceParams.name,
currency => decode(currency, Params#fistful_SourceParams.currency),
resource => decode({source, resource}, Params#fistful_SourceParams.resource)
}, decode(context, Params#fistful_SourceParams.context))
of
ok ->
handle_function_('GetSource', [SourceID], Context, Opts);
{error, {identity, notfound}} ->
woody_error:raise(business, #fistful_IdentityNotFound{});
{error, {currency, notfound}} ->
woody_error:raise(business, #fistful_CurrencyNotFound{});
{error, Error} ->
woody_error:raise(system, {internal, result_unexpected, woody_error:format_details(Error)})
end;
handle_function_('GetSource', [ID], _Context, _Opts) ->
case ff_source:get_machine(ID) of
{ok, Machine} ->
{ok, encode(source, {ID, Machine})};
{error, notfound} ->
woody_error:raise(business, #fistful_SourceNotFound{})
end;
handle_function_('CreateDeposit', [Params], Context, Opts) ->
DepositID = next_id('deposit'),
case ff_deposit:create(DepositID, #{
source => Params#fistful_DepositParams.source,
destination => Params#fistful_DepositParams.destination,
body => decode({deposit, body}, Params#fistful_DepositParams.body)
}, decode(context, Params#fistful_DepositParams.context))
of
ok ->
handle_function_('GetDeposit', [DepositID], Context, Opts);
{error, {source, notfound}} ->
woody_error:raise(business, #fistful_SourceNotFound{});
{error, {source, unauthorized}} ->
woody_error:raise(business, #fistful_SourceUnauthorized{});
{error, {destination, notfound}} ->
woody_error:raise(business, #fistful_DestinationNotFound{});
{error, Error} ->
woody_error:raise(system, {internal, result_unexpected, woody_error:format_details(Error)})
end;
handle_function_('GetDeposit', [ID], _Context, _Opts) ->
case ff_deposit:get_machine(ID) of
{ok, Machine} ->
{ok, encode(deposit, {ID, Machine})};
{error, notfound} ->
woody_error:raise(business, #fistful_DepositNotFound{})
end.
decode({source, resource}, #fistful_SourceResource{details = Details}) ->
genlib_map:compact(#{
type => internal,
details => Details
});
decode({deposit, body}, #fistful_DepositBody{amount = Amount, currency = Currency}) ->
{Amount, decode(currency, Currency)};
decode(currency, #fistful_CurrencyRef{symbolic_code = V}) ->
V;
decode(context, Context) ->
Context.
encode(source, {ID, Machine}) ->
Source = ff_source:get(Machine),
#fistful_Source{
id = ID,
name = ff_source:name(Source),
identity_id = ff_source:identity(Source),
currency = encode(currency, ff_source:currency(Source)),
resource = encode({source, resource}, ff_source:resource(Source)),
status = encode({source, status}, ff_source:status(Source)),
context = encode(context, ff_machine:ctx(Machine))
};
encode({source, status}, Status) ->
Status;
encode({source, resource}, Resource) ->
#fistful_SourceResource{
details = genlib_map:get(details, Resource)
};
encode(deposit, {ID, Machine}) ->
Deposit = ff_deposit:get(Machine),
#fistful_Deposit{
id = ID,
source = ff_deposit:source(Deposit),
destination = ff_deposit:destination(Deposit),
body = encode({deposit, body}, ff_deposit:body(Deposit)),
status = encode({deposit, status}, ff_deposit:status(Deposit)),
context = encode(context, ff_machine:ctx(Machine))
};
encode({deposit, body}, {Amount, Currency}) ->
#fistful_DepositBody{
amount = Amount,
currency = encode(currency, Currency)
};
encode({deposit, status}, pending) ->
{pending, #fistful_DepositStatusPending{}};
encode({deposit, status}, succeeded) ->
{succeeded, #fistful_DepositStatusSucceeded{}};
encode({deposit, status}, {failed, Details}) ->
{failed, #fistful_DepositStatusFailed{details = woody_error:format_details(Details)}};
encode(currency, V) ->
#fistful_CurrencyRef{symbolic_code = V};
encode(context, #{}) ->
undefined;
encode(context, Ctx) ->
Ctx.
next_id(Type) ->
NS = 'ff/sequence',
erlang:integer_to_binary(
ff_sequence:next(NS, ff_string:join($/, [Type, id]), fistful:backend(NS))
).

View File

@ -0,0 +1,134 @@
%%%
%%% Deposit
%%%
-module(ff_deposit).
-type id() :: ff_transfer_machine:id().
-type source() :: ff_source:id(_).
-type wallet() :: ff_wallet:id(_).
-type deposit() :: ff_transfer:transfer(transfer_params()).
-type transfer_params() :: #{
source := source(),
destination := wallet()
}.
-type machine() :: ff_transfer_machine:st(transfer_params()).
-type events() :: ff_transfer_machine:events().
-export_type([deposit/0]).
-export_type([machine/0]).
-export_type([transfer_params/0]).
-export_type([events/0]).
%% ff_transfer_machine behaviour
-behaviour(ff_transfer_machine).
-export([process_transfer/1]).
%% Accessors
-export([source/1]).
-export([destination/1]).
-export([id/1]).
-export([source_acc/1]).
-export([destination_acc/1]).
-export([body/1]).
-export([status/1]).
%% API
-export([create/3]).
-export([get/1]).
-export([get_machine/1]).
-export([events/2]).
%% Pipeline
-import(ff_pipeline, [do/1, unwrap/1, unwrap/2, valid/2]).
%% Accessors
-spec source(deposit()) -> source().
-spec destination(deposit()) -> wallet().
-spec id(deposit()) -> ff_transfer:id().
-spec source_acc(deposit()) -> ff_account:account().
-spec destination_acc(deposit()) -> ff_account:account().
-spec body(deposit()) -> ff_transfer:body().
-spec status(deposit()) -> ff_transfer:status().
source(T) -> maps:get(source, ff_transfer:params(T)).
destination(T) -> maps:get(destination, ff_transfer:params(T)).
id(T) -> ff_transfer:id(T).
source_acc(T) -> ff_transfer:source(T).
destination_acc(T) -> ff_transfer:destination(T).
body(T) -> ff_transfer:body(T).
status(T) -> ff_transfer:status(T).
%%
-define(NS, 'ff/deposit_v1').
-type ctx() :: ff_ctx:ctx().
-type params() :: #{
source := ff_source:id(),
destination := ff_wallet_machine:id(),
body := ff_transaction:body()
}.
-spec create(id(), params(), ctx()) ->
ok |
{error,
{source, notfound | unauthorized} |
{destination, notfound} |
{provider, notfound} |
exists |
_TransferError
}.
create(ID, #{source := SourceID, destination := DestinationID, body := Body}, Ctx) ->
do(fun() ->
Source = ff_source:get(unwrap(source, ff_source:get_machine(SourceID))),
Destination = ff_wallet_machine:wallet(unwrap(destination, ff_wallet_machine:get(DestinationID))),
ok = unwrap(source, valid(authorized, ff_source:status(Source))),
Params = #{
handler => ?MODULE,
source => ff_source:account(Source),
destination => ff_wallet:account(Destination),
body => Body,
params => #{
source => SourceID,
destination => DestinationID
}
},
unwrap(ff_transfer_machine:create(?NS, ID, Params, Ctx))
end).
-spec get(machine()) ->
deposit().
get(St) ->
ff_transfer_machine:transfer(St).
-spec get_machine(id()) ->
{ok, machine()} |
{error, notfound}.
get_machine(ID) ->
ff_transfer_machine:get(?NS, ID).
-spec events(id(), machinery:range()) ->
{ok, events()} |
{error, notfound}.
events(ID, Range) ->
ff_transfer_machine:events(?NS, ID, Range).
%% ff_transfer_machine behaviour
-spec process_transfer(deposit()) ->
{ok, [ff_transfer_machine:event(ff_transfer:event())]} |
{error, _Reason}.
process_transfer(#{status := pending, p_transfer := #{status := prepared}}) ->
{ok, [{status_changed, succeeded}]};
process_transfer(Transfer) ->
ff_transfer:process_transfer(Transfer).

View File

@ -0,0 +1,103 @@
%%%
%%% Destination
%%%
%%% TODOs
%%%
%%% - We must consider withdrawal provider terms ensure that the provided
%%% Resource is ok to withdraw to.
%%%
-module(ff_destination).
-type ctx() :: ff_ctx:ctx().
-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() :: ff_identity:status().
-type resource() ::
{bank_card, resource_bank_card()}.
-type resource_bank_card() :: #{
token := binary(),
payment_system => atom(), % TODO
bin => binary(),
masked_pan => binary()
}.
-type destination() :: ff_instrument:instrument(resource()).
-type params() :: ff_instrument_machine:params(resource()).
-type machine() :: ff_instrument_machine:st(resource()).
-export_type([id/0]).
-export_type([destination/0]).
-export_type([status/0]).
-export_type([resource/0]).
%% Accessors
-export([account/1]).
-export([id/1]).
-export([name/1]).
-export([identity/1]).
-export([currency/1]).
-export([resource/1]).
-export([status/1]).
%% API
-export([create/3]).
-export([get_machine/1]).
-export([get/1]).
-export([is_accessible/1]).
%% Accessors
-spec id(destination()) -> id().
-spec name(destination()) -> name().
-spec account(destination()) -> account().
-spec identity(destination()) -> identity().
-spec currency(destination()) -> currency().
-spec resource(destination()) -> resource().
-spec status(destination()) -> status().
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).
%% API
-define(NS, 'ff/destination_v2').
-spec create(id(), params(), ctx()) ->
ok |
{error,
_InstrumentCreateError |
exists
}.
create(ID, Params, Ctx) ->
ff_instrument_machine:create(?NS, ID, Params, Ctx).
-spec get_machine(id()) ->
{ok, machine()} |
{error, notfound} .
get_machine(ID) ->
ff_instrument_machine:get(?NS, ID).
-spec get(machine()) ->
destination().
get(Machine) ->
ff_instrument_machine:instrument(Machine).
-spec is_accessible(destination()) ->
{ok, accessible} |
{error, ff_party:inaccessibility()}.
is_accessible(Destination) ->
ff_instrument:is_accessible(Destination).

View File

@ -0,0 +1,140 @@
%%%
%%% 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 name() :: binary().
-type resource(T) :: T.
-type account() :: ff_account:account().
-type identity() :: ff_identity:id().
-type currency() :: ff_currency:id().
-type status() ::
unauthorized |
authorized.
-type instrument(T) :: #{
account := account() | undefined,
resource := resource(T),
name := name(),
status := status()
}.
-type event(T) ::
{created, instrument(T)} |
{account, ff_account:ev()} |
{status_changed, status()}.
-export_type([id/0]).
-export_type([instrument/1]).
-export_type([status/0]).
-export_type([resource/1]).
-export_type([event/1]).
-export([account/1]).
-export([id/1]).
-export([name/1]).
-export([identity/1]).
-export([currency/1]).
-export([resource/1]).
-export([status/1]).
-export([create/5]).
-export([authorize/1]).
-export([is_accessible/1]).
-export([apply_event/2]).
%% Pipeline
-import(ff_pipeline, [do/1, unwrap/1, unwrap/2]).
%% Accessors
-spec account(instrument(_)) ->
account().
account(#{account := V}) ->
V.
-spec id(instrument(_)) ->
id().
-spec name(instrument(_)) ->
binary().
-spec identity(instrument(_)) ->
identity().
-spec currency(instrument(_)) ->
currency().
-spec resource(instrument(T)) ->
resource(T).
-spec status(instrument(_)) ->
status().
id(Instrument) ->
ff_account:id(account(Instrument)).
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.
%%
-spec create(id(), identity(), binary(), currency(), resource(T)) ->
{ok, [event(T)]} |
{error, _WalletError}.
create(ID, IdentityID, Name, CurrencyID, 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)),
[{created, #{name => Name, resource => Resource}}] ++
[{account, Ev} || Ev <- Events] ++
[{status_changed, unauthorized}]
end).
-spec authorize(instrument(T)) ->
{ok, [event(T)]} |
{error, _TODO}.
authorize(#{status := unauthorized}) ->
% TODO
% - Do the actual authorization
{ok, [{status_changed, authorized}]};
authorize(#{status := authorized}) ->
{ok, []}.
-spec is_accessible(instrument(_)) ->
{ok, accessible} |
{error, ff_party:inaccessibility()}.
is_accessible(Instrument) ->
ff_account:is_accessible(account(Instrument)).
%%
-spec apply_event(event(T), ff_maybe:maybe(instrument(T))) ->
instrument(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}).

View File

@ -1,26 +1,28 @@
%%%
%%% Destination machine
%%% Instrument machine
%%%
-module(ff_destination_machine).
-module(ff_instrument_machine).
%% API
-type id() :: machinery:id().
-type ns() :: machinery:ns().
-type ctx() :: ff_ctx:ctx().
-type destination() :: ff_destination:destination().
-type instrument(T) :: ff_instrument:instrument(T).
-type st() ::
ff_machine:st(destination()).
-type st(T) ::
ff_machine:st(instrument(T)).
-export_type([id/0]).
-export_type([st/1]).
-export([create/3]).
-export([get/1]).
-export([create/4]).
-export([get/2]).
%% Accessors
-export([destination/1]).
-export([instrument/1]).
%% Machinery
@ -36,53 +38,51 @@
%%
-define(NS, 'ff/destination_v2').
-type params() :: #{
-type params(T) :: #{
identity := ff_identity:id(),
name := binary(),
currency := ff_currency:id(),
resource := ff_destination:resource()
resource := ff_instrument:resource(T)
}.
-spec create(id(), params(), ctx()) ->
-spec create(ns(), id(), params(_), ctx()) ->
ok |
{error,
_DestinationCreateError |
_InstrumentCreateError |
exists
}.
create(ID, #{identity := IdentityID, name := Name, currency := CurrencyID, resource := Resource}, Ctx) ->
create(NS, ID, #{identity := IdentityID, name := Name, currency := CurrencyID, resource := Resource}, Ctx) ->
do(fun () ->
Events = unwrap(ff_destination:create(ID, IdentityID, Name, CurrencyID, Resource)),
unwrap(machinery:start(?NS, ID, {Events, Ctx}, fistful:backend(?NS)))
Events = unwrap(ff_instrument:create(ID, IdentityID, Name, CurrencyID, Resource)),
unwrap(machinery:start(NS, ID, {Events, Ctx}, fistful:backend(NS)))
end).
-spec get(id()) ->
{ok, st()} |
-spec get(ns(), id()) ->
{ok, st(_)} |
{error, notfound} .
get(ID) ->
ff_machine:get(ff_destination, ?NS, ID).
get(NS, ID) ->
ff_machine:get(ff_instrument, NS, ID).
%% Accessors
-spec destination(st()) ->
destination().
-spec instrument(st(T)) ->
instrument(T).
destination(St) ->
instrument(St) ->
ff_machine:model(St).
%% Machinery
-type event() ::
ff_destination:event().
-type event(T) ::
ff_instrument:event(T).
-type machine() :: ff_machine:machine(event()).
-type result() :: ff_machine:result(event()).
-type machine() :: ff_machine:machine(event(_)).
-type result() :: ff_machine:result(event(_)).
-type handler_opts() :: machinery:handler_opts(_).
-spec init({[event()], ctx()}, machine(), _, handler_opts()) ->
-spec init({[event(_)], ctx()}, machine(), _, handler_opts()) ->
result().
init({Events, Ctx}, #{}, _, _Opts) ->
@ -98,12 +98,12 @@ init({Events, Ctx}, #{}, _, _Opts) ->
result().
process_timeout(Machine, _, _Opts) ->
St = ff_machine:collapse(ff_destination, Machine),
St = ff_machine:collapse(ff_instrument, Machine),
process_timeout(deduce_activity(ff_machine:model(St)), St).
process_timeout(authorize, St) ->
D0 = destination(St),
case ff_destination:authorize(D0) of
D0 = instrument(St),
case ff_instrument:authorize(D0) of
{ok, Events} ->
#{
events => ff_machine:emit_events(Events)

View File

@ -0,0 +1,97 @@
%%%
%%% Source
%%%
%%% TODOs
%%%
%%% - Implement a generic source instead of a current dummy one.
%%%
-module(ff_source).
-type ctx() :: ff_ctx:ctx().
-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() :: ff_identity:status().
-type resource() :: #{
type := internal,
details => binary()
}.
-type source() :: ff_instrument:instrument(resource()).
-type params() :: ff_instrument_machine:params(resource()).
-type machine() :: ff_instrument_machine:st(resource()).
-export_type([id/0]).
-export_type([source/0]).
-export_type([status/0]).
-export_type([resource/0]).
%% Accessors
-export([account/1]).
-export([id/1]).
-export([name/1]).
-export([identity/1]).
-export([currency/1]).
-export([resource/1]).
-export([status/1]).
%% API
-export([create/3]).
-export([get_machine/1]).
-export([get/1]).
-export([is_accessible/1]).
%% Accessors
-spec id(source()) -> id().
-spec name(source()) -> name().
-spec account(source()) -> account().
-spec identity(source()) -> identity().
-spec currency(source()) -> currency().
-spec resource(source()) -> resource().
-spec status(source()) -> status().
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).
%% API
-define(NS, 'ff/source_v1').
-spec create(id(), params(), ctx()) ->
ok |
{error,
_InstrumentCreateError |
exists
}.
create(ID, Params, Ctx) ->
ff_instrument_machine:create(?NS, ID, Params, Ctx).
-spec get_machine(id()) ->
{ok, machine()} |
{error, notfound} .
get_machine(ID) ->
ff_instrument_machine:get(?NS, ID).
-spec get(machine()) ->
source().
get(Machine) ->
ff_instrument_machine:instrument(Machine).
-spec is_accessible(source()) ->
{ok, accessible} |
{error, ff_party:inaccessibility()}.
is_accessible(Source) ->
ff_instrument:is_accessible(Source).

View File

@ -1,6 +1,6 @@
{application, ff_withdraw, [
{application, ff_transfer, [
{description,
"Withdrawal processing"
"Transfer processing"
},
{vsn, "1"},
{registered, []},
@ -17,7 +17,8 @@
{env, []},
{modules, []},
{maintainers, [
"Andrey Mayorov <a.mayorov@rbkmoney.com>"
"Andrey Mayorov <a.mayorov@rbkmoney.com>",
"Anton Belyaev <a.belyaev@rbkmoney.com>"
]},
{licenses, []},
{links, ["https://github.com/rbkmoney/fistful-server"]}

View File

@ -0,0 +1,196 @@
%%%
%%% Transfer between 2 accounts
%%%
-module(ff_transfer).
-type id(T) :: T.
-type handler() :: module().
-type account() :: ff_account:account().
-type body() :: ff_transaction:body().
-type params(T) :: T.
-type p_transfer() :: ff_postings_transfer:transfer().
-type transfer(T) :: #{
version := non_neg_integer(),
id := id(binary()),
handler := handler(),
source := account(),
destination := account(),
body := body(),
params := params(T),
p_transfer := ff_maybe:maybe(p_transfer()),
session => session(),
status => status()
}.
-type session() ::
id(_).
-type status() ::
pending |
succeeded |
{failed, _TODO} .
-type event() ::
{created, transfer(_)} |
{p_transfer, ff_postings_transfer:ev()} |
{session_started, session()} |
{session_finished, session()} |
{status_changed, status()} .
-export_type([transfer/1]).
-export_type([handler/0]).
-export_type([params/1]).
-export_type([event/0]).
-export([id/1]).
-export([handler/1]).
-export([source/1]).
-export([destination/1]).
-export([body/1]).
-export([params/1]).
-export([p_transfer/1]).
-export([status/1]).
-export([create/6]).
%% ff_transfer_machine behaviour
-behaviour(ff_transfer_machine).
-export([process_transfer/1]).
%% Event source
-export([apply_event/2]).
%% Pipeline
-import(ff_pipeline, [do/1, unwrap/1, with/3]).
%% Accessors
-spec id(transfer(_)) -> id(binary()).
-spec handler(transfer(_)) -> handler().
-spec source(transfer(_)) -> account().
-spec destination(transfer(_)) -> account().
-spec body(transfer(_)) -> body().
-spec params(transfer(T)) -> params(T).
-spec status(transfer(_)) -> status().
-spec p_transfer(transfer(_)) -> p_transfer().
id(#{id := V}) -> V.
handler(#{handler := V}) -> V.
source(#{source := V}) -> V.
destination(#{destination := V}) -> V.
body(#{body := V}) -> V.
params(#{params := V}) -> V.
status(#{status := V}) -> V.
p_transfer(#{p_transfer := V}) -> V.
%%
-spec create(handler(), id(_), account(), account(), body(), params(_)) ->
{ok, [event()]} |
{error,
_PostingsTransferError
}.
create(Handler, ID, Source, Destination, Body, Params) ->
do(fun () ->
PTransferID = construct_p_transfer_id(ID),
PostingsTransferEvents = unwrap(ff_postings_transfer:create(PTransferID, [{Source, Destination, Body}])),
[{created, #{
version => 1,
id => ID,
handler => Handler,
source => Source,
destination => Destination,
body => Body,
params => Params
}}] ++
[{p_transfer, Ev} || Ev <- PostingsTransferEvents] ++
[{status_changed, pending}]
end).
construct_p_transfer_id(ID) ->
ID.
%% ff_transfer_machine behaviour
-spec process_transfer(transfer(_)) ->
{ok, [ff_transfer_machine:event(event())] | poll} |
{error, _Reason}.
process_transfer(Transfer) ->
process_activity(deduce_activity(Transfer), Transfer).
-type activity() ::
prepare_transfer |
commit_transfer |
cancel_transfer |
undefined .
-spec deduce_activity(transfer(_)) ->
activity().
deduce_activity(#{status := {failed, _}, p_transfer := #{status := prepared}}) ->
cancel_transfer;
deduce_activity(#{status := succeeded, p_transfer := #{status := prepared}}) ->
commit_transfer;
deduce_activity(#{status := pending, p_transfer := #{status := created}}) ->
prepare_transfer;
deduce_activity(_) ->
undefined.
process_activity(prepare_transfer, Transfer) ->
with(p_transfer, Transfer, fun ff_postings_transfer:prepare/1);
process_activity(commit_transfer, Transfer) ->
with(p_transfer, Transfer, fun ff_postings_transfer:commit/1);
process_activity(cancel_transfer, Transfer) ->
with(p_transfer, Transfer, fun ff_postings_transfer:cancel/1).
%%
-spec apply_event(event(), ff_maybe:maybe(transfer(T))) ->
transfer(T).
apply_event(Ev, T) ->
apply_event_(maybe_migrate(Ev), T).
apply_event_({created, T}, undefined) ->
T;
apply_event_({status_changed, S}, T) ->
maps:put(status, S, T);
apply_event_({p_transfer, Ev}, T = #{p_transfer := PT}) ->
T#{p_transfer := ff_postings_transfer:apply_event(Ev, PT)};
apply_event_({p_transfer, Ev}, T) ->
apply_event({p_transfer, Ev}, T#{p_transfer => undefined});
apply_event_({session_started, S}, T) ->
maps:put(session, S, T);
apply_event_({session_finished, S}, T = #{session := S}) ->
maps:remove(session, T).
maybe_migrate(Ev = {created, #{version := 1}}) ->
Ev;
maybe_migrate({created, T}) ->
DestinationID = maps:get(destination, T),
{ok, DestinationSt} = ff_destination:get_machine(DestinationID),
DestinationAcc = ff_destination:account(ff_destination:get(DestinationSt)),
SourceID = maps:get(source, T),
{ok, SourceSt} = ff_wallet_machine:get(SourceID),
SourceAcc = ff_wallet:account(ff_wallet_machine:wallet(SourceSt)),
{created, T#{
version => 1,
handler => ff_withdrawal,
source => SourceAcc,
destination => DestinationAcc,
params => #{
destination => DestinationID,
source => SourceID
}
}};
maybe_migrate({transfer, PTransferEv}) ->
{p_transfer, PTransferEv};
maybe_migrate(Ev) ->
Ev.

View File

@ -0,0 +1,165 @@
%%%
%%% Transfer machine
%%%
-module(ff_transfer_machine).
%% API
-type id() :: machinery:id().
-type ns() :: machinery:namespace().
-type ctx() :: ff_ctx:ctx().
-type transfer(T) :: ff_transfer:transfer(T).
-type account() :: ff_account:account().
-type event(T) :: T.
-type events(T) :: [{integer(), ff_machine:timestamped_event(event(T))}].
%% Behaviour definition
-type st(T) :: ff_machine:st(transfer(T)).
-export_type([id/0]).
-export_type([ns/0]).
-export_type([st/1]).
-export_type([event/1]).
-export_type([events/1]).
-callback process_transfer(transfer(_)) ->
{ok, [event(_)] | poll} |
{error, _Reason}.
-callback process_call(_CallArgs, transfer(_)) ->
{ok, [event(_)] | poll} |
{error, _Reason}.
-optional_callbacks([process_call/2]).
%% API
-export([create/4]).
-export([get/2]).
-export([events/3]).
%% Accessors
-export([transfer/1]).
%% Machinery
-behaviour(machinery).
-export([init/4]).
-export([process_timeout/3]).
-export([process_call/4]).
%% Pipeline
-import(ff_pipeline, [do/1, unwrap/1]).
%%
-type params() :: #{
handler := ff_transfer:handler(),
source := account(),
destination := account(),
body := ff_transaction:body(),
params := ff_transfer:params()
}.
-spec create(ns(), id(), params(), ctx()) ->
ok |
{error,
_TransferError |
exists
}.
create(NS, ID,
#{handler := Handler, source := Source, destination := Destination, body := Body, params := Params},
Ctx)
->
do(fun () ->
Events = unwrap(ff_transfer:create(Handler, ID, Source, Destination, Body, Params)),
unwrap(machinery:start(NS, ID, {Events, Ctx}, backend(NS)))
end).
-spec get(ns(), id()) ->
{ok, st(_)} |
{error, notfound}.
get(NS, ID) ->
ff_machine:get(ff_transfer, NS, ID).
-spec events(ns(), id(), machinery:range()) ->
{ok, events(_)} |
{error, notfound}.
events(NS, ID, Range) ->
do(fun () ->
#{history := History} = unwrap(machinery:get(NS, ID, Range, backend(NS))),
[{EventID, TsEv} || {EventID, _, TsEv} <- History]
end).
backend(NS) ->
fistful:backend(NS).
%% Accessors
-spec transfer(st(T)) ->
transfer(T).
transfer(St) ->
ff_machine:model(St).
%% Machinery
-type machine() :: ff_machine:machine(event(_)).
-type result() :: ff_machine:result(event(_)).
-type handler_opts() :: machinery:handler_opts(_).
-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_opts()) ->
result().
process_timeout(Machine, _, _Opts) ->
St = ff_machine:collapse(ff_transfer, Machine),
Transfer = transfer(St),
process_result((ff_transfer:handler(Transfer)):process_transfer(Transfer), St).
-spec process_call(_CallArgs, machine(), _, handler_opts()) ->
{ok, result()}.
process_call(CallArgs, Machine, _, _Opts) ->
St = ff_machine:collapse(ff_transfer, Machine),
Transfer = transfer(St),
{ok, process_result((ff_transfer:handler(Transfer)):process_call(CallArgs, Transfer), St)}.
process_result({ok, poll}, St) ->
#{
action => set_poll_timer(St)
};
process_result({ok, Events}, _St) ->
#{
events => ff_machine:emit_events(Events),
action => continue
};
process_result({error, Reason}, _St) ->
#{
events => emit_failure(Reason)
}.
set_poll_timer(St) ->
Now = machinery_time:now(),
Timeout = erlang:max(1, machinery_time:interval(Now, ff_machine:updated(St)) div 1000),
{set_timer, {timeout, Timeout}}.
emit_failure(Reason) ->
ff_machine:emit_event({status_changed, {failed, Reason}}).

View File

@ -0,0 +1,177 @@
%%%
%%% Withdrawal
%%%
-module(ff_withdrawal).
-type id() :: ff_transfer_machine:id().
-type wallet() :: ff_wallet:id(_).
-type destination() :: ff_destination:id(_).
-type withdrawal() :: ff_transfer:transfer(transfer_params()).
-type transfer_params() :: #{
source := wallet(),
destination := destination()
}.
-type machine() :: ff_transfer_machine:st(transfer_params()).
-type events() :: ff_transfer_machine:events().
-export_type([withdrawal/0]).
-export_type([machine/0]).
-export_type([transfer_params/0]).
-export_type([events/0]).
%% ff_transfer_machine behaviour
-behaviour(ff_transfer_machine).
-export([process_transfer/1]).
%% Accessors
-export([source/1]).
-export([destination/1]).
-export([id/1]).
-export([source_acc/1]).
-export([destination_acc/1]).
-export([body/1]).
-export([status/1]).
%% API
-export([create/3]).
-export([get/1]).
-export([get_machine/1]).
-export([events/2]).
%% Pipeline
-import(ff_pipeline, [do/1, unwrap/1, unwrap/2, valid/2]).
%% Accessors
-spec source(withdrawal()) -> wallet().
-spec destination(withdrawal()) -> destination().
-spec id(withdrawal()) -> ff_transfer:id().
-spec source_acc(withdrawal()) -> ff_account:account().
-spec destination_acc(withdrawal()) -> ff_account:account().
-spec body(withdrawal()) -> ff_transfer:body().
-spec status(withdrawal()) -> ff_transfer:status().
source(T) -> maps:get(source, ff_transfer:params(T)).
destination(T) -> maps:get(destination, ff_transfer:params(T)).
id(T) -> ff_transfer:id(T).
source_acc(T) -> ff_transfer:source(T).
destination_acc(T) -> ff_transfer:destination(T).
body(T) -> ff_transfer:body(T).
status(T) -> ff_transfer:status(T).
%%
-define(NS, 'ff/withdrawal_v2').
-type ctx() :: ff_ctx:ctx().
-type params() :: #{
source := ff_wallet_machine:id(),
destination := ff_destination:id(),
body := ff_transaction:body()
}.
-spec create(id(), params(), ctx()) ->
ok |
{error,
{source, notfound} |
{destination, notfound | unauthorized} |
{provider, notfound} |
exists |
_TransferError
}.
create(ID, #{source := SourceID, destination := DestinationID, body := Body}, Ctx) ->
do(fun() ->
Source = ff_wallet_machine:wallet(unwrap(source, ff_wallet_machine:get(SourceID))),
Destination = ff_destination:get(
unwrap(destination, ff_destination:get_machine(DestinationID))
),
ok = unwrap(destination, valid(authorized, ff_destination:status(Destination))),
Params = #{
handler => ?MODULE,
source => ff_wallet:account(Source),
destination => ff_destination:account(Destination),
body => Body,
params => #{
destination => DestinationID
}
},
unwrap(ff_transfer_machine:create(?NS, ID, Params, Ctx))
end).
-spec get(machine()) ->
withdrawal().
get(St) ->
ff_transfer_machine:transfer(St).
-spec get_machine(id()) ->
{ok, machine()} |
{error, notfound}.
get_machine(ID) ->
ff_transfer_machine:get(?NS, ID).
-spec events(id(), machinery:range()) ->
{ok, events()} |
{error, notfound}.
events(ID, Range) ->
ff_transfer_machine:events(?NS, ID, Range).
%% ff_transfer_machine behaviour
-spec process_transfer(withdrawal()) ->
{ok, [ff_transfer_machine:event(ff_transfer:event())]} |
{error, _Reason}.
process_transfer(Transfer = #{status := pending, session := _}) ->
poll_session_completion(Transfer);
process_transfer(Transfer = #{status := pending, p_transfer := #{status := prepared}}) ->
create_session(Transfer);
process_transfer(Transfer) ->
ff_transfer:process_transfer(Transfer).
create_session(Withdrawal) ->
ID = construct_session_id(id(Withdrawal)),
{ok, SenderSt} = ff_identity_machine:get(ff_account:identity(ff_transfer:source(Withdrawal))),
{ok, ReceiverSt} = ff_identity_machine:get(ff_account:identity(ff_transfer:destination(Withdrawal))),
TransferData = #{
id => ID,
cash => body(Withdrawal),
sender => ff_identity_machine:identity(SenderSt),
receiver => ff_identity_machine:identity(ReceiverSt)
},
do(fun () ->
ok = unwrap(ff_withdrawal_session_machine:create(ID, TransferData, #{destination => destination(Withdrawal)})),
[{session_started, ID}]
end).
construct_session_id(ID) ->
ID.
poll_session_completion(#{session := SID}) ->
{ok, Session} = ff_withdrawal_session_machine:get(SID),
do(fun () ->
case ff_withdrawal_session_machine:status(Session) of
active ->
poll;
{finished, {success, _}} ->
[
{session_finished, SID},
{status_changed, succeeded}
];
{finished, {failed, Failure}} ->
[
{session_finished, SID},
{status_changed, {failed, Failure}}
]
end
end).

View File

@ -8,23 +8,21 @@
-module(ff_withdrawal_provider).
-export([id/1]).
-export([get/1]).
-export([get_adapter/1]).
-export([choose/2]).
%%
-type id() :: binary().
-type provider() :: #{
_ => _ % TODO
}.
-export([id/1]).
-type adapter() :: {ff_adapter:adapter(), ff_adapter:opts()}.
-export_type([adapter/0]).
-export([get/1]).
-export([choose/3]).
-export([create_session/3]).
%%
adapter(#{adapter := V}) ->
V.
adapter_opts(P) ->
maps:get(adapter_opts, P, #{}).
%%
@ -38,21 +36,31 @@ id(_) ->
ff_map:result(provider()).
get(ID) ->
case genlib_map:get(ID, genlib_app:env(ff_withdraw, provider, #{})) of
case genlib_map:get(ID, genlib_map:get(provider, genlib_app:env(ff_transfer, withdrawal, #{}))) of
V when V /= undefined ->
{ok, V};
undefined ->
{error, notfound}
end.
-spec choose(ff_wallet:wallet(), ff_destination:destination(), ff_transaction:body()) ->
-spec get_adapter(id()) ->
{ok, adapter()} |
{error, notfound}.
get_adapter(ID) ->
case ?MODULE:get(ID) of
{ok, Provider} ->
{ok, {adapter(Provider), adapter_opts(Provider)}};
Error = {error, _} ->
Error
end.
-spec choose(ff_destination:destination(), ff_transaction:body()) ->
{ok, id()} |
{error, notfound}.
choose(_Source, Destination, _Body) ->
{ok, IdentitySt} = ff_identity_machine:get(ff_account:identity(ff_destination:account(Destination))),
{ok, Provider} = ff_provider:get(ff_identity:provider(ff_identity_machine:identity(IdentitySt))),
[ID | _] = ff_provider:routes(Provider),
choose(Destination, Body) ->
ID = route(Destination, Body),
case ?MODULE:get(ID) of
{ok, _} -> {ok, ID};
E = {error, _} -> E
@ -60,9 +68,14 @@ choose(_Source, Destination, _Body) ->
%%
-spec create_session(id(), ff_adapter_withdrawal:withdrawal(), provider()) ->
ok | {error, exists}.
route(Destination, _Body) ->
{ok, IdentitySt} = ff_identity_machine:get(ff_account:identity(ff_destination:account(Destination))),
{ok, Provider} = ff_provider:get(ff_identity:provider(ff_identity_machine:identity(IdentitySt))),
[ID | _] = ff_provider:routes(Provider),
ID.
create_session(ID, Withdrawal, Provider) ->
Adapter = {adapter(Provider), adapter_opts(Provider)},
ff_withdrawal_session_machine:create(ID, Adapter, Withdrawal).
adapter(#{adapter := V}) ->
V.
adapter_opts(P) ->
maps:get(adapter_opts, P, #{}).

View File

@ -15,10 +15,9 @@
%% API
-export([status/1]).
-export([create/3]).
-export([get/1]).
-export([status/1]).
%% machinery
@ -29,38 +28,48 @@
%%
%% Types
%%
-type id() :: machinery:id().
-type session() :: #{
id => id(),
status => session_status(),
id => id(),
status => status(),
withdrawal => withdrawal(),
adapter => adapter()
provider => ff_withdrawal_provider:provider(),
adapter => ff_withdrawal_provider:adapter()
}.
-type session_result() :: {success, trx_info()} | {failed, ff_adapter_withdrawal:failure()}.
-type session_status() :: active
| {finished, session_result()}.
-type status() :: active
| {finished, {success, _} | {failed, _}}.
-type ev() :: {created, session()}
| {next_state, ff_adapter:state()}
| {finished, session_result()}.
-type adapter() :: {ff_adapter:adapter(), ff_adapter:opts()}.
-type data() :: #{
id := id(),
cash := ff_transaction:body(),
sender := ff_identity:identity(),
receiver := ff_identity:identity()
}.
-type params() :: #{
destination := ff_destination:id(_)
}.
%%
%% Internal types
%%
-type id() :: machinery:id().
-type trx_info() :: dmsl_domain_thrift:'TransactionInfo'().
-type auxst() :: undefined.
-type withdrawal() :: ff_adapter_withdrawal:withdrawal().
-type machine() :: machinery:machine(ev(), auxst()).
-type result() :: machinery:result(ev(), auxst()).
-type withdrawal() :: ff_adapter_withdrawal:withdrawal().
-type machine() :: machinery:machine(ev(), auxst()).
-type result() :: machinery:result(ev(), auxst()).
-type handler_opts() :: machinery:handler_opts(_).
-type st() :: #{
@ -77,19 +86,16 @@
%%
-spec status(session()) ->
session_status().
status().
status(#{status := V}) -> V.
%%
-spec create(ID, Adapter, Withdrawal) -> ok | Error when
ID :: id(),
Adapter :: adapter(),
Withdrawal :: withdrawal(),
Error :: {error, exists}.
create(ID, Adapter, Withdrawal) ->
Session = create_session(ID, Adapter, Withdrawal),
-spec create(id(), data(), params()) ->
ok | {error, exists}.
create(ID, Data, Params) ->
Session = create_session(ID, Data, Params),
do(fun () ->
unwrap(machinery:start(?NS, ID, Session, backend()))
end).
@ -162,16 +168,32 @@ process_intent({sleep, Timer}) ->
%%
-spec create_session(id(), adapter(), ff_adapter_withdrawal:withdrawal()) -> session().
create_session(ID, Adapter, Withdrawal) ->
-spec create_session(id(), data(), params()) ->
session().
create_session(ID, Data = #{cash := Cash}, #{destination := DestinationID}) ->
{ok, DestinationSt} = ff_destination:get_machine(DestinationID),
Destination = ff_destination:get(DestinationSt),
ProviderID = get_provider(Destination, Cash),
#{
id => ID,
withdrawal => Withdrawal,
adapter => Adapter,
status => active
id => ID,
withdrawal => create_adapter_withdrawal(Data, Destination),
provider => ProviderID,
adapter => get_adapter(ProviderID),
status => active
}.
-spec set_session_status(session_status(), session()) -> session().
get_provider(Destination, Cash) ->
{ok, ProviderID} = ff_withdrawal_provider:choose(Destination, Cash),
ProviderID.
get_adapter(ProviderID) ->
{ok, Adapter} = ff_withdrawal_provider:get_adapter(ProviderID),
Adapter.
create_adapter_withdrawal(Data, Destination) ->
Data#{destination => Destination}.
-spec set_session_status(status(), session()) -> session().
set_session_status(SessionState, Session) ->
Session#{status => SessionState}.
@ -212,4 +234,3 @@ emit_ts_events(Es) ->
emit_ts_events(Es, Ts) ->
[{ev, Ts, Body} || Body <- Es].

View File

@ -1,13 +1,19 @@
-module(ff_withdrawal_SUITE).
-module(ff_transfer_SUITE).
-include_lib("fistful_proto/include/ff_proto_fistful_thrift.hrl").
-export([all/0]).
-export([groups/0]).
-export([init_per_suite/1]).
-export([end_per_suite/1]).
-export([init_per_group/2]).
-export([end_per_group/2]).
-export([init_per_testcase/2]).
-export([end_per_testcase/2]).
-export([get_missing_fails/1]).
-export([withdrawal_ok/1]).
-export([deposit_via_admin_ok/1]).
-export([deposit_withdrawal_ok/1]).
-import(ct_helper, [cfg/2]).
@ -19,9 +25,17 @@
-spec all() -> [test_case_name() | {group, group_name()}].
all() ->
[{group, default}].
-spec groups() -> [{group_name(), list(), [test_case_name()]}].
groups() ->
[
get_missing_fails,
withdrawal_ok
{default, [parallel], [
get_missing_fails,
deposit_via_admin_ok,
deposit_withdrawal_ok
]}
].
-spec init_per_suite(config()) -> config().
@ -44,7 +58,11 @@ init_per_suite(C) ->
}},
{backends, maps:from_list([{NS, Be} || NS <- [
'ff/identity' ,
'ff/sequence' ,
'ff/wallet_v2' ,
'ff/source_v1' ,
'ff/deposit_v1' ,
'ff/deposit/session_v1' ,
'ff/destination_v2' ,
'ff/withdrawal_v2' ,
'ff/withdrawal/session_v2'
@ -53,8 +71,10 @@ init_per_suite(C) ->
get_provider_config()
}
]},
{ff_withdraw, [
{provider, get_withdrawal_provider_config()}
{ff_transfer, [
{withdrawal,
#{provider => get_withdrawal_provider_config()}
}
]}
]),
SuiteSup = ct_sup:start(),
@ -62,20 +82,25 @@ init_per_suite(C) ->
Routes = machinery_mg_backend:get_routes(
[
construct_handler(ff_identity_machine , "identity" , BeConf),
construct_handler(ff_sequence , "sequence" , BeConf),
construct_handler(ff_wallet_machine , "wallet" , BeConf),
construct_handler(ff_destination_machine , "destination" , BeConf),
construct_handler(ff_withdrawal_machine , "withdrawal" , BeConf),
construct_handler(ff_instrument_machine , "source" , BeConf),
construct_handler(ff_transfer_machine , "deposit" , BeConf),
construct_handler(ff_deposit_session_machine , "deposit/session" , BeConf),
construct_handler(ff_instrument_machine , "destination" , BeConf),
construct_handler(ff_transfer_machine , "withdrawal" , BeConf),
construct_handler(ff_withdrawal_session_machine , "withdrawal/session" , BeConf)
],
BeOpts
),
AdminRoutes = get_admin_routes(),
{ok, _} = supervisor:start_child(SuiteSup, woody_server:child_spec(
?MODULE,
BeOpts#{
ip => {0, 0, 0, 0},
port => 8022,
handlers => [],
additional_routes => Routes
additional_routes => AdminRoutes ++ Routes
}
)),
C1 = ct_helper:makeup_cfg(
@ -98,6 +123,13 @@ construct_handler(Module, Suffix, BeConf) ->
{{fistful, Module},
#{path => ff_string:join(["/v1/stateproc/ff/", Suffix]), backend_config => BeConf}}.
get_admin_routes() ->
Path = <<"/v1/admin">>,
woody_server_thrift_http_handler:get_routes(#{
handlers => [{Path, {{ff_proto_fistful_thrift, 'FistfulAdmin'}, {ff_server_handler, []}}}],
event_handler => scoper_woody_event_handler
}).
-spec end_per_suite(config()) -> _.
end_per_suite(C) ->
@ -107,6 +139,17 @@ end_per_suite(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) ->
@ -122,30 +165,112 @@ end_per_testcase(_Name, _C) ->
%%
-spec get_missing_fails(config()) -> test_return().
-spec withdrawal_ok(config()) -> test_return().
-spec deposit_via_admin_ok(config()) -> test_return().
-spec deposit_withdrawal_ok(config()) -> test_return().
get_missing_fails(_C) ->
ID = genlib:unique(),
{error, notfound} = ff_withdrawal_machine:get(ID).
{error, notfound} = ff_withdrawal:get_machine(ID).
withdrawal_ok(C) ->
deposit_via_admin_ok(C) ->
Party = create_party(C),
Resource = {bank_card, ct_cardstore:bank_card(<<"4150399999000900">>, {12, 2025}, C)},
IID = create_identity(Party, C),
ICID = genlib:unique(),
SID = create_wallet(IID, <<"HAHA NO">>, <<"RUB">>, C),
% Create destination
DID = create_destination(IID, <<"XDDD">>, <<"RUB">>, Resource, C),
{ok, DS1} = ff_destination_machine:get(DID),
D1 = ff_destination_machine:destination(DS1),
unauthorized = ff_destination:status(D1),
WalID = create_wallet(IID, <<"HAHA NO">>, <<"RUB">>, C),
ok = await_wallet_balance({0, <<"RUB">>}, WalID),
% Create source
{ok, Src1} = admin_call('CreateSource', [#fistful_SourceParams{
name = <<"HAHA NO">>,
identity_id = IID,
currency = #fistful_CurrencyRef{symbolic_code = <<"RUB">>},
resource = #fistful_SourceResource{details = <<"Infinite source of cash">>}
}]),
unauthorized = Src1#fistful_Source.status,
SrcID = Src1#fistful_Source.id,
authorized = ct_helper:await(
authorized,
fun () ->
{ok, DS} = ff_destination_machine:get(DID),
ff_destination:status(ff_destination_machine:destination(DS))
{ok, Src} = admin_call('GetSource', [SrcID]),
Src#fistful_Source.status
end
),
% Process deposit
{ok, Dep1} = admin_call('CreateDeposit', [#fistful_DepositParams{
source = SrcID,
destination = WalID,
body = #fistful_DepositBody{
amount = 20000,
currency = #fistful_CurrencyRef{symbolic_code = <<"RUB">>}
}
}]),
DepID = Dep1#fistful_Deposit.id,
{pending, _} = Dep1#fistful_Deposit.status,
succeeded = ct_helper:await(
succeeded,
fun () ->
{ok, Dep} = admin_call('GetDeposit', [DepID]),
{Status, _} = Dep#fistful_Deposit.status,
Status
end,
genlib_retry:linear(3, 5000)
),
ok = await_wallet_balance({20000, <<"RUB">>}, WalID).
deposit_withdrawal_ok(C) ->
Party = create_party(C),
IID = create_identity(Party, C),
ICID = genlib:unique(),
WalID = create_wallet(IID, <<"HAHA NO">>, <<"RUB">>, C),
ok = await_wallet_balance({0, <<"RUB">>}, WalID),
% Create source
SrcResource = #{type => internal, details => <<"Infinite source of cash">>},
SrcID = create_instrument(source, IID, <<"XSource">>, <<"RUB">>, SrcResource, C),
{ok, SrcM1} = ff_source:get_machine(SrcID),
Src1 = ff_source:get(SrcM1),
unauthorized = ff_source:status(Src1),
authorized = ct_helper:await(
authorized,
fun () ->
{ok, SrcM} = ff_source:get_machine(SrcID),
ff_source:status(ff_source:get(SrcM))
end
),
% Process deposit
DepID = generate_id(),
ok = ff_deposit:create(
DepID,
#{source => SrcID, destination => WalID, body => {10000, <<"RUB">>}},
ff_ctx:new()
),
{ok, DepM1} = ff_deposit:get_machine(DepID),
pending = ff_deposit:status(ff_deposit:get(DepM1)),
succeeded = ct_helper:await(
succeeded,
fun () ->
{ok, DepM} = ff_deposit:get_machine(DepID),
ff_deposit:status(ff_deposit:get(DepM))
end,
genlib_retry:linear(3, 5000)
),
ok = await_wallet_balance({10000, <<"RUB">>}, WalID),
% Create destination
DestResource = {bank_card, ct_cardstore:bank_card(<<"4150399999000900">>, {12, 2025}, C)},
DestID = create_instrument(destination, IID, <<"XDesination">>, <<"RUB">>, DestResource, C),
{ok, DestM1} = ff_destination:get_machine(DestID),
Dest1 = ff_destination:get(DestM1),
unauthorized = ff_destination:status(Dest1),
authorized = ct_helper:await(
authorized,
fun () ->
{ok, DestM} = ff_destination:get_machine(DestID),
ff_destination:status(ff_destination:get(DestM))
end
),
% Pass identification
Doc1 = ct_identdocstore:rus_retiree_insurance_cert(genlib:unique(), C),
Doc2 = ct_identdocstore:rus_domestic_passport(C),
@ -164,24 +289,25 @@ withdrawal_ok(C) ->
ff_identity_challenge:status(IC)
end
),
% Process withdrawal
WID = generate_id(),
ok = ff_withdrawal_machine:create(
WID,
#{source => SID, destination => DID, body => {4242, <<"RUB">>}},
WdrID = generate_id(),
ok = ff_withdrawal:create(
WdrID,
#{source => WalID, destination => DestID, body => {4242, <<"RUB">>}},
ff_ctx:new()
),
{ok, WS1} = ff_withdrawal_machine:get(WID),
W1 = ff_withdrawal_machine:withdrawal(WS1),
pending = ff_withdrawal:status(W1),
{ok, WdrM1} = ff_withdrawal:get_machine(WdrID),
pending = ff_withdrawal:status(ff_withdrawal:get(WdrM1)),
succeeded = ct_helper:await(
succeeded,
fun () ->
{ok, WS} = ff_withdrawal_machine:get(WID),
ff_withdrawal:status(ff_withdrawal_machine:withdrawal(WS))
{ok, WdrM} = ff_withdrawal:get_machine(WdrID),
ff_withdrawal:status(ff_withdrawal:get(WdrM))
end,
genlib_retry:linear(3, 5000)
).
genlib_retry:linear(5, 5000)
),
ok = await_wallet_balance({10000 - 4242, <<"RUB">>}, WalID).
create_party(_C) ->
ID = genlib:unique(),
@ -209,18 +335,48 @@ create_wallet(IdentityID, Name, Currency, _C) ->
),
ID.
create_destination(IdentityID, Name, Currency, Resource, _C) ->
await_wallet_balance(Balance, ID) ->
Balance = ct_helper:await(
Balance,
fun () -> get_wallet_balance(ID) end,
genlib_retry:linear(3, 500)
),
ok.
get_wallet_balance(ID) ->
{ok, Machine} = ff_wallet_machine:get(ID),
Account = ff_wallet:account(ff_wallet_machine:wallet(Machine)),
{ok, {Amounts, Currency}} = ff_transaction:balance(ff_account:accounter_account_id(Account)),
{ff_indef:current(Amounts), Currency}.
create_instrument(Type, IdentityID, Name, Currency, Resource, C) ->
ID = genlib:unique(),
ok = ff_destination_machine:create(
ok = create_instrument(
Type,
ID,
#{identity => IdentityID, name => Name, currency => Currency, resource => Resource},
ff_ctx:new()
ff_ctx:new(),
C
),
ID.
create_instrument(destination, ID, Params, Ctx, _C) ->
ff_destination:create(ID, Params, Ctx);
create_instrument(source, ID, Params, Ctx, _C) ->
ff_source:create(ID, Params, Ctx).
generate_id() ->
genlib:to_binary(genlib_time:ticks() div 1000).
admin_call(Fun, Args) ->
Service = {ff_proto_fistful_thrift, 'FistfulAdmin'},
Request = {Service, Fun, Args},
Client = ff_woody_client:new(#{
url => <<"http://localhost:8022/v1/admin">>,
event_handler => woody_event_handler_default
}),
ff_woody_client:call(Client, Request).
%%
-include_lib("ff_cth/include/ct_domain.hrl").

View File

@ -1,149 +0,0 @@
%%%
%%% Destination
%%%
%%% TODOs
%%%
%%% - We must consider withdrawal provider terms ensure that the provided
%%% Resource is ok to withdraw to.
%%%
-module(ff_destination).
-type account() :: ff_account:account().
-type resource() ::
{bank_card, resource_bank_card()}.
-type id() :: binary().
-type identity() :: ff_identity:id().
-type currency() :: ff_currency:id().
-type resource_bank_card() :: #{
token := binary(),
payment_system => atom(), % TODO
bin => binary(),
masked_pan => binary()
}.
-type status() ::
unauthorized |
authorized.
-type destination() :: #{
account := account() | undefined,
resource := resource(),
name := binary(),
status := status()
}.
-type event() ::
{created, destination()} |
{account, ff_account:ev()} |
{status_changed, status()}.
-export_type([id/0]).
-export_type([destination/0]).
-export_type([status/0]).
-export_type([resource/0]).
-export_type([event/0]).
-export([account/1]).
-export([id/1]).
-export([name/1]).
-export([identity/1]).
-export([currency/1]).
-export([resource/1]).
-export([status/1]).
-export([create/5]).
-export([authorize/1]).
-export([is_accessible/1]).
-export([apply_event/2]).
%% Pipeline
-import(ff_pipeline, [do/1, unwrap/1, unwrap/2]).
%% Accessors
-spec account(destination()) ->
account().
account(#{account := V}) ->
V.
-spec id(destination()) ->
id().
-spec name(destination()) ->
binary().
-spec identity(destination()) ->
identity().
-spec currency(destination()) ->
currency().
-spec resource(destination()) ->
resource().
-spec status(destination()) ->
status().
id(Destination) ->
ff_account:id(account(Destination)).
name(#{name := V}) ->
V.
identity(Destination) ->
ff_account:identity(account(Destination)).
currency(Destination) ->
ff_account:currency(account(Destination)).
resource(#{resource := V}) ->
V.
status(#{status := V}) ->
V.
%%
-spec create(id(), identity(), binary(), currency(), resource()) ->
{ok, [event()]} |
{error, _WalletError}.
create(ID, IdentityID, Name, CurrencyID, 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)),
[{created, #{name => Name, resource => Resource}}] ++
[{account, Ev} || Ev <- Events] ++
[{status_changed, unauthorized}]
end).
-spec authorize(destination()) ->
{ok, [event()]} |
{error, _TODO}.
authorize(#{status := unauthorized}) ->
% TODO
% - Do the actual authorization
{ok, [{status_changed, authorized}]};
authorize(#{status := authorized}) ->
{ok, []}.
-spec is_accessible(destination()) ->
{ok, accessible} |
{error, ff_party:inaccessibility()}.
is_accessible(Destination) ->
ff_account:is_accessible(account(Destination)).
%%
-spec apply_event(event(), ff_maybe:maybe(destination())) ->
destination().
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}).

View File

@ -1,212 +0,0 @@
%%%
%%% Withdrawal
%%%
-module(ff_withdrawal).
-type id(T) :: T.
-type wallet() :: ff_wallet:id(_).
-type destination() :: ff_destination:id(_).
-type body() :: ff_transaction:body().
-type provider() :: ff_withdrawal_provider:id().
-type transfer() :: ff_transfer:transfer().
-type withdrawal() :: #{
id := id(binary()),
source := wallet(),
destination := destination(),
body := body(),
provider := provider(),
transfer := ff_maybe:maybe(transfer()),
session => session(),
status => status()
}.
-type session() ::
id(_).
-type status() ::
pending |
succeeded |
{failed, _TODO} .
-type event() ::
{created, withdrawal()} |
{transfer, ff_transfer:ev()} |
{session_started, session()} |
{session_finished, session()} |
{status_changed, status()} .
-export_type([withdrawal/0]).
-export_type([event/0]).
-export([id/1]).
-export([source/1]).
-export([destination/1]).
-export([body/1]).
-export([provider/1]).
-export([transfer/1]).
-export([status/1]).
-export([create/4]).
-export([prepare_transfer/1]).
-export([commit_transfer/1]).
-export([cancel_transfer/1]).
-export([create_session/1]).
-export([poll_session_completion/1]).
%% Event source
-export([apply_event/2]).
%% Pipeline
-import(ff_pipeline, [do/1, unwrap/1, unwrap/2, with/3, valid/2]).
%% Accessors
-spec id(withdrawal()) -> id(binary()).
-spec source(withdrawal()) -> wallet().
-spec destination(withdrawal()) -> destination().
-spec body(withdrawal()) -> body().
-spec provider(withdrawal()) -> provider().
-spec status(withdrawal()) -> status().
-spec transfer(withdrawal()) -> transfer().
id(#{id := V}) -> V.
source(#{source := V}) -> V.
destination(#{destination := V}) -> V.
body(#{body := V}) -> V.
provider(#{provider := V}) -> V.
status(#{status := V}) -> V.
transfer(#{transfer := V}) -> V.
%%
-spec create(id(_), wallet(), destination(), body()) ->
{ok, [event()]} |
{error,
{source, notfound} |
{destination, notfound | unauthorized} |
{provider, notfound} |
_TransferError
}.
create(ID, SourceID, DestinationID, Body) ->
do(fun () ->
Source = ff_wallet_machine:wallet(
unwrap(source, ff_wallet_machine:get(SourceID))
),
Destination = ff_destination_machine:destination(
unwrap(destination, ff_destination_machine:get(DestinationID))
),
ok = unwrap(destination, valid(authorized, ff_destination:status(Destination))),
ProviderID = unwrap(provider, ff_withdrawal_provider:choose(Source, Destination, Body)),
TransferEvents = unwrap(ff_transfer:create(
construct_transfer_id(ID),
[{ff_wallet:account(Source), ff_destination:account(Destination), Body}]
)),
[{created, #{
id => ID,
source => SourceID,
destination => DestinationID,
body => Body,
provider => ProviderID
}}] ++
[{transfer, Ev} || Ev <- TransferEvents] ++
[{status_changed, pending}]
end).
construct_transfer_id(ID) ->
ID.
-spec prepare_transfer(withdrawal()) ->
{ok, [event()]} |
{error, _TransferError}.
prepare_transfer(Withdrawal) ->
with(transfer, Withdrawal, fun ff_transfer:prepare/1).
-spec commit_transfer(withdrawal()) ->
{ok, [event()]} |
{error, _TransferError}.
commit_transfer(Withdrawal) ->
with(transfer, Withdrawal, fun ff_transfer:commit/1).
-spec cancel_transfer(withdrawal()) ->
{ok, [event()]} |
{error, _TransferError}.
cancel_transfer(Withdrawal) ->
with(transfer, Withdrawal, fun ff_transfer:cancel/1).
-spec create_session(withdrawal()) ->
{ok, [event()]} |
{error, _SessionError}.
create_session(Withdrawal) ->
ID = construct_session_id(id(Withdrawal)),
{ok, SourceSt} = ff_wallet_machine:get(source(Withdrawal)),
Source = ff_wallet_machine:wallet(SourceSt),
{ok, DestinationSt} = ff_destination_machine:get(destination(Withdrawal)),
Destination = ff_destination_machine:destination(DestinationSt),
{ok, Provider} = ff_withdrawal_provider:get(provider(Withdrawal)),
{ok, SenderSt} = ff_identity_machine:get(ff_wallet:identity(Source)),
{ok, ReceiverSt} = ff_identity_machine:get(ff_destination:identity(Destination)),
WithdrawalParams = #{
id => ID,
destination => Destination,
cash => body(Withdrawal),
sender => ff_identity_machine:identity(SenderSt),
receiver => ff_identity_machine:identity(ReceiverSt)
},
do(fun () ->
ok = unwrap(ff_withdrawal_provider:create_session(ID, WithdrawalParams, Provider)),
[{session_started, ID}]
end).
construct_session_id(ID) ->
ID.
-spec poll_session_completion(withdrawal()) ->
{ok, [event()]}.
poll_session_completion(_Withdrawal = #{session := SID}) ->
{ok, Session} = ff_withdrawal_session_machine:get(SID),
do(fun () ->
case ff_withdrawal_session_machine:status(Session) of
active ->
[];
{finished, {success, _}} ->
[
{session_finished, SID},
{status_changed, succeeded}
];
{finished, {failed, Failure}} ->
[
{session_finished, SID},
{status_changed, {failed, Failure}}
]
end
end);
poll_session_completion(_Withdrawal) ->
{error, {session, notfound}}.
%%
-spec apply_event(event(), ff_maybe:maybe(withdrawal())) ->
withdrawal().
apply_event({created, W}, undefined) ->
W;
apply_event({status_changed, S}, W) ->
maps:put(status, S, W);
apply_event({transfer, Ev}, W = #{transfer := T}) ->
W#{transfer := ff_transfer:apply_event(Ev, T)};
apply_event({transfer, Ev}, W) ->
apply_event({transfer, Ev}, W#{transfer => undefined});
apply_event({session_started, S}, W) ->
maps:put(session, S, W);
apply_event({session_finished, S}, W = #{session := S}) ->
maps:remove(session, W).

View File

@ -1,204 +0,0 @@
%%%
%%% Withdrawal machine
%%%
-module(ff_withdrawal_machine).
%% API
-type id() :: machinery:id().
-type ctx() :: ff_ctx:ctx().
-type withdrawal() :: ff_withdrawal:withdrawal().
-type activity() ::
prepare_transfer |
create_session |
await_session_completion |
commit_transfer |
cancel_transfer |
undefined .
-type st() ::
ff_machine:st(withdrawal()).
-export_type([id/0]).
-export([create/3]).
-export([get/1]).
-export([events/2]).
%% Accessors
-export([withdrawal/1]).
%% Machinery
-behaviour(machinery).
-export([init/4]).
-export([process_timeout/3]).
-export([process_call/4]).
%% Pipeline
-import(ff_pipeline, [do/1, unwrap/1]).
%%
-define(NS, 'ff/withdrawal_v2').
-type params() :: #{
source := ff_wallet_machine:id(),
destination := ff_destination_machine:id(),
body := ff_transaction:body()
}.
-spec create(id(), params(), ctx()) ->
ok |
{error,
_WithdrawalError |
exists
}.
create(ID, #{source := SourceID, destination := DestinationID, body := Body}, Ctx) ->
do(fun () ->
Events = unwrap(ff_withdrawal:create(ID, SourceID, DestinationID, Body)),
unwrap(machinery:start(?NS, ID, {Events, Ctx}, backend()))
end).
-spec get(id()) ->
{ok, st()} |
{error, notfound}.
get(ID) ->
ff_machine:get(ff_withdrawal, ?NS, ID).
-spec events(id(), machinery:range()) ->
{ok, [{integer(), ff_machine:timestamped_event(event())}]} |
{error, notfound}.
events(ID, Range) ->
do(fun () ->
#{history := History} = unwrap(machinery:get(?NS, ID, Range, backend())),
[{EventID, TsEv} || {EventID, _, TsEv} <- History]
end).
backend() ->
fistful:backend(?NS).
%% Accessors
-spec withdrawal(st()) ->
withdrawal().
withdrawal(St) ->
ff_machine:model(St).
%% Machinery
-type event() ::
ff_withdrawal:event().
-type machine() :: ff_machine:machine(event()).
-type result() :: ff_machine:result(event()).
-type handler_opts() :: machinery:handler_opts(_).
-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_opts()) ->
result().
process_timeout(Machine, _, _Opts) ->
St = ff_machine:collapse(ff_withdrawal, Machine),
process_activity(deduce_activity(withdrawal(St)), St).
process_activity(prepare_transfer, St) ->
case ff_withdrawal:prepare_transfer(withdrawal(St)) of
{ok, Events} ->
#{
events => ff_machine:emit_events(Events),
action => continue
};
{error, Reason} ->
#{
events => emit_failure(Reason)
}
end;
process_activity(create_session, St) ->
case ff_withdrawal:create_session(withdrawal(St)) of
{ok, Events} ->
#{
events => ff_machine:emit_events(Events),
action => set_poll_timer(St)
};
{error, Reason} ->
#{
events => emit_failure(Reason)
}
end;
process_activity(await_session_completion, St) ->
case ff_withdrawal:poll_session_completion(withdrawal(St)) of
{ok, Events} when length(Events) > 0 ->
#{
events => ff_machine:emit_events(Events),
action => continue
};
{ok, []} ->
#{
action => set_poll_timer(St)
}
end;
process_activity(commit_transfer, St) ->
{ok, Events} = ff_withdrawal:commit_transfer(withdrawal(St)),
#{
events => ff_machine:emit_events(Events)
};
process_activity(cancel_transfer, St) ->
{ok, Events} = ff_withdrawal:cancel_transfer(withdrawal(St)),
#{
events => ff_machine:emit_events(Events)
}.
set_poll_timer(St) ->
Now = machinery_time:now(),
Timeout = erlang:max(1, machinery_time:interval(Now, ff_machine:updated(St)) div 1000),
{set_timer, {timeout, Timeout}}.
-spec process_call(_CallArgs, machine(), _, handler_opts()) ->
{ok, result()}.
process_call(_CallArgs, #{}, _, _Opts) ->
{ok, #{}}.
emit_failure(Reason) ->
ff_machine:emit_event({status_changed, {failed, Reason}}).
%%
-spec deduce_activity(withdrawal()) ->
activity().
deduce_activity(#{status := {failed, _}}) ->
cancel_transfer;
deduce_activity(#{status := succeeded}) ->
commit_transfer;
deduce_activity(#{session := _}) ->
await_session_completion;
deduce_activity(#{transfer := #{status := prepared}}) ->
create_session;
deduce_activity(#{transfer := #{status := created}}) ->
prepare_transfer;
deduce_activity(_) ->
undefined.

View File

@ -11,7 +11,7 @@
%%% rid of the `wallet closed` failure but I see no way to do so.
%%%
-module(ff_transfer).
-module(ff_postings_transfer).
-type posting() :: {account(), account(), body()}.

View File

@ -276,7 +276,7 @@ create_destination(Params = #{<<"identity">> := IdenityId}, Context) ->
DestinationId = next_id('destination'),
do(fun() ->
_ = check_resource(identity, IdenityId, Context),
ok = unwrap(ff_destination_machine:create(
ok = unwrap(ff_destination:create(
DestinationId, from_swag(destination_params, Params), make_ctx(Params, [], Context)
)),
unwrap(get_destination(DestinationId, Context))
@ -294,7 +294,7 @@ create_destination(Params = #{<<"identity">> := IdenityId}, Context) ->
create_withdrawal(Params, Context) ->
WithdrawalId = next_id('withdrawal'),
do(fun() ->
ok = unwrap(ff_withdrawal_machine:create(
ok = unwrap(ff_withdrawal:create(
WithdrawalId, from_swag(withdrawal_params, Params), make_ctx(Params, [], Context)
)),
unwrap(get_withdrawal(WithdrawalId, Context))
@ -378,7 +378,7 @@ get_event_type({withdrawal, event}) -> withdrawal_event.
get_collector({identity, challenge_event}, Id) ->
fun(C, L) -> unwrap(ff_identity_machine:events(Id, {C, L, forward})) end;
get_collector({withdrawal, event}, Id) ->
fun(C, L) -> unwrap(ff_withdrawal_machine:events(Id, {C, L, forward})) end.
fun(C, L) -> unwrap(ff_withdrawal:events(Id, {C, L, forward})) end.
collect_events(Collector, Filter, Cursor, Limit) ->
collect_events(Collector, Filter, Cursor, Limit, []).
@ -417,8 +417,8 @@ 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(withdrawal, Id) -> ff_withdrawal_machine:get(Id).
do_get_state(destination, Id) -> ff_destination:get_machine(Id);
do_get_state(withdrawal, Id) -> ff_withdrawal:get_machine(Id).
check_resource(Resource, Id, Context) ->
_ = get_state(Resource, Id, Context),
@ -684,7 +684,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),
@ -719,7 +719,7 @@ to_swag(destination_resource, {bank_card, BankCard}) ->
to_swag(pan_last_digits, MaskedPan) ->
wapi_utils:get_last_pan_digits(MaskedPan);
to_swag(withdrawal, State) ->
Withdrawal = ff_withdrawal_machine:withdrawal(State),
Withdrawal = ff_withdrawal:get(State),
to_swag(map, maps:merge(
#{
<<"id">> => ff_withdrawal:id(Withdrawal),

View File

@ -31,6 +31,7 @@
{providers, #{
<<"ncoeps">> => #{
payment_institution_id => 100,
routes => [<<"mocketbank">>],
identity_classes => #{
<<"person">> => #{
name => <<"Person">>,
@ -68,15 +69,17 @@
}}
]},
{ff_withdraw, [
{provider, #{
<<"mocketbank">> => #{
adapter => #{
event_handler => scoper_woody_event_handler,
url => <<"http://adapter-mocketbank:8022/proxy/mocketbank/p2p-credit">>
},
adapter_opts => #{}
}
{ff_transfer, [
{withdrawal,
#{provider => #{
<<"mocketbank">> => #{
adapter => #{
event_handler => scoper_woody_event_handler,
url => <<"http://adapter-mocketbank:8022/proxy/mocketbank/p2p-credit">>
},
adapter_opts => #{}
}
}
}}
]},
@ -123,6 +126,10 @@
{services, #{
'automaton' => "http://machinegun:8022/v1/automaton"
}},
{admin, #{
%% handler_limits => #{},
path => <<"/v1/admin">>
}},
{net_opts, [
% Bump keepalive timeout up to a minute
{timeout, 60000}

View File

@ -3,7 +3,7 @@
% mandatory
debug_info,
% warnings_as_errors,
warnings_as_errors,
warn_export_all,
warn_missing_spec,
warn_untyped_record,
@ -72,6 +72,9 @@
},
{identdocstore_proto,
{git, "git@github.com:rbkmoney/identdocstore-proto.git", {branch, "master"}}
},
{fistful_proto,
{git, "git@github.com:rbkmoney/fistful-proto.git", {branch, "master"}}
}
]}.

View File

@ -36,6 +36,10 @@
{git,"https://github.com/kpy3/erlang_localtime",
{ref,"c79fa7dd454343e7cbbdcce0c7a95ad86af1485d"}},
0},
{<<"fistful_proto">>,
{git,"git@github.com:rbkmoney/fistful-proto.git",
{ref,"060ea5b25935f974f4cb2a0e233083ebd9013017"}},
0},
{<<"genlib">>,
{git,"https://github.com/rbkmoney/genlib.git",
{ref,"e9e5aed04a870a064312590e798f89d46ce5585c"}},

View File

@ -26,6 +26,15 @@ namespaces:
ff/wallet_v2:
processor:
url: http://fistful-server:8022/v1/stateproc/ff/wallet
ff/source_v1:
processor:
url: http://fistful-server:8022/v1/stateproc/ff/source
ff/deposit_v1:
processor:
url: http://fistful-server:8022/v1/stateproc/ff/deposit
ff/deposit/session_v1:
processor:
url: http://fistful-server:8022/v1/stateproc/ff/deposit/session
ff/destination_v2:
processor:
url: http://fistful-server:8022/v1/stateproc/ff/destination