diff --git a/apps/ff_cth/src/ct_helper.erl b/apps/ff_cth/src/ct_helper.erl index 188004a..f922c03 100644 --- a/apps/ff_cth/src/ct_helper.erl +++ b/apps/ff_cth/src/ct_helper.erl @@ -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, []}]} ]} ]), #{}}; diff --git a/apps/ff_server/src/ff_server.app.src b/apps/ff_server/src/ff_server.app.src index 6fb136d..1277600 100644 --- a/apps/ff_server/src/ff_server.app.src +++ b/apps/ff_server/src/ff_server.app.src @@ -13,7 +13,8 @@ lager, scoper, fistful, - ff_withdraw, + ff_transfer, + fistful_proto, wapi ]}, {env, []}, diff --git a/apps/ff_server/src/ff_server.erl b/apps/ff_server/src/ff_server.erl index cb54a4a..196a389 100644 --- a/apps/ff_server/src/ff_server.erl +++ b/apps/ff_server/src/ff_server.erl @@ -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 + })). diff --git a/apps/ff_server/src/ff_server_handler.erl b/apps/ff_server/src/ff_server_handler.erl new file mode 100644 index 0000000..7563c82 --- /dev/null +++ b/apps/ff_server/src/ff_server_handler.erl @@ -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)) + ). diff --git a/apps/ff_withdraw/rebar.config b/apps/ff_transfer/rebar.config similarity index 100% rename from apps/ff_withdraw/rebar.config rename to apps/ff_transfer/rebar.config diff --git a/apps/ff_withdraw/src/ff_adapter.erl b/apps/ff_transfer/src/ff_adapter.erl similarity index 100% rename from apps/ff_withdraw/src/ff_adapter.erl rename to apps/ff_transfer/src/ff_adapter.erl diff --git a/apps/ff_withdraw/src/ff_adapter_withdrawal.erl b/apps/ff_transfer/src/ff_adapter_withdrawal.erl similarity index 100% rename from apps/ff_withdraw/src/ff_adapter_withdrawal.erl rename to apps/ff_transfer/src/ff_adapter_withdrawal.erl diff --git a/apps/ff_transfer/src/ff_deposit.erl b/apps/ff_transfer/src/ff_deposit.erl new file mode 100644 index 0000000..41b1133 --- /dev/null +++ b/apps/ff_transfer/src/ff_deposit.erl @@ -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). diff --git a/apps/ff_transfer/src/ff_destination.erl b/apps/ff_transfer/src/ff_destination.erl new file mode 100644 index 0000000..e22e5d9 --- /dev/null +++ b/apps/ff_transfer/src/ff_destination.erl @@ -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). diff --git a/apps/ff_transfer/src/ff_instrument.erl b/apps/ff_transfer/src/ff_instrument.erl new file mode 100644 index 0000000..dbbc035 --- /dev/null +++ b/apps/ff_transfer/src/ff_instrument.erl @@ -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}). diff --git a/apps/ff_withdraw/src/ff_destination_machine.erl b/apps/ff_transfer/src/ff_instrument_machine.erl similarity index 53% rename from apps/ff_withdraw/src/ff_destination_machine.erl rename to apps/ff_transfer/src/ff_instrument_machine.erl index df26e12..44b4605 100644 --- a/apps/ff_withdraw/src/ff_destination_machine.erl +++ b/apps/ff_transfer/src/ff_instrument_machine.erl @@ -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) diff --git a/apps/ff_transfer/src/ff_source.erl b/apps/ff_transfer/src/ff_source.erl new file mode 100644 index 0000000..6ab404d --- /dev/null +++ b/apps/ff_transfer/src/ff_source.erl @@ -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). diff --git a/apps/ff_withdraw/src/ff_withdraw.app.src b/apps/ff_transfer/src/ff_transfer.app.src similarity index 70% rename from apps/ff_withdraw/src/ff_withdraw.app.src rename to apps/ff_transfer/src/ff_transfer.app.src index e0f83d2..3406209 100644 --- a/apps/ff_withdraw/src/ff_withdraw.app.src +++ b/apps/ff_transfer/src/ff_transfer.app.src @@ -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 " + "Andrey Mayorov ", + "Anton Belyaev " ]}, {licenses, []}, {links, ["https://github.com/rbkmoney/fistful-server"]} diff --git a/apps/ff_transfer/src/ff_transfer.erl b/apps/ff_transfer/src/ff_transfer.erl new file mode 100644 index 0000000..1a482a1 --- /dev/null +++ b/apps/ff_transfer/src/ff_transfer.erl @@ -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. diff --git a/apps/ff_transfer/src/ff_transfer_machine.erl b/apps/ff_transfer/src/ff_transfer_machine.erl new file mode 100644 index 0000000..d38bec5 --- /dev/null +++ b/apps/ff_transfer/src/ff_transfer_machine.erl @@ -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}}). diff --git a/apps/ff_transfer/src/ff_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl new file mode 100644 index 0000000..3f40d04 --- /dev/null +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -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). diff --git a/apps/ff_withdraw/src/ff_withdrawal_provider.erl b/apps/ff_transfer/src/ff_withdrawal_provider.erl similarity index 58% rename from apps/ff_withdraw/src/ff_withdrawal_provider.erl rename to apps/ff_transfer/src/ff_withdrawal_provider.erl index 04d49a5..4694c23 100644 --- a/apps/ff_withdraw/src/ff_withdrawal_provider.erl +++ b/apps/ff_transfer/src/ff_withdrawal_provider.erl @@ -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, #{}). diff --git a/apps/ff_withdraw/src/ff_withdrawal_session_machine.erl b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl similarity index 70% rename from apps/ff_withdraw/src/ff_withdrawal_session_machine.erl rename to apps/ff_transfer/src/ff_withdrawal_session_machine.erl index a5dd0f2..75cd0b6 100644 --- a/apps/ff_withdraw/src/ff_withdrawal_session_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl @@ -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]. - diff --git a/apps/ff_withdraw/test/ff_withdrawal_SUITE.erl b/apps/ff_transfer/test/ff_transfer_SUITE.erl similarity index 56% rename from apps/ff_withdraw/test/ff_withdrawal_SUITE.erl rename to apps/ff_transfer/test/ff_transfer_SUITE.erl index b8c197f..fa0517d 100644 --- a/apps/ff_withdraw/test/ff_withdrawal_SUITE.erl +++ b/apps/ff_transfer/test/ff_transfer_SUITE.erl @@ -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"). diff --git a/apps/ff_withdraw/src/ff_destination.erl b/apps/ff_withdraw/src/ff_destination.erl deleted file mode 100644 index d879020..0000000 --- a/apps/ff_withdraw/src/ff_destination.erl +++ /dev/null @@ -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}). diff --git a/apps/ff_withdraw/src/ff_withdrawal.erl b/apps/ff_withdraw/src/ff_withdrawal.erl deleted file mode 100644 index 153a1b1..0000000 --- a/apps/ff_withdraw/src/ff_withdrawal.erl +++ /dev/null @@ -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). diff --git a/apps/ff_withdraw/src/ff_withdrawal_machine.erl b/apps/ff_withdraw/src/ff_withdrawal_machine.erl deleted file mode 100644 index 00265f8..0000000 --- a/apps/ff_withdraw/src/ff_withdrawal_machine.erl +++ /dev/null @@ -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. diff --git a/apps/fistful/src/ff_transfer.erl b/apps/fistful/src/ff_postings_transfer.erl similarity index 99% rename from apps/fistful/src/ff_transfer.erl rename to apps/fistful/src/ff_postings_transfer.erl index cf20986..8193008 100644 --- a/apps/fistful/src/ff_transfer.erl +++ b/apps/fistful/src/ff_postings_transfer.erl @@ -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()}. diff --git a/apps/wapi/src/wapi_wallet_ff_backend.erl b/apps/wapi/src/wapi_wallet_ff_backend.erl index 88aba44..6dfc913 100644 --- a/apps/wapi/src/wapi_wallet_ff_backend.erl +++ b/apps/wapi/src/wapi_wallet_ff_backend.erl @@ -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), diff --git a/config/sys.config b/config/sys.config index 79a1469..c804c19 100644 --- a/config/sys.config +++ b/config/sys.config @@ -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} diff --git a/rebar.config b/rebar.config index 7cb3973..1a363b3 100644 --- a/rebar.config +++ b/rebar.config @@ -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"}} } ]}. diff --git a/rebar.lock b/rebar.lock index 3c6a56b..e16967b 100644 --- a/rebar.lock +++ b/rebar.lock @@ -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"}}, diff --git a/test/machinegun/config.yaml b/test/machinegun/config.yaml index 6094c6b..53c1757 100644 --- a/test/machinegun/config.yaml +++ b/test/machinegun/config.yaml @@ -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