mirror of
https://github.com/valitydev/wapi-lib.git
synced 2024-11-07 02:35:18 +00:00
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:
parent
ddf5630f5b
commit
9f27ef35ef
@ -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, []}]}
|
||||
]}
|
||||
]), #{}};
|
||||
|
||||
|
@ -13,7 +13,8 @@
|
||||
lager,
|
||||
scoper,
|
||||
fistful,
|
||||
ff_withdraw,
|
||||
ff_transfer,
|
||||
fistful_proto,
|
||||
wapi
|
||||
]},
|
||||
{env, []},
|
||||
|
@ -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
|
||||
})).
|
||||
|
144
apps/ff_server/src/ff_server_handler.erl
Normal file
144
apps/ff_server/src/ff_server_handler.erl
Normal 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))
|
||||
).
|
134
apps/ff_transfer/src/ff_deposit.erl
Normal file
134
apps/ff_transfer/src/ff_deposit.erl
Normal 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).
|
103
apps/ff_transfer/src/ff_destination.erl
Normal file
103
apps/ff_transfer/src/ff_destination.erl
Normal 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).
|
140
apps/ff_transfer/src/ff_instrument.erl
Normal file
140
apps/ff_transfer/src/ff_instrument.erl
Normal 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}).
|
@ -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)
|
97
apps/ff_transfer/src/ff_source.erl
Normal file
97
apps/ff_transfer/src/ff_source.erl
Normal 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).
|
@ -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"]}
|
196
apps/ff_transfer/src/ff_transfer.erl
Normal file
196
apps/ff_transfer/src/ff_transfer.erl
Normal 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.
|
165
apps/ff_transfer/src/ff_transfer_machine.erl
Normal file
165
apps/ff_transfer/src/ff_transfer_machine.erl
Normal 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}}).
|
177
apps/ff_transfer/src/ff_withdrawal.erl
Normal file
177
apps/ff_transfer/src/ff_withdrawal.erl
Normal 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).
|
@ -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, #{}).
|
@ -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].
|
||||
|
@ -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").
|
@ -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}).
|
@ -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).
|
@ -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.
|
@ -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()}.
|
||||
|
@ -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),
|
||||
|
@ -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}
|
||||
|
@ -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"}}
|
||||
}
|
||||
]}.
|
||||
|
||||
|
@ -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"}},
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user