From 7c7ad60e1b6663f05c6593cf9d2a5d66fc09bf22 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Mon, 16 Jul 2018 17:21:17 +0300 Subject: [PATCH] Handle transfers through noion of accounts (yeah, again) (#9) * Drop (de)hydration for now, we'll think about it later. * Reduce boilerplate w/ the help of `ff_machine` though much to be done still. * Drop half-baked `ff_machine` from ff_core * Supply missing specs + fix marshalling types * Update test fixtures --- README.md | 17 +- apps/ff_core/src/ff_machine.erl | 34 --- apps/ff_cth/src/ct_identdocstore.erl | 6 + apps/ff_withdraw/src/ff_destination.erl | 141 +++++------ .../src/ff_destination_machine.erl | 132 +++------- apps/ff_withdraw/src/ff_withdrawal.erl | 200 ++++++--------- .../ff_withdraw/src/ff_withdrawal_machine.erl | 177 +++++-------- .../src/ff_withdrawal_provider.erl | 15 +- .../src/ff_withdrawal_session_machine.erl | 6 +- apps/ff_withdraw/test/ff_withdrawal_SUITE.erl | 16 +- apps/fistful/src/ff_account.erl | 129 ++++++++++ apps/fistful/src/ff_identity.erl | 233 ++++++++---------- apps/fistful/src/ff_identity_challenge.erl | 94 ++++--- apps/fistful/src/ff_identity_class.erl | 20 +- apps/fistful/src/ff_identity_machine.erl | 158 ++++-------- apps/fistful/src/ff_machine.erl | 161 ++++++++++++ apps/fistful/src/ff_party.erl | 23 +- apps/fistful/src/ff_provider.erl | 38 +-- apps/fistful/src/ff_transaction.erl | 2 + apps/fistful/src/ff_transfer.erl | 163 ++++++------ apps/fistful/src/ff_wallet.erl | 174 ++++--------- apps/fistful/src/ff_wallet_machine.erl | 93 ++----- apps/fistful/test/ff_wallet_SUITE.erl | 28 +-- apps/wapi/src/wapi_swagger_server.erl | 7 +- apps/wapi/src/wapi_wallet_ff_backend.erl | 70 +++--- docker-compose.sh | 4 +- 26 files changed, 1010 insertions(+), 1131 deletions(-) delete mode 100644 apps/ff_core/src/ff_machine.erl create mode 100644 apps/fistful/src/ff_account.erl create mode 100644 apps/fistful/src/ff_machine.erl diff --git a/README.md b/README.md index aac8d94..c9d6782 100644 --- a/README.md +++ b/README.md @@ -10,20 +10,25 @@ * [x] Реализовать честный identity challenge * [x] Запилить payment provider interface * [ ] Запилить контактные данные личности -* [ ] Запилить отмену identity challenge -* [ ] Запилить авторизацию по активной идентификации +* [x] Запилить нормально трансферы +* [ ] Заворачивать изменения в единственный ивент в рамках операции +* [.] Компактизировать состояние сессий * [ ] Запилить контроль лимитов по кошелькам +* [ ] Запилить авторизацию по активной идентификации +* [ ] Запилить отмену identity challenge * [ ] Запускать выводы через оплату инвойса провайдеру выводов * [ ] Обслуживать выводы по факту оплаты инвойса ### Корректность * [.] Схема хранения моделей +* [ ] [Дегидратация](#дегидратация) * [ ] [Поддержка checkout](#поддержка-checkout) * [ ] [Коммуналка](#коммуналка) ### Удобство поддержки +* [ ] Добавить [служебные лимиты](#служебные-лимиты) в рамках одного party * [ ] Добавить ручную прополку для всех асинхронных процессов * [ ] Вынести _ff_withdraw_ в отдельный сервис * [ ] Разделить _development_, _release_ и _test_ зависимости @@ -36,3 +41,11 @@ ## Коммуналка Сервис должен давать возможность работать _нескольким_ клиентам, которые возможно не знают ничего друг о друге кроме того, что у них разные _tenant id_. В идеале _tenant_ должен иметь возможность давать знать о себе _динамически_, в рантайме, однако это довольно трудоёмкая задача. Если приводить аналогию с _Riak KV_, клиенты к нему могут: создать новый _bucket type_ с необходимыми характеристиками, создать новый _bucket_ с требуемыми параметрами N/R/W и так далее. + +## Дегидратация + +В итоге как будто бы не самая здравая идея. Есть ощущение, что проще и дешевле хранить и оперировать идентификаторами, и разыменовывать их каждый раз по необходимости. + +## Служебные лимиты + +Нужно уметь _ограничивать_ максимальное _ожидаемое_ количество тех или иных объектов, превышение которого может негативно влиять на качество обслуживания системы. Например, мы можем считать количество _выводов_ одним участником неограниченным, однако при этом неограниченное количество созданных _личностей_ мы совершенно не ожидаем. В этом случае возможно будет разумно ограничить их количество сверху труднодостижимой для подавляющего большинства планкой, например, в 1000 объектов. В идеале подобное должно быть точечно конфигурируемым. diff --git a/apps/ff_core/src/ff_machine.erl b/apps/ff_core/src/ff_machine.erl deleted file mode 100644 index 0841d83..0000000 --- a/apps/ff_core/src/ff_machine.erl +++ /dev/null @@ -1,34 +0,0 @@ -%%% -%%% Fistful machine generic accessors. -%%% - --module(ff_machine). - - --type timestamp() :: machinery:timestamp(). --type ctx() :: ff_ctx:ctx(). - --type st() ::#{ - ctx := ctx(), - times => {timestamp(), timestamp()}, - _ => _ -}. --export_type([st/0]). - -%% Accessors API --export([ctx/1]). --export([created/1]). --export([updated/1]). - -%% Accessors - --spec ctx(st()) -> ctx(). --spec created(st()) -> timestamp() | undefined. --spec updated(st()) -> timestamp() | undefined. - -ctx(#{ctx := V}) -> V. -created(St) -> erlang:element(1, times(St)). -updated(St) -> erlang:element(2, times(St)). - -times(St) -> - genlib_map:get(times, St, {undefined, undefined}). diff --git a/apps/ff_cth/src/ct_identdocstore.erl b/apps/ff_cth/src/ct_identdocstore.erl index 1d49b52..9bfd331 100644 --- a/apps/ff_cth/src/ct_identdocstore.erl +++ b/apps/ff_cth/src/ct_identdocstore.erl @@ -7,6 +7,9 @@ -include_lib("identdocstore_proto/include/identdocstore_identity_document_storage_thrift.hrl"). +-spec rus_domestic_passport(ct_helper:config()) -> + {rus_domestic_passport, binary()}. + rus_domestic_passport(C) -> Document = { russian_domestic_passport, @@ -31,6 +34,9 @@ rus_domestic_passport(C) -> {rus_domestic_passport, Token} end. +-spec rus_retiree_insurance_cert(_Number :: binary(), ct_helper:config()) -> + {rus_retiree_insurance_cert, binary()}. + rus_retiree_insurance_cert(Number, C) -> Document = { russian_retiree_insurance_certificate, diff --git a/apps/ff_withdraw/src/ff_destination.erl b/apps/ff_withdraw/src/ff_destination.erl index 59d810b..afb69fb 100644 --- a/apps/ff_withdraw/src/ff_destination.erl +++ b/apps/ff_withdraw/src/ff_destination.erl @@ -9,11 +9,14 @@ -module(ff_destination). --type id() :: machinery:id(). --type wallet() :: ff_wallet:wallet(). +-type account() :: ff_account:account(). -type resource() :: {bank_card, resource_bank_card()}. +-type id(T) :: T. +-type identity() :: ff_identity:id(). +-type currency() :: ff_currency:id(). + -type resource_bank_card() :: #{ token := binary(), payment_system => atom(), % TODO @@ -26,75 +29,92 @@ authorized. -type destination() :: #{ - id := id(), + account := account() | undefined, resource := resource(), - wallet => wallet(), - status => status() + name := binary(), + status := status() }. --type ev() :: +-type event() :: {created, destination()} | - {wallet, ff_wallet:ev()} | + {account, ff_account:ev()} | {status_changed, status()}. --type outcome() :: [ev()]. - -export_type([destination/0]). -export_type([status/0]). -export_type([resource/0]). --export_type([ev/0]). +-export_type([event/0]). + +-export([account/1]). -export([id/1]). --export([wallet/1]). +-export([name/1]). +-export([identity/1]). +-export([currency/1]). -export([resource/1]). -export([status/1]). -export([create/5]). -export([authorize/1]). --export([apply_event/2]). +-export([is_accessible/1]). --export([dehydrate/1]). --export([hydrate/2]). +-export([apply_event/2]). %% Pipeline --import(ff_pipeline, [do/1, unwrap/1, unwrap/2]). +-import(ff_pipeline, [do/1, unwrap/1]). %% Accessors --spec id(destination()) -> id(). --spec wallet(destination()) -> wallet(). --spec resource(destination()) -> resource(). --spec status(destination()) -> status(). +-spec account(destination()) -> + account(). -id(#{id := V}) -> V. -wallet(#{wallet := V}) -> V. -resource(#{resource := V}) -> V. -status(#{status := V}) -> V. +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(), ff_identity:identity(), binary(), ff_currency:id(), resource()) -> - {ok, outcome()} | +-spec create(id(_), identity(), binary(), currency(), resource()) -> + {ok, [event()]} | {error, _WalletError}. -create(ID, Identity, Name, Currency, Resource) -> +create(ID, IdentityID, Name, CurrencyID, Resource) -> do(fun () -> - WalletEvents1 = unwrap(ff_wallet:create(ID, Identity, Name, Currency)), - WalletEvents2 = unwrap(ff_wallet:setup_wallet(ff_wallet:collapse_events(WalletEvents1))), - [ - {created, #{ - id => ID, - resource => Resource - }} - ] ++ - [{wallet, Ev} || Ev <- WalletEvents1 ++ WalletEvents2] ++ + Events = unwrap(ff_account:create(ID, IdentityID, CurrencyID)), + [{created, #{name => Name, resource => Resource}}] ++ + [{account, Ev} || Ev <- Events] ++ [{status_changed, unauthorized}] end). -spec authorize(destination()) -> - {ok, outcome()} | + {ok, [event()]} | {error, _TODO}. authorize(#{status := unauthorized}) -> @@ -104,40 +124,23 @@ authorize(#{status := unauthorized}) -> 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(ev(), undefined | destination()) -> +-spec apply_event(event(), ff_maybe:maybe(destination())) -> destination(). -apply_event({created, D}, undefined) -> - D; -apply_event({status_changed, S}, D) -> - D#{status => S}; -apply_event({wallet, Ev}, D) -> - D#{wallet => ff_wallet:apply_event(Ev, genlib_map:get(wallet, D))}. - --spec dehydrate(ev()) -> - term(). - --spec hydrate(term(), undefined | destination()) -> - ev(). - -dehydrate({created, D}) -> - {created, #{ - id => id(D), - resource => resource(D) - }}; -dehydrate({wallet, Ev}) -> - {wallet, ff_wallet:dehydrate(Ev)}; -dehydrate({status_changed, S}) -> - {status_changed, S}. - -hydrate({created, V}, undefined) -> - {created, #{ - id => maps:get(id, V), - resource => maps:get(resource, V) - }}; -hydrate({wallet, Ev}, D) -> - {wallet, ff_wallet:hydrate(Ev, genlib_map:get(wallet, D))}; -hydrate({status_changed, S}, _) -> - {status_changed, S}. +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_destination_machine.erl b/apps/ff_withdraw/src/ff_destination_machine.erl index 744fda9..3994a19 100644 --- a/apps/ff_withdraw/src/ff_destination_machine.erl +++ b/apps/ff_withdraw/src/ff_destination_machine.erl @@ -7,20 +7,11 @@ %% API -type id() :: machinery:id(). --type timestamp() :: machinery:timestamp(). -type ctx() :: ff_ctx:ctx(). -type destination() :: ff_destination:destination(). --type activity() :: - undefined | - authorize . - --type st() :: #{ - activity := activity(), - destination := destination(), - ctx := ctx(), - times => {timestamp(), timestamp()} -}. +-type st() :: + ff_machine:st(destination()). -export_type([id/0]). @@ -30,10 +21,6 @@ %% Accessors -export([destination/1]). --export([activity/1]). --export([ctx/1]). --export([created/1]). --export([updated/1]). %% Machinery @@ -45,14 +32,14 @@ %% Pipeline --import(ff_pipeline, [do/1, unwrap/1, unwrap/2]). +-import(ff_pipeline, [do/1, unwrap/1]). %% -define(NS, 'ff/destination'). -type params() :: #{ - identity := ff_identity_machine:id(), + identity := ff_identity:id(), name := binary(), currency := ff_currency:id(), resource := ff_destination:resource() @@ -61,18 +48,14 @@ -spec create(id(), params(), ctx()) -> ok | {error, - {identity, notfound} | - {currency, notfound} | - _DestinationError | + _DestinationCreateError | exists }. -create(ID, #{identity := IdentityID, name := Name, currency := Currency, resource := Resource}, Ctx) -> +create(ID, #{identity := IdentityID, name := Name, currency := CurrencyID, resource := Resource}, Ctx) -> do(fun () -> - Identity = ff_identity_machine:identity(unwrap(identity, ff_identity_machine:get(IdentityID))), - _ = unwrap(currency, ff_currency:get(Currency)), - Events = unwrap(ff_destination:create(ID, Identity, Name, Currency, Resource)), - unwrap(machinery:start(?NS, ID, {Events, Ctx}, backend())) + Events = unwrap(ff_destination:create(ID, IdentityID, Name, CurrencyID, Resource)), + unwrap(machinery:start(?NS, ID, {Events, Ctx}, fistful:backend(?NS))) end). -spec get(id()) -> @@ -80,113 +63,62 @@ create(ID, #{identity := IdentityID, name := Name, currency := Currency, resourc {error, notfound} . get(ID) -> - do(fun () -> - collapse(unwrap(machinery:get(?NS, ID, backend()))) - end). - -backend() -> - fistful:backend(?NS). + ff_machine:get(ff_destination, ?NS, ID). %% Accessors --spec destination(st()) -> destination(). --spec activity(st()) -> activity(). --spec ctx(st()) -> ctx(). --spec created(st()) -> timestamp() | undefined. --spec updated(st()) -> timestamp() | undefined. +-spec destination(st()) -> + destination(). -destination(#{destination := V}) -> V. -activity(#{activity := V}) -> V. -ctx(#{ctx := V}) -> V. -created(St) -> erlang:element(1, times(St)). -updated(St) -> erlang:element(2, times(St)). - -times(St) -> - genlib_map:get(times, St, {undefined, undefined}). +destination(St) -> + ff_machine:model(St). %% Machinery --type ts_ev(T) :: - {ev, timestamp(), T}. +-type event() :: + ff_destination:event(). --type ev() :: - ff_destination:ev(). - --type auxst() :: - #{ctx => ctx()}. - --type machine() :: machinery:machine(ts_ev(ev()), auxst()). --type result() :: machinery:result(ts_ev(ev()), auxst()). +-type machine() :: ff_machine:machine(event()). +-type result() :: ff_machine:result(event()). -type handler_opts() :: machinery:handler_opts(_). --spec init({[ev()], ctx()}, machine(), _, handler_opts()) -> +-spec init({[event()], ctx()}, machine(), _, handler_opts()) -> result(). init({Events, Ctx}, #{}, _, _Opts) -> #{ - events => emit_ts_events(Events), + events => ff_machine:emit_events(Events), action => continue, aux_state => #{ctx => Ctx} }. +%% + -spec process_timeout(machine(), _, handler_opts()) -> result(). process_timeout(Machine, _, _Opts) -> - process_timeout(collapse(Machine)). + St = ff_machine:collapse(ff_destination, Machine), + process_timeout(deduce_activity(ff_machine:model(St)), St). -process_timeout(#{activity := authorize} = St) -> +process_timeout(authorize, St) -> D0 = destination(St), case ff_destination:authorize(D0) of {ok, Events} -> #{ - events => emit_ts_events(Events) + events => ff_machine:emit_events(Events) } end. +deduce_activity(#{status := unauthorized}) -> + authorize; +deduce_activity(#{}) -> + undefined. + +%% + -spec process_call(_CallArgs, machine(), _, handler_opts()) -> {ok, result()}. process_call(_CallArgs, #{}, _, _Opts) -> {ok, #{}}. - -%% - -collapse(#{history := History, aux_state := #{ctx := Ctx}}) -> - collapse_history(History, #{ctx => Ctx}). - -collapse_history(History, St) -> - lists:foldl(fun merge_event/2, St, History). - -merge_event({_ID, _Ts, TsEv}, St0) -> - {EvBody, St1} = merge_ts_event(TsEv, St0), - merge_event_body(EvBody, St1). - -merge_event_body(Ev, St) -> - Destination = genlib_map:get(destination, St), - St#{ - activity => deduce_activity(Ev), - destination => ff_destination:apply_event(ff_destination:hydrate(Ev, Destination), Destination) - }. - -deduce_activity({created, _}) -> - undefined; -deduce_activity({wallet, _}) -> - undefined; -deduce_activity({status_changed, unauthorized}) -> - authorize; -deduce_activity({status_changed, authorized}) -> - undefined. - -%% - -emit_ts_events(Es) -> - emit_ts_events(Es, machinery_time:now()). - -emit_ts_events(Es, Ts) -> - [{ev, Ts, ff_destination:dehydrate(Body)} || Body <- Es]. - -merge_ts_event({ev, Ts, Body}, St = #{times := {Created, _Updated}}) -> - {Body, St#{times => {Created, Ts}}}; -merge_ts_event({ev, Ts, Body}, St = #{}) -> - {Body, St#{times => {Ts, Ts}}}. diff --git a/apps/ff_withdraw/src/ff_withdrawal.erl b/apps/ff_withdraw/src/ff_withdrawal.erl index 3adbf22..bad3cd7 100644 --- a/apps/ff_withdraw/src/ff_withdrawal.erl +++ b/apps/ff_withdraw/src/ff_withdrawal.erl @@ -5,19 +5,19 @@ -module(ff_withdrawal). -type id(T) :: T. --type wallet() :: ff_wallet:wallet(). --type destination() :: ff_destination:destination(). +-type wallet() :: ff_wallet:id(_). +-type destination() :: ff_destination:id(_). -type body() :: ff_transaction:body(). --type provider() :: ff_withdrawal_provider:provider(). +-type provider() :: ff_withdrawal_provider:id(). -type transfer() :: ff_transfer:transfer(). -type withdrawal() :: #{ - id := id(_), + id := id(binary()), source := wallet(), destination := destination(), body := body(), provider := provider(), - transfer => transfer(), + transfer := ff_maybe:maybe(transfer()), session => session(), status => status() }. @@ -30,17 +30,15 @@ succeeded | {failed, _TODO} . --type ev() :: +-type event() :: {created, withdrawal()} | {transfer, ff_transfer:ev()} | {session_started, session()} | {session_finished, session()} | {status_changed, status()} . --type outcome() :: [ev()]. - -export_type([withdrawal/0]). --export_type([ev/0]). +-export_type([event/0]). -export([id/1]). -export([source/1]). @@ -50,8 +48,7 @@ -export([transfer/1]). -export([status/1]). --export([create/5]). --export([create_transfer/1]). +-export([create/4]). -export([prepare_transfer/1]). -export([commit_transfer/1]). -export([cancel_transfer/1]). @@ -60,25 +57,21 @@ %% Event source --export([collapse_events/1]). -export([apply_event/2]). --export([dehydrate/1]). --export([hydrate/2]). - %% Pipeline --import(ff_pipeline, [do/1, unwrap/1, unwrap/2, with/3]). +-import(ff_pipeline, [do/1, unwrap/1, unwrap/2, with/3, valid/2]). %% Accessors --spec id(withdrawal()) -> id(_). +-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()) -> {ok, transfer()} | {error | notfound}. +-spec transfer(withdrawal()) -> transfer(). id(#{id := V}) -> V. source(#{source := V}) -> V. @@ -86,70 +79,98 @@ destination(#{destination := V}) -> V. body(#{body := V}) -> V. provider(#{provider := V}) -> V. status(#{status := V}) -> V. -transfer(W) -> ff_map:find(transfer, W). +transfer(#{transfer := V}) -> V. %% --spec create(id(_), wallet(), destination(), body(), provider()) -> - {ok, outcome()}. +-spec create(id(_), wallet(), destination(), body()) -> + {ok, [event()]} | + {error, + {source, notfound} | + {destination, notfound | unauthorized} | + {provider, notfound} | + _TransferError + }. -create(ID, Source, Destination, Body, Provider) -> +create(ID, SourceID, DestinationID, Body) -> do(fun () -> - [ - {created, #{ - id => ID, - source => Source, - destination => Destination, - body => Body, - provider => Provider - }}, - {status_changed, - pending - } - ] + 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), + [{{wallet, SourceID}, {destination, DestinationID}, Body}] + )), + [{created, #{ + id => ID, + source => SourceID, + destination => DestinationID, + body => Body, + provider => ProviderID + }}] ++ + [{transfer, Ev} || Ev <- TransferEvents] ++ + [{status_changed, pending}] end). -create_transfer(Withdrawal) -> - Source = source(Withdrawal), - Destination = ff_destination:wallet(destination(Withdrawal)), - TrxID = construct_transfer_id(id(Withdrawal)), - Posting = {Source, Destination, body(Withdrawal)}, - do(fun () -> - Events = unwrap(transfer, ff_transfer:create(TrxID, [Posting])), - [{transfer, Ev} || Ev <- Events] - end). +construct_transfer_id(ID) -> + ID. -construct_transfer_id(TrxID) -> - ff_string:join($/, [TrxID, transfer]). +-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) -> - SID = construct_session_id(id(Withdrawal)), - Source = source(Withdrawal), - Destination = destination(Withdrawal), - Provider = provider(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 => SID, + id => ID, destination => Destination, cash => body(Withdrawal), - sender => ff_wallet:identity(Source), - receiver => ff_wallet:identity(ff_destination:wallet(Destination)) + sender => ff_identity_machine:identity(SenderSt), + receiver => ff_identity_machine:identity(ReceiverSt) }, do(fun () -> - ok = unwrap(ff_withdrawal_provider:create_session(SID, WithdrawalParams, Provider)), - [{session_started, SID}] + ok = unwrap(ff_withdrawal_provider:create_session(ID, WithdrawalParams, Provider)), + [{session_started, ID}] end). -construct_session_id(TrxID) -> - TrxID. +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), @@ -174,77 +195,18 @@ poll_session_completion(_Withdrawal) -> %% --spec collapse_events([ev(), ...]) -> - withdrawal(). - -collapse_events(Evs) when length(Evs) > 0 -> - apply_events(Evs, undefined). - --spec apply_events([ev()], undefined | withdrawal()) -> - undefined | withdrawal(). - -apply_events(Evs, Identity) -> - lists:foldl(fun apply_event/2, Identity, Evs). - --spec apply_event(ev(), undefined | withdrawal()) -> +-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) -> - maps:update_with(transfer, fun (T) -> ff_transfer:apply_event(Ev, T) end, maps:merge(#{transfer => undefined}, 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). - -%% - --spec dehydrate(ev()) -> - term(). - --spec hydrate(term(), undefined | withdrawal()) -> - ev(). - -dehydrate({created, W}) -> - {created, #{ - id => id(W), - source => ff_wallet:id(source(W)), - destination => ff_destination:id(destination(W)), - body => body(W), - provider => ff_withdrawal_provider:id(provider(W)) - }}; -dehydrate({status_changed, S}) -> - {status_changed, S}; -dehydrate({transfer, Ev}) -> - % TODO - % - `ff_transfer:dehydrate(Ev)` - {transfer, Ev}; -dehydrate({session_started, SID}) -> - {session_started, SID}; -dehydrate({session_finished, SID}) -> - {session_finished, SID}. - -hydrate({created, V}, undefined) -> - {ok, SourceSt} = ff_wallet_machine:get(maps:get(source, V)), - {ok, DestinationSt} = ff_destination_machine:get(maps:get(destination, V)), - {ok, Provider} = ff_withdrawal_provider:get(maps:get(provider, V)), - {created, #{ - id => maps:get(id, V), - source => ff_wallet_machine:wallet(SourceSt), - destination => ff_destination_machine:destination(DestinationSt), - body => maps:get(body, V), - provider => Provider - }}; -hydrate({status_changed, S}, _) -> - {status_changed, S}; -hydrate({transfer, Ev}, _) -> - % TODO - % - `ff_transfer:hydrate(Ev)` - {transfer, Ev}; -hydrate({session_started, SID}, _) -> - {session_started, SID}; -hydrate({session_finished, SID}, _) -> - {session_finished, SID}. diff --git a/apps/ff_withdraw/src/ff_withdrawal_machine.erl b/apps/ff_withdraw/src/ff_withdrawal_machine.erl index 464549d..d89e3ec 100644 --- a/apps/ff_withdraw/src/ff_withdrawal_machine.erl +++ b/apps/ff_withdraw/src/ff_withdrawal_machine.erl @@ -7,7 +7,6 @@ %% API -type id() :: machinery:id(). --type timestamp() :: machinery:timestamp(). -type ctx() :: ff_ctx:ctx(). -type withdrawal() :: ff_withdrawal:withdrawal(). @@ -19,12 +18,8 @@ cancel_transfer | undefined . --type st() :: #{ - activity := activity(), - withdrawal := withdrawal(), - ctx := ctx(), - times => {timestamp(), timestamp()} -}. +-type st() :: + ff_machine:st(withdrawal()). -export_type([id/0]). @@ -35,10 +30,6 @@ %% Accessors -export([withdrawal/1]). --export([activity/1]). --export([ctx/1]). --export([created/1]). --export([updated/1]). %% Machinery @@ -50,7 +41,7 @@ %% Pipeline --import(ff_pipeline, [do/1, unwrap/1, unwrap/2, valid/2]). +-import(ff_pipeline, [do/1, unwrap/1]). %% @@ -65,24 +56,14 @@ -spec create(id(), params(), ctx()) -> ok | {error, - {source, notfound} | - {destination, notfound | unauthorized} | - {provider, notfound} | - _TransferError | + _WithdrawalError | exists }. 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_machine:destination( - unwrap(destination, ff_destination_machine:get(DestinationID)) - ), - ok = unwrap(destination, valid(authorized, ff_destination:status(Destination))), - Provider = unwrap(provider, ff_withdrawal_provider:choose(Destination, Body)), - Events1 = unwrap(ff_withdrawal:create(ID, Source, Destination, Body, Provider)), - Events2 = unwrap(ff_withdrawal:create_transfer(ff_withdrawal:collapse_events(Events1))), - unwrap(machinery:start(?NS, ID, {Events1 ++ Events2, Ctx}, backend())) + Events = unwrap(ff_withdrawal:create(ID, SourceID, DestinationID, Body)), + unwrap(machinery:start(?NS, ID, {Events, Ctx}, backend())) end). -spec get(id()) -> @@ -90,12 +71,10 @@ create(ID, #{source := SourceID, destination := DestinationID, body := Body}, Ct {error, notfound}. get(ID) -> - do(fun () -> - collapse(unwrap(machinery:get(?NS, ID, backend()))) - end). + ff_machine:get(ff_withdrawal, ?NS, ID). -spec events(id(), machinery:range()) -> - {ok, [{integer(), ts_ev(ev())}]} | + {ok, [{integer(), ff_machine:timestamped_event(event())}]} | {error, notfound}. events(ID, Range) -> @@ -109,98 +88,92 @@ backend() -> %% Accessors --spec withdrawal(st()) -> withdrawal(). --spec activity(st()) -> activity(). --spec ctx(st()) -> ctx(). --spec created(st()) -> timestamp() | undefined. --spec updated(st()) -> timestamp() | undefined. +-spec withdrawal(st()) -> + withdrawal(). -withdrawal(#{withdrawal := V}) -> V. -activity(#{activity := V}) -> V. -ctx(#{ctx := V}) -> V. -created(St) -> erlang:element(1, times(St)). -updated(St) -> erlang:element(2, times(St)). - -times(St) -> - genlib_map:get(times, St, {undefined, undefined}). +withdrawal(St) -> + ff_machine:model(St). %% Machinery --type ts_ev(T) :: - {ev, timestamp(), T}. +-type event() :: + ff_withdrawal:event(). --type ev() :: - ff_withdrawal:ev(). - --type auxst() :: - #{ctx => ctx()}. - --type machine() :: machinery:machine(ts_ev(ev()), auxst()). --type result() :: machinery:result(ts_ev(ev()), auxst()). +-type machine() :: ff_machine:machine(event()). +-type result() :: ff_machine:result(event()). -type handler_opts() :: machinery:handler_opts(_). --spec init({[ev()], ctx()}, machine(), _, handler_opts()) -> +-spec init({[event()], ctx()}, machine(), _, handler_opts()) -> result(). init({Events, Ctx}, #{}, _, _Opts) -> #{ - events => emit_events(Events), + events => ff_machine:emit_events(Events), action => continue, aux_state => #{ctx => Ctx} }. -spec process_timeout(machine(), _, handler_opts()) -> - %% result(). - %% The return type is result(), but we run into a very strange dialyzer behaviour here - %% so meet a crappy workaround: - machinery:result(ts_ev(ev()), auxst()). + result(). process_timeout(Machine, _, _Opts) -> - St = collapse(Machine), - process_activity(activity(St), St). + 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 => emit_events(Events), action => continue}; + #{ + events => ff_machine:emit_events(Events), + action => continue + }; {error, Reason} -> - #{events => emit_failure(Reason)} + #{ + events => emit_failure(Reason) + } end; process_activity(create_session, St) -> case ff_withdrawal:create_session(withdrawal(St)) of {ok, Events} -> #{ - events => emit_events(Events), + events => ff_machine:emit_events(Events), action => set_poll_timer(St) }; {error, Reason} -> - #{events => emit_failure(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 => emit_events(Events), action => continue}; + #{ + events => ff_machine:emit_events(Events), + action => continue + }; {ok, []} -> - #{action => set_poll_timer(St)} + #{ + action => set_poll_timer(St) + } end; process_activity(commit_transfer, St) -> {ok, Events} = ff_withdrawal:commit_transfer(withdrawal(St)), #{ - events => emit_events(Events ++ [{status_changed, succeeded}]) + events => ff_machine:emit_events(Events) }; process_activity(cancel_transfer, St) -> {ok, Events} = ff_withdrawal:cancel_transfer(withdrawal(St)), #{ - events => emit_events(Events ++ [{status_changed, {failed, <<"transfer cancelled">>}}]) + events => ff_machine:emit_events(Events) }. set_poll_timer(St) -> Now = machinery_time:now(), - Timeout = erlang:max(1, machinery_time:interval(Now, updated(St)) div 1000), + 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()) -> @@ -210,62 +183,22 @@ process_call(_CallArgs, #{}, _, _Opts) -> {ok, #{}}. emit_failure(Reason) -> - emit_event({status_changed, {failed, Reason}}). + ff_machine:emit_event({status_changed, {failed, Reason}}). %% -collapse(#{history := History, aux_state := #{ctx := Ctx}}) -> - collapse_history(History, #{activity => idle, ctx => Ctx}). +-spec deduce_activity(withdrawal()) -> + activity(). -collapse_history(History, St) -> - lists:foldl(fun merge_event/2, St, History). - -merge_event({_ID, _Ts, TsEv}, St0) -> - {EvBody, St1} = merge_ts_event(TsEv, St0), - apply_event(ff_withdrawal:hydrate(EvBody, maps:get(withdrawal, St1, undefined)), St1). - -apply_event(Ev, St) -> - W1 = ff_withdrawal:apply_event(Ev, maps:get(withdrawal, St, undefined)), - St#{ - activity => deduce_activity(Ev), - withdrawal => W1 - }. - -deduce_activity({created, _}) -> - undefined; -deduce_activity({transfer, {created, _}}) -> - undefined; -deduce_activity({transfer, {status_changed, created}}) -> - prepare_transfer; -deduce_activity({transfer, {status_changed, prepared}}) -> - create_session; -deduce_activity({session_started, _}) -> - await_session_completion; -deduce_activity({session_finished, _}) -> - undefined; -deduce_activity({status_changed, succeeded}) -> - commit_transfer; -deduce_activity({status_changed, {failed, _}}) -> +deduce_activity(#{status := {failed, _}}) -> cancel_transfer; -deduce_activity({transfer, {status_changed, committed}}) -> - undefined; -deduce_activity({transfer, {status_changed, cancelled}}) -> - undefined; -deduce_activity({status_changed, _}) -> +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. - -%% - -emit_event(E) -> - emit_events([E]). - -emit_events(Es) -> - emit_events(Es, machinery_time:now()). - -emit_events(Es, Ts) -> - [{ev, Ts, ff_withdrawal:dehydrate(Body)} || Body <- Es]. - -merge_ts_event({ev, Ts, Body}, St = #{times := {Created, _Updated}}) -> - {Body, St#{times => {Created, Ts}}}; -merge_ts_event({ev, Ts, Body}, St = #{}) -> - {Body, St#{times => {Ts, Ts}}}. diff --git a/apps/ff_withdraw/src/ff_withdrawal_provider.erl b/apps/ff_withdraw/src/ff_withdrawal_provider.erl index 2ba4c40..4a02e44 100644 --- a/apps/ff_withdraw/src/ff_withdrawal_provider.erl +++ b/apps/ff_withdraw/src/ff_withdrawal_provider.erl @@ -16,13 +16,15 @@ -export([id/1]). -export([get/1]). --export([choose/2]). +-export([choose/3]). -export([create_session/3]). %% -adapter(#{adapter := V}) -> V. -adapter_opts(P) -> maps:get(adapter_opts, P, #{}). +adapter(#{adapter := V}) -> + V. +adapter_opts(P) -> + maps:get(adapter_opts, P, #{}). %% @@ -43,11 +45,11 @@ get(_) -> {error, notfound} end. --spec choose(ff_destination:destination(), ff_transaction:body()) -> +-spec choose(ff_wallet:wallet(), ff_destination:destination(), ff_transaction:body()) -> {ok, provider()} | {error, notfound}. -choose(_Destination, _Body) -> +choose(_Source, _Destination, _Body) -> case genlib_app:env(ff_withdraw, provider) of V when V /= undefined -> {ok, V}; @@ -57,6 +59,9 @@ choose(_Destination, _Body) -> %% +-spec create_session(id(), ff_adapter_withdrawal:withdrawal(), provider()) -> + ok | {error, exists}. + create_session(ID, Withdrawal, Provider) -> Adapter = {adapter(Provider), adapter_opts(Provider)}, ff_withdrawal_session_machine:create(ID, Adapter, Withdrawal). diff --git a/apps/ff_withdraw/src/ff_withdrawal_session_machine.erl b/apps/ff_withdraw/src/ff_withdrawal_session_machine.erl index 24990bc..71cba8f 100644 --- a/apps/ff_withdraw/src/ff_withdrawal_session_machine.erl +++ b/apps/ff_withdraw/src/ff_withdrawal_session_machine.erl @@ -76,6 +76,9 @@ %% API %% +-spec status(session()) -> + session_status(). + status(#{status := V}) -> V. %% @@ -91,7 +94,8 @@ create(ID, Adapter, Withdrawal) -> unwrap(machinery:start(?NS, ID, Session, backend())) end). --spec get(id()) -> {ok, session()} | {error, notfound}. +-spec get(id()) -> + ff_map:result(session()). get(ID) -> do(fun () -> session(collapse(unwrap(machinery:get(?NS, ID, backend())))) diff --git a/apps/ff_withdraw/test/ff_withdrawal_SUITE.erl b/apps/ff_withdraw/test/ff_withdrawal_SUITE.erl index dc05e15..d8a3b33 100644 --- a/apps/ff_withdraw/test/ff_withdrawal_SUITE.erl +++ b/apps/ff_withdraw/test/ff_withdrawal_SUITE.erl @@ -295,11 +295,17 @@ get_domain_config(C) -> get_default_termset() -> #domain_TermSet{ - % TODO - % - Strangely enough, hellgate checks wallet currency against _payments_ - % terms. - payments = #domain_PaymentsServiceTerms{ - currencies = {value, ?ordset([?cur(<<"RUB">>)])} + wallets = #domain_WalletServiceTerms{ + currencies = {value, ?ordset([?cur(<<"RUB">>)])}, + cash_limit = {decisions, [ + #domain_CashLimitDecision{ + if_ = {condition, {currency_is, ?cur(<<"RUB">>)}}, + then_ = {value, ?cashrng( + {inclusive, ?cash( 0, <<"RUB">>)}, + {exclusive, ?cash(10000000, <<"RUB">>)} + )} + } + ]} } }. diff --git a/apps/fistful/src/ff_account.erl b/apps/fistful/src/ff_account.erl new file mode 100644 index 0000000..b96517c --- /dev/null +++ b/apps/fistful/src/ff_account.erl @@ -0,0 +1,129 @@ +%%% +%%% Account +%%% +%%% Responsible for, at least: +%%% - managing partymgmt-related wallet stuff, +%%% - acknowledging transfer postings, +%%% - accounting and checking limits. +%%% + +-module(ff_account). + +-type id(T) :: T. +-type identity() :: ff_identity:id(). +-type currency() :: ff_currency:id(). + +-type account() :: #{ + id := id(binary()), + identity := identity(), + currency := currency(), + pm_wallet := ff_party:wallet_id() +}. + +-type event() :: + {created, account()}. + +-export_type([account/0]). +-export_type([event/0]). + +-export([id/1]). +-export([identity/1]). +-export([currency/1]). +-export([pm_wallet/1]). + +-export([pm_account/1]). + +-export([create/3]). +-export([is_accessible/1]). + +-export([apply_event/2]). + +%% Pipeline + +-import(ff_pipeline, [do/1, unwrap/1, unwrap/2]). + +%% Accessors + +-spec id(account()) -> + id(binary()). +-spec identity(account()) -> + identity(). +-spec currency(account()) -> + currency(). +-spec pm_wallet(account()) -> + ff_party:wallet_id(). + +id(#{id := ID}) -> + ID. +identity(#{identity := IdentityID}) -> + IdentityID. +currency(#{currency := CurrencyID}) -> + CurrencyID. +pm_wallet(#{pm_wallet := PMWalletID}) -> + PMWalletID. + +-spec pm_account(account()) -> + ff_transaction:account(). + +pm_account(Account) -> + {ok, Identity} = ff_identity_machine:get(identity(Account)), + {ok, PMAccount} = ff_party:get_wallet_account( + ff_identity:party(ff_identity_machine:identity(Identity)), + pm_wallet(Account) + ), + PMAccount. + +%% Actuators + +-spec create(id(_), identity(), currency()) -> + {ok, [event()]} | + {error, + {identity, notfound} | + {currency, notfound} | + {contract, notfound} | + ff_party:inaccessibility() | + invalid + }. + +create(ID, IdentityID, CurrencyID) -> + do(fun () -> + Identity = ff_identity_machine:identity(unwrap(identity, ff_identity_machine:get(IdentityID))), + _Currency = unwrap(currency, ff_currency:get(CurrencyID)), + PMWalletID = unwrap(ff_party:create_wallet( + ff_identity:party(Identity), + ff_identity:contract(Identity), + #{ + name => ff_string:join($/, [<<"ff/account">>, ID]), + currency => CurrencyID + } + )), + [{created, #{ + id => ID, + identity => IdentityID, + currency => CurrencyID, + pm_wallet => PMWalletID + }}] + end). + +-spec is_accessible(account()) -> + {ok, accessible} | + {error, ff_party:inaccessibility()}. + +is_accessible(Account) -> + do(fun () -> + Identity = get_identity(Account), + accessible = unwrap(ff_identity:is_accessible(Identity)), + accessible = unwrap(ff_party:is_wallet_accessible(ff_identity:party(Identity), pm_wallet(Account))) + end). + +get_identity(Account) -> + {ok, V} = ff_identity_machine:get(identity(Account)), + ff_identity_machine:identity(V). + +%% State + +-spec apply_event(event(), ff_maybe:maybe(account())) -> + account(). + +apply_event({created, Account}, undefined) -> + Account. diff --git a/apps/fistful/src/ff_identity.erl b/apps/fistful/src/ff_identity.erl index ccdb86c..7d5edc4 100644 --- a/apps/fistful/src/ff_identity.erl +++ b/apps/fistful/src/ff_identity.erl @@ -17,11 +17,11 @@ -type id(T) :: T. -type party() :: ff_party:id(). --type provider() :: ff_provider:provider(). +-type provider() :: ff_provider:id(). -type contract() :: ff_party:contract(). --type class() :: ff_identity_class:class(). --type level() :: ff_identity_class:level(). --type challenge_class() :: ff_identity_class:challenge_class(). +-type class() :: ff_identity_class:id(). +-type level() :: ff_identity_class:level_id(). +-type challenge_class() :: ff_identity_class:challenge_class_id(). -type challenge_id() :: id(_). -type identity() :: #{ @@ -38,18 +38,14 @@ -type challenge() :: ff_identity_challenge:challenge(). --type ev() :: +-type event() :: {created , identity()} | - {contract_set , contract()} | {level_changed , level()} | {effective_challenge_changed, challenge_id()} | {challenge , challenge_id(), ff_identity_challenge:ev()} . --type outcome() :: - [ev()]. - -export_type([identity/0]). --export_type([ev/0]). +-export_type([event/0]). -export([id/1]). -export([provider/1]). @@ -64,44 +60,49 @@ -export([is_accessible/1]). -export([create/4]). --export([setup_contract/1]). -export([start_challenge/4]). -export([poll_challenge_completion/2]). --export([collapse_events/1]). --export([apply_events/2]). -export([apply_event/2]). --export([dehydrate/1]). --export([hydrate/2]). - %% Pipeline -import(ff_pipeline, [do/1, unwrap/1, unwrap/2, expect/2, flip/1, valid/2]). %% Accessors --spec id(identity()) -> id(_). -id(#{id := V}) -> V. - --spec provider(identity()) -> provider(). -provider(#{provider := V}) -> V. - --spec class(identity()) -> class(). -class(#{class := V}) -> V. - --spec level(identity()) -> level(). -level(#{level := V}) -> V. - --spec party(identity()) -> party(). -party(#{party := V}) -> V. +-spec id(identity()) -> + id(_). +-spec provider(identity()) -> + provider(). +-spec class(identity()) -> + class(). +-spec level(identity()) -> + level(). +-spec party(identity()) -> + party(). -spec contract(identity()) -> - ff_map:result(contract()). + contract(). -contract(V) -> - ff_map:find(contract, V). +id(#{id := V}) -> + V. + +provider(#{provider := V}) -> + V. + +class(#{class := V}) -> + V. + +level(#{level := V}) -> + V. + +party(#{party := V}) -> + V. + +contract(#{contract := V}) -> + V. -spec challenges(identity()) -> #{challenge_id() => challenge()}. @@ -123,7 +124,7 @@ challenge(ChallengeID, Identity) -> -spec is_accessible(identity()) -> {ok, accessible} | - {error, {inaccessible, suspended | blocked}}. + {error, ff_party:inaccessibility()}. is_accessible(Identity) -> ff_party:is_accessible(party(Identity)). @@ -131,61 +132,72 @@ is_accessible(Identity) -> %% Constructor -spec create(id(_), party(), provider(), class()) -> - {ok, outcome()}. + {ok, [event()]} | + {error, + {provider, notfound} | + {identity_class, notfound} | + ff_party:inaccessibility() | + invalid + }. -create(ID, Party, Provider, Class) -> +create(ID, Party, ProviderID, ClassID) -> do(fun () -> + Provider = unwrap(provider, ff_provider:get(ProviderID)), + Class = unwrap(identity_class, ff_provider:get_identity_class(ClassID, Provider)), + LevelID = ff_identity_class:initial_level(Class), + {ok, Level} = ff_identity_class:level(LevelID, Class), + Contract = unwrap(ff_party:create_contract(Party, #{ + payinst => ff_provider:payinst(Provider), + contract_template => ff_identity_class:contract_template(Class), + contractor_level => ff_identity_class:contractor_level(Level) + })), [ {created, #{ id => ID, party => Party, - provider => Provider, - class => Class + provider => ProviderID, + class => ClassID, + contract => Contract }}, {level_changed, - ff_identity_class:initial_level(Class) + LevelID } ] end). --spec setup_contract(identity()) -> - {ok, outcome()} | - {error, - invalid - }. - -setup_contract(Identity) -> - do(fun () -> - Class = class(Identity), - Contract = unwrap(ff_party:create_contract(party(Identity), #{ - payinst => ff_provider:payinst(provider(Identity)), - contract_template => ff_identity_class:contract_template(Class), - contractor_level => ff_identity_class:contractor_level(level(Identity)) - })), - [{contract_set, Contract}] - end). - %% -spec start_challenge(challenge_id(), challenge_class(), [ff_identity_challenge:proof()], identity()) -> - {ok, outcome()} | + {ok, [event()]} | {error, exists | + {challenge_class, notfound} | {level, ff_identity_class:level()} | _CreateChallengeError }. -start_challenge(ChallengeID, ChallengeClass, Proofs, Identity) -> +start_challenge(ChallengeID, ChallengeClassID, Proofs, Identity) -> do(fun () -> - BaseLevel = ff_identity_class:base_level(ChallengeClass), - notfound = expect(exists, flip(challenge(ChallengeID, Identity))), - ok = unwrap(level, valid(BaseLevel, level(Identity))), - Events = unwrap(ff_identity_challenge:create(id(Identity), ChallengeClass, Proofs)), - [{challenge, ChallengeID, Ev} || Ev <- Events] + notfound = expect(exists, flip(challenge(ChallengeID, Identity))), + IdentityClass = get_identity_class(Identity), + ChallengeClass = unwrap(challenge_class, ff_identity_class:challenge_class( + ChallengeClassID, + IdentityClass + )), + ok = unwrap(level, valid(ff_identity_class:base_level(ChallengeClass), level(Identity))), + Events = unwrap(ff_identity_challenge:create( + ChallengeID, + id(Identity), + provider(Identity), + class(Identity), + ChallengeClassID, + Proofs + )), + [{{challenge, ChallengeID}, Ev} || Ev <- Events] end). -spec poll_challenge_completion(challenge_id(), identity()) -> - {ok, outcome()} | + {ok, [event()]} | {error, notfound | ff_identity_challenge:status() @@ -198,44 +210,49 @@ poll_challenge_completion(ChallengeID, Identity) -> [] -> []; Events = [_ | _] -> - {ok, Contract} = contract(Identity), - TargetLevel = ff_identity_class:target_level(ff_identity_challenge:class(Challenge)), - ContractorLevel = ff_identity_class:contractor_level(TargetLevel), - ok = unwrap(ff_party:change_contractor_level(party(Identity), Contract, ContractorLevel)), - [{challenge, ChallengeID, Ev} || Ev <- Events] ++ + Contract = contract(Identity), + IdentityClass = get_identity_class(Identity), + ChallengeClass = get_challenge_class(Challenge, Identity), + TargetLevelID = ff_identity_class:target_level(ChallengeClass), + {ok, Level} = ff_identity_class:level(TargetLevelID, IdentityClass), + ok = unwrap(ff_party:change_contractor_level( + party(Identity), + Contract, + ff_identity_class:contractor_level(Level) + )), + [{{challenge, ChallengeID}, Ev} || Ev <- Events] ++ [ - {level_changed, TargetLevel}, + {level_changed, TargetLevelID}, {effective_challenge_changed, ChallengeID} ] end end). +get_provider(Identity) -> + {ok, V} = ff_provider:get(provider(Identity)), V. + +get_identity_class(Identity) -> + {ok, V} = ff_provider:get_identity_class(class(Identity), get_provider(Identity)), V. + +get_challenge_class(Challenge, Identity) -> + {ok, V} = ff_identity_class:challenge_class( + ff_identity_challenge:class(Challenge), + get_identity_class(Identity) + ), + V. + %% --spec collapse_events([ev(), ...]) -> - identity(). - -collapse_events(Evs) when length(Evs) > 0 -> - apply_events(Evs, undefined). - --spec apply_events([ev()], undefined | identity()) -> - undefined | identity(). - -apply_events(Evs, Identity) -> - lists:foldl(fun apply_event/2, Identity, Evs). - --spec apply_event(ev(), undefined | identity()) -> +-spec apply_event(event(), ff_maybe:maybe(identity())) -> identity(). apply_event({created, Identity}, undefined) -> Identity; -apply_event({contract_set, C}, Identity) -> - Identity#{contract => C}; apply_event({level_changed, L}, Identity) -> Identity#{level => L}; apply_event({effective_challenge_changed, ID}, Identity) -> Identity#{effective => ID}; -apply_event({challenge, ID, Ev}, Identity) -> +apply_event({{challenge, ID}, Ev}, Identity) -> with_challenges( fun (Cs) -> with_challenge( @@ -252,47 +269,3 @@ with_challenges(Fun, Identity) -> with_challenge(ID, Fun, Challenges) -> maps:update_with(ID, Fun, maps:merge(#{ID => undefined}, Challenges)). - -%% - --spec dehydrate(ev()) -> - term(). - --spec hydrate(term(), undefined | identity()) -> - ev(). - -dehydrate({created, I}) -> - {created, #{ - id => id(I), - party => party(I), - provider => ff_provider:id(provider(I)), - class => ff_identity_class:id(class(I)) - }}; -dehydrate({contract_set, C}) -> - {contract_set, C}; -dehydrate({level_changed, L}) -> - {level_changed, ff_identity_class:level_id(L)}; -dehydrate({effective_challenge_changed, ID}) -> - {effective_challenge_changed, ID}; -dehydrate({challenge, ID, Ev}) -> - {challenge, ID, ff_identity_challenge:dehydrate(Ev)}. - -hydrate({created, I}, undefined) -> - {ok, Provider} = ff_provider:get(maps:get(provider, I)), - {ok, Class} = ff_provider:get_identity_class(maps:get(class, I), Provider), - {created, #{ - id => maps:get(id, I), - party => maps:get(party, I), - provider => Provider, - class => Class - }}; -hydrate({contract_set, C}, _) -> - {contract_set, C}; -hydrate({level_changed, L}, Identity) -> - {ok, Level} = ff_identity_class:level(L, class(Identity)), - {level_changed, Level}; -hydrate({effective_challenge_changed, ID}, _) -> - {effective_challenge_changed, ID}; -hydrate({challenge, ID, Ev}, Identity) -> - Challenge = ff_maybe:from_result(challenge(ID, Identity)), - {challenge, ID, ff_identity_challenge:hydrate(Ev, Challenge)}. diff --git a/apps/fistful/src/ff_identity_challenge.erl b/apps/fistful/src/ff_identity_challenge.erl index 80a0a5e..350ed2f 100644 --- a/apps/fistful/src/ff_identity_challenge.erl +++ b/apps/fistful/src/ff_identity_challenge.erl @@ -1,24 +1,36 @@ %%% %%% Identity challenge activity %%% +%%% TODOs +%%% +%%% - `ProviderID` + `IdentityClassID` + `ChallengeClassID` easily replaceable +%%% with a _single_ identifier if we drop strictly hierarchical provider +%%% definition. +%%% -module(ff_identity_challenge). %% API -type id(T) :: T. +-type claimant() :: id(binary()). -type timestamp() :: machinery:timestamp(). --type class() :: ff_identity_class:challenge_class(). +-type provider() :: ff_provider:id(). +-type identity_class() :: ff_identity_class:id(). +-type challenge_class() :: ff_identity_class:challenge_class_id(). -type master_id() :: id(binary()). -type claim_id() :: id(binary()). -type challenge() :: #{ - class := class(), - claimant := id(_), - proofs := [proof()], - master_id := master_id(), - claim_id := claim_id(), - status => status() + id := id(_), + claimant := claimant(), + provider := provider(), + identity_class := identity_class(), + challenge_class := challenge_class(), + proofs := [proof()], + master_id := master_id(), + claim_id := claim_id(), + status => status() }. -type proof() :: @@ -49,15 +61,12 @@ -type failure() :: _TODO. --type ev() :: +-type event() :: {created, challenge()} | {status_changed, status()}. --type outcome() :: - [ev()]. - -export_type([challenge/0]). --export_type([ev/0]). +-export_type([event/0]). -export([claimant/1]). -export([status/1]). @@ -67,15 +76,11 @@ -export([claim_id/1]). -export([master_id/1]). --export([create/3]). +-export([create/6]). -export([poll_completion/1]). --export([apply_events/2]). -export([apply_event/2]). --export([dehydrate/1]). --export([hydrate/2]). - %% Pipeline -import(ff_pipeline, [do/1, unwrap/1, unwrap/2, valid/2]). @@ -89,15 +94,15 @@ status(#{status := V}) -> V. -spec claimant(challenge()) -> - id(_). + claimant(). claimant(#{claimant := V}) -> V. -spec class(challenge()) -> - class(). + challenge_class(). -class(#{class := V}) -> +class(#{challenge_class := V}) -> V. -spec proofs(challenge()) -> @@ -132,25 +137,32 @@ claim_id(#{claim_id := V}) -> %% --spec create(id(_), class(), [proof()]) -> - {ok, outcome()} | +-spec create(id(_), claimant(), provider(), identity_class(), challenge_class(), [proof()]) -> + {ok, [event()]} | {error, {proof, notfound | insufficient} | _StartError }. -create(Claimant, Class, Proofs) -> +create(ID, Claimant, ProviderID, IdentityClassID, ChallengeClassID, Proofs) -> do(fun () -> - TargetLevel = ff_identity_class:target_level(Class), + Provider = unwrap(provider, ff_provider:get(ProviderID)), + IdentityClass = unwrap(identity_class, ff_provider:get_identity_class(IdentityClassID, Provider)), + ChallengeClass = unwrap(challenge_class, ff_identity_class:challenge_class(ChallengeClassID, IdentityClass)), + TargetLevelID = ff_identity_class:target_level(ChallengeClass), + {ok, TargetLevel} = ff_identity_class:level(TargetLevelID, IdentityClass), MasterID = unwrap(deduce_identity_id(Proofs)), - ClaimID = unwrap(create_claim(MasterID, TargetLevel, Claimant, Proofs)), + ClaimID = unwrap(create_claim(MasterID, TargetLevel, Claimant, Proofs)), [ {created, #{ - class => Class, - claimant => Claimant, - proofs => Proofs, - master_id => MasterID, - claim_id => ClaimID + id => ID, + claimant => Claimant, + provider => ProviderID, + identity_class => IdentityClassID, + challenge_class => ChallengeClassID, + proofs => Proofs, + master_id => MasterID, + claim_id => ClaimID }}, {status_changed, pending @@ -159,7 +171,7 @@ create(Claimant, Class, Proofs) -> end). -spec poll_completion(challenge()) -> - {ok, outcome()} | + {ok, [event()]} | {error, notfound | status() @@ -185,13 +197,7 @@ poll_completion(Challenge) -> %% --spec apply_events([ev()], undefined | challenge()) -> - undefined | challenge(). - -apply_events(Evs, Challenge) -> - lists:foldl(fun apply_event/2, Challenge, Evs). - --spec apply_event(ev(), undefined | challenge()) -> +-spec apply_event(event(), ff_maybe:maybe(challenge())) -> challenge(). apply_event({created, Challenge}, undefined) -> @@ -199,18 +205,6 @@ apply_event({created, Challenge}, undefined) -> apply_event({status_changed, S}, Challenge) -> Challenge#{status => S}. --spec dehydrate(ev()) -> - term(). - --spec hydrate(term(), undefined | challenge()) -> - ev(). - -dehydrate(Ev) -> - Ev. - -hydrate(Ev, _) -> - Ev. - %% -include_lib("id_proto/include/id_proto_identification_thrift.hrl"). diff --git a/apps/fistful/src/ff_identity_class.erl b/apps/fistful/src/ff_identity_class.erl index 4a91740..d21bcdb 100644 --- a/apps/fistful/src/ff_identity_class.erl +++ b/apps/fistful/src/ff_identity_class.erl @@ -49,12 +49,10 @@ -export([initial_level/1]). -export([level/2]). --export([level_id/1]). -export([level_name/1]). -export([contractor_level/1]). -export([challenge_class/2]). --export([challenge_class_id/1]). -export([base_level/1]). -export([target_level/1]). -export([challenge_class_name/1]). @@ -87,7 +85,7 @@ contract_template(#{contract_template_ref := V}) -> V. -spec initial_level(class()) -> - level(). + level_id(). initial_level(#{initial_level := V}) -> V. @@ -108,12 +106,6 @@ challenge_class(ID, #{challenge_classes := ChallengeClasses}) -> %% Level --spec level_id(level()) -> - level_id(). - -level_id(#{id := V}) -> - V. - -spec level_name(level()) -> binary(). @@ -128,12 +120,6 @@ contractor_level(#{contractor_level := V}) -> %% Challenge --spec challenge_class_id(challenge_class()) -> - challenge_class_id(). - -challenge_class_id(#{id := V}) -> - V. - -spec challenge_class_name(challenge_class()) -> binary(). @@ -141,13 +127,13 @@ challenge_class_name(#{name := V}) -> V. -spec base_level(challenge_class()) -> - level(). + level_id(). base_level(#{base_level := V}) -> V. -spec target_level(challenge_class()) -> - level(). + level_id(). target_level(#{target_level := V}) -> V. diff --git a/apps/fistful/src/ff_identity_machine.erl b/apps/fistful/src/ff_identity_machine.erl index 52e545c..674eb8a 100644 --- a/apps/fistful/src/ff_identity_machine.erl +++ b/apps/fistful/src/ff_identity_machine.erl @@ -20,19 +20,9 @@ -type id() :: machinery:id(). -type identity() :: ff_identity:identity(). --type timestamp() :: machinery:timestamp(). -type ctx() :: ff_ctx:ctx(). --type activity() :: - {challenge, challenge_id()} | - undefined . - --type st() :: #{ - activity := activity(), - identity := identity(), - ctx := ctx(), - times => {timestamp(), timestamp()} -}. +-type st() :: ff_machine:st(identity()). -type challenge_id() :: machinery:id(). @@ -42,15 +32,12 @@ -export([create/3]). -export([get/1]). -export([events/2]). + -export([start_challenge/2]). %% Accessors -export([identity/1]). --export([activity/1]). --export([ctx/1]). --export([created/1]). --export([updated/1]). %% Machinery @@ -62,7 +49,7 @@ %% Pipeline --import(ff_pipeline, [do/1, do/2, unwrap/1, unwrap/2]). +-import(ff_pipeline, [do/1, do/2, unwrap/1]). -define(NS, 'ff/identity'). @@ -75,20 +62,14 @@ -spec create(id(), params(), ctx()) -> ok | {error, - {provider, notfound} | - {identity_class, notfound} | - _SetupContractError | + _IdentityCreateError | exists }. create(ID, #{party := Party, provider := ProviderID, class := IdentityClassID}, Ctx) -> do(fun () -> - Provider = unwrap(provider, ff_provider:get(ProviderID)), - IdentityClass = unwrap(identity_class, ff_provider:get_identity_class(IdentityClassID, Provider)), - Events0 = unwrap(ff_identity:create(ID, Party, Provider, IdentityClass)), - Identity = ff_identity:collapse_events(Events0), - Events1 = unwrap(ff_identity:setup_contract(Identity)), - unwrap(machinery:start(?NS, ID, {Events0 ++ Events1, Ctx}, backend())) + Events = unwrap(ff_identity:create(ID, Party, ProviderID, IdentityClassID)), + unwrap(machinery:start(?NS, ID, {Events, Ctx}, backend())) end). -spec get(id()) -> @@ -96,12 +77,10 @@ create(ID, #{party := Party, provider := ProviderID, class := IdentityClassID}, {error, notfound} . get(ID) -> - do(fun () -> - collapse(unwrap(machinery:get(?NS, ID, backend()))) - end). + ff_machine:get(ff_identity, ?NS, ID). -spec events(id(), machinery:range()) -> - {ok, [{integer(), ts_ev(ev())}]} | + {ok, [{integer(), ff_machine:timestamped_event(event())}]} | {error, notfound}. events(ID, Range) -> @@ -120,11 +99,7 @@ events(ID, Range) -> ok | {error, notfound | - {challenge, - {pending, challenge_id()} | - {class, notfound} | - _IdentityChallengeError - } + _IdentityChallengeError }. start_challenge(ID, Params) -> @@ -140,42 +115,27 @@ backend() -> %% Accessors --spec identity(st()) -> identity(). --spec activity(st()) -> activity(). --spec ctx(st()) -> ctx(). --spec created(st()) -> timestamp() | undefined. --spec updated(st()) -> timestamp() | undefined. +-spec identity(st()) -> + identity(). -identity(#{identity := V}) -> V. -activity(#{activity := V}) -> V. -ctx(#{ctx := V}) -> V. -created(St) -> erlang:element(1, times(St)). -updated(St) -> erlang:element(2, times(St)). - -times(St) -> - genlib_map:get(times, St, {undefined, undefined}). +identity(St) -> + ff_machine:model(St). %% Machinery --type ts_ev(T) :: - {ev, timestamp(), T}. +-type event() :: + ff_identity:event(). --type ev() :: - ff_identity:ev(). - --type auxst() :: - #{ctx => ctx()}. - --type machine() :: machinery:machine(ts_ev(ev()), auxst()). --type result() :: machinery:result(ts_ev(ev()), auxst()). +-type machine() :: ff_machine:machine(event()). +-type result() :: ff_machine:result(event()). -type handler_opts() :: machinery:handler_opts(_). --spec init({[ev()], ctx()}, machine(), _, handler_opts()) -> +-spec init({[event()], ctx()}, machine(), _, handler_opts()) -> result(). init({Events, Ctx}, #{}, _, _Opts) -> #{ - events => emit_ts_events(Events), + events => ff_machine:emit_events(Events), aux_state => #{ctx => Ctx} }. @@ -185,21 +145,22 @@ init({Events, Ctx}, #{}, _, _Opts) -> result(). process_timeout(Machine, _, _Opts) -> - process_activity(collapse(Machine)). + St = ff_machine:collapse(ff_identity, Machine), + process_activity(deduce_activity(identity(St)), St). -process_activity(#{activity := {challenge, ChallengeID}} = St) -> +process_activity({challenge, ChallengeID}, St) -> Identity = identity(St), {ok, Events} = ff_identity:poll_challenge_completion(ChallengeID, Identity), case Events of [] -> #{action => set_poll_timer(St)}; _Some -> - #{events => emit_ts_events(Events)} + #{events => ff_machine:emit_events(Events)} end. set_poll_timer(St) -> Now = machinery_time:now(), - Timeout = erlang:max(1, machinery_time:interval(Now, updated(St)) div 1000), + Timeout = erlang:max(1, machinery_time:interval(Now, ff_machine:updated(St)) div 1000), {set_timer, {timeout, Timeout}}. %% @@ -211,9 +172,15 @@ set_poll_timer(St) -> {_TODO, result()}. process_call({start_challenge, Params}, Machine, _, _Opts) -> - do_start_challenge(Params, collapse(Machine)). + St = ff_machine:collapse(ff_identity, Machine), + case deduce_activity(identity(St)) of + undefined -> + do_start_challenge(Params, St); + {challenge, ChallengeID} -> + handle_result({error, {challenge, {pending, ChallengeID}}}) + end. -do_start_challenge(Params, #{activity := undefined} = St) -> +do_start_challenge(Params, St) -> Identity = identity(St), handle_result(do(challenge, fun () -> #{ @@ -221,16 +188,12 @@ do_start_challenge(Params, #{activity := undefined} = St) -> class := ChallengeClassID, proofs := Proofs } = Params, - Class = ff_identity:class(Identity), - ChallengeClass = unwrap(class, ff_identity_class:challenge_class(ChallengeClassID, Class)), - Events = unwrap(ff_identity:start_challenge(ChallengeID, ChallengeClass, Proofs, Identity)), + Events = unwrap(ff_identity:start_challenge(ChallengeID, ChallengeClassID, Proofs, Identity)), #{ - events => emit_ts_events(Events), + events => ff_machine:emit_events(Events), action => continue } - end)); -do_start_challenge(_Params, #{activity := {challenge, ChallengeID}}) -> - handle_result({error, {challenge, {pending, ChallengeID}}}). + end)). handle_result({ok, R}) -> {ok, R}; @@ -239,46 +202,13 @@ handle_result({error, _} = Error) -> %% -collapse(#{history := History, aux_state := #{ctx := Ctx}}) -> - apply_events(History, #{ctx => Ctx}). - -apply_events(History, St) -> - lists:foldl(fun apply_event/2, St, History). - -apply_event({_ID, _Ts, TsEv}, St0) -> - {EvBody, St1} = apply_ts_event(TsEv, St0), - apply_event_body(ff_identity:hydrate(EvBody, maps:get(identity, St1, undefined)), St1). - -apply_event_body(IdentityEv, St) -> - St#{ - activity => deduce_activity(IdentityEv), - identity => ff_identity:apply_event(IdentityEv, maps:get(identity, St, undefined)) - }. - -deduce_activity({created, _}) -> - undefined; -deduce_activity({contract_set, _}) -> - undefined; -deduce_activity({level_changed, _}) -> - undefined; -deduce_activity({effective_challenge_changed, _}) -> - undefined; -deduce_activity({challenge, _ChallengeID, {created, _}}) -> - undefined; -deduce_activity({challenge, ChallengeID, {status_changed, pending}}) -> - {challenge, ChallengeID}; -deduce_activity({challenge, _ChallengeID, {status_changed, _}}) -> +deduce_activity(#{challenges := Challenges}) -> + Filter = fun (_, Challenge) -> ff_identity_challenge:status(Challenge) == pending end, + case maps:keys(maps:filter(Filter, Challenges)) of + [ChallengeID] -> + {challenge, ChallengeID}; + [] -> + undefined + end; +deduce_activity(#{}) -> undefined. - -%% - -emit_ts_events(Es) -> - emit_ts_events(Es, machinery_time:now()). - -emit_ts_events(Es, Ts) -> - [{ev, Ts, ff_identity:dehydrate(Body)} || Body <- Es]. - -apply_ts_event({ev, Ts, Body}, St = #{times := {Created, _Updated}}) -> - {Body, St#{times => {Created, Ts}}}; -apply_ts_event({ev, Ts, Body}, St = #{}) -> - {Body, St#{times => {Ts, Ts}}}. diff --git a/apps/fistful/src/ff_machine.erl b/apps/fistful/src/ff_machine.erl new file mode 100644 index 0000000..a517da4 --- /dev/null +++ b/apps/fistful/src/ff_machine.erl @@ -0,0 +1,161 @@ +%%% +%%% Generic machine +%%% +%%% TODOs +%%% +%%% - Split ctx and time tracking into different machine layers. +%%% + +-module(ff_machine). + +-type id() :: machinery:id(). +-type namespace() :: machinery:namespace(). +-type timestamp() :: machinery:timestamp(). +-type ctx() :: ff_ctx:ctx(). + +-type st(Model) :: #{ + model := Model, + ctx := ctx(), + times => {timestamp(), timestamp()} +}. + +-type timestamped_event(T) :: + {ev, timestamp(), T}. + +-type auxst() :: + #{ctx := ctx()}. + +-type machine(T) :: + machinery:machine(timestamped_event(T), auxst()). + +-type result(T) :: + machinery:result(timestamped_event(T), auxst()). + +-export_type([st/1]). +-export_type([machine/1]). +-export_type([result/1]). +-export_type([timestamped_event/1]). + +%% Accessors + +-export([model/1]). +-export([ctx/1]). +-export([created/1]). +-export([updated/1]). + +%% + +-export([get/3]). + +-export([collapse/2]). + +-export([emit_event/1]). +-export([emit_events/1]). + +%% + +-export([init/4]). +-export([process_timeout/3]). +-export([process_call/4]). + +%% + +-import(ff_pipeline, [do/1, unwrap/1]). + +%% + +-spec model(st(Model)) -> + Model. +-spec ctx(st(_)) -> + ctx(). +-spec created(st(_)) -> + timestamp() | undefined. +-spec updated(st(_)) -> + timestamp() | undefined. + +model(#{model := V}) -> + V. +ctx(#{ctx := V}) -> + V. +created(St) -> + erlang:element(1, times(St)). +updated(St) -> + erlang:element(2, times(St)). + +times(St) -> + genlib_map:get(times, St, {undefined, undefined}). + +%% + +-spec get(module(), namespace(), id()) -> + {ok, st(_)} | + {error, notfound}. + +get(Mod, NS, ID) -> + do(fun () -> + collapse(Mod, unwrap(machinery:get(NS, ID, fistful:backend(NS)))) + end). + +-spec collapse(module(), machine(_)) -> + st(_). + +collapse(Mod, #{history := History, aux_state := #{ctx := Ctx}}) -> + collapse_history(Mod, History, #{ctx => Ctx}). + +collapse_history(Mod, History, St0) -> + lists:foldl(fun (Ev, St) -> merge_event(Mod, Ev, St) end, St0, History). + +-spec emit_event(E) -> + [timestamped_event(E)]. + +emit_event(Event) -> + emit_events([Event]). + +-spec emit_events([E]) -> + [timestamped_event(E)]. + +emit_events(Events) -> + emit_timestamped_events(Events, machinery_time:now()). + +emit_timestamped_events(Events, Ts) -> + [{ev, Ts, Body} || Body <- Events]. + +merge_event(Mod, {_ID, _Ts, TsEvent}, St0) -> + {Ev, St1} = merge_timestamped_event(TsEvent, St0), + Model1 = Mod:apply_event(Ev, maps:get(model, St1, undefined)), + St1#{model => Model1}. + +merge_timestamped_event({ev, Ts, Body}, St = #{times := {Created, _Updated}}) -> + {Body, St#{times => {Created, Ts}}}; +merge_timestamped_event({ev, Ts, Body}, St = #{}) -> + {Body, St#{times => {Ts, Ts}}}. + +%% + +-spec init({machinery:args(_), ctx()}, machinery:machine(E, A), module(), _) -> + machinery:result(E, A). + +init({Args, Ctx}, _Machine, Mod, _) -> + Events = Mod:init(Args), + #{ + events => emit_events(Events), + aux_state => #{ctx => Ctx} + }. + +-spec process_timeout(machinery:machine(E, A), module(), _) -> + machinery:result(E, A). + +process_timeout(Machine, Mod, _) -> + Events = Mod:process_timeout(collapse(Mod, Machine)), + #{ + events => emit_events(Events) + }. + +-spec process_call(machinery:args(_), machinery:machine(E, A), module(), _) -> + {machinery:response(_), machinery:result(E, A)}. + +process_call(Args, Machine, Mod, _) -> + {Response, Events} = Mod:process_call(Args, collapse(Mod, Machine)), + {Response, #{ + events => emit_events(Events) + }}. diff --git a/apps/fistful/src/ff_party.erl b/apps/fistful/src/ff_party.erl index 210b1a6..dc6cae9 100644 --- a/apps/fistful/src/ff_party.erl +++ b/apps/fistful/src/ff_party.erl @@ -24,6 +24,11 @@ -export_type([wallet/0]). -export_type([party_params/0]). +-type inaccessiblity() :: + {inaccessible, blocked | suspended}. + +-export_type([inaccessiblity/0]). + -export([create/1]). -export([create/2]). -export([is_accessible/1]). @@ -36,7 +41,7 @@ %% Pipeline --import(ff_pipeline, [do/1, unwrap/1, unwrap/2]). +-import(ff_pipeline, [do/1, unwrap/1]). %% @@ -55,7 +60,7 @@ create(ID, Params) -> -spec is_accessible(id()) -> {ok, accessible} | - {error, {inaccessible, suspended | blocked}}. + {error, inaccessiblity()}. is_accessible(ID) -> case do_get_party(ID) of @@ -117,6 +122,7 @@ is_wallet_accessible(ID, WalletID) -> -spec create_contract(id(), contract_prototype()) -> {ok, contract()} | + {error, inaccessiblity()} | {error, invalid}. create_contract(ID, Prototype) -> @@ -135,6 +141,7 @@ generate_contract_id() -> -spec change_contractor_level(id(), contract(), dmsl_domain_thrift:'ContractorIdentificationLevel'()) -> ok | + {error, inaccessiblity()} | {error, invalid}. change_contractor_level(ID, ContractID, ContractorLevel) -> @@ -154,6 +161,7 @@ change_contractor_level(ID, ContractID, ContractorLevel) -> -spec create_wallet(id(), contract(), wallet_prototype()) -> {ok, wallet()} | + {error, inaccessiblity()} | {error, invalid}. create_wallet(ID, ContractID, Prototype) -> @@ -217,8 +225,12 @@ do_create_claim(ID, Changeset) -> case call('CreateClaim', [construct_userinfo(), ID, Changeset]) of {ok, Claim} -> {ok, Claim}; - {exception, #payproc_InvalidChangeset{reason = _Reason}} -> + {exception, #payproc_InvalidChangeset{ + reason = {invalid_wallet, #payproc_InvalidWallet{reason = {contract_terms_violated, _}}} + }} -> {error, invalid}; + {exception, #payproc_InvalidPartyStatus{status = Status}} -> + {error, construct_inaccessibilty(Status)}; {exception, Unexpected} -> error(Unexpected) end. @@ -238,6 +250,11 @@ do_accept_claim(ID, Claim) -> error(Unexpected) end. +construct_inaccessibilty({blocking, _}) -> + {inaccessible, blocked}; +construct_inaccessibilty({suspension, _}) -> + {inaccessible, suspended}. + %% -define(contractor_mod(ID, Mod), diff --git a/apps/fistful/src/ff_provider.erl b/apps/fistful/src/ff_provider.erl index b174bb8..9267942 100644 --- a/apps/fistful/src/ff_provider.erl +++ b/apps/fistful/src/ff_provider.erl @@ -47,17 +47,23 @@ %% --spec id(provider()) -> id(). -id(#{id := ID}) -> ID. +-spec id(provider()) -> + id(). +-spec name(provider()) -> + binary(). +-spec residences(provider()) -> + [ff_residence:id()]. +-spec payinst(provider()) -> + payinst_ref(). --spec name(provider()) -> binary(). -name(#{payinst := PI}) -> PI#domain_PaymentInstitution.name. - --spec residences(provider()) -> [ff_residence:id()]. -residences(#{payinst := PI}) -> PI#domain_PaymentInstitution.residences. - --spec payinst(provider()) -> payinst_ref(). -payinst(#{payinst_ref := V}) -> V. +id(#{id := ID}) -> + ID. +name(#{payinst := PI}) -> + PI#domain_PaymentInstitution.name. +residences(#{payinst := PI}) -> + PI#domain_PaymentInstitution.residences. +payinst(#{payinst_ref := V}) -> + V. %% @@ -106,24 +112,24 @@ get(ID) -> CCName = maps:get(name, CCC, CCID), BaseLevelID = maps:get(base, CCC), TargetLevelID = maps:get(target, CCC), - {ok, BaseLevel} = maps:find(BaseLevelID, Levels), - {ok, TargetLevel} = maps:find(TargetLevelID, Levels), + {ok, _} = maps:find(BaseLevelID, Levels), + {ok, _} = maps:find(TargetLevelID, Levels), #{ id => CCID, name => CCName, - base_level => BaseLevel, - target_level => TargetLevel + base_level => BaseLevelID, + target_level => TargetLevelID } end, maps:get(challenges, ICC, #{}) ), InitialLevelID = maps:get(initial_level, ICC), - {ok, InitialLevel} = maps:find(InitialLevelID, Levels), + {ok, _} = maps:find(InitialLevelID, Levels), #{ id => ICID, name => Name, contract_template_ref => ContractTemplateRef, - initial_level => InitialLevel, + initial_level => InitialLevelID, levels => Levels, challenge_classes => ChallengeClasses } diff --git a/apps/fistful/src/ff_transaction.erl b/apps/fistful/src/ff_transaction.erl index a780102..3f73ff6 100644 --- a/apps/fistful/src/ff_transaction.erl +++ b/apps/fistful/src/ff_transaction.erl @@ -1,6 +1,8 @@ %%% %%% Financial transaction between accounts %%% +%%% - Rename to `ff_posting_plan`? +%%% -module(ff_transaction). diff --git a/apps/fistful/src/ff_transfer.erl b/apps/fistful/src/ff_transfer.erl index 24a1370..3ba6a62 100644 --- a/apps/fistful/src/ff_transfer.erl +++ b/apps/fistful/src/ff_transfer.erl @@ -6,7 +6,6 @@ %%% - We must synchronise any transfers on wallet machine, as one may request %%% us to close wallet concurrently. Moreover, we should probably check any %%% limits there too. -%%% - Well, we will need `cancel` soon too. %%% - What if we get rid of some failures in `prepare`, specifically those %%% which related to wallet blocking / suspension? It would be great to get %%% rid of the `wallet closed` failure but I see no way to do so. @@ -14,10 +13,10 @@ -module(ff_transfer). --type wallet() :: ff_wallet:wallet(). +-type id() :: ff_transaction:id(). +-type account() :: ff_account:id(). -type body() :: ff_transaction:body(). --type trxid() :: ff_transaction:id(). --type posting() :: {wallet(), wallet(), body()}. +-type posting() :: {account(), account(), body()}. -type status() :: created | @@ -26,24 +25,21 @@ cancelled . -type transfer() :: #{ - trxid := trxid(), + id := id(), postings := [posting()], status => status() }. --type ev() :: +-type event() :: {created, transfer()} | {status_changed, status()}. --type outcome() :: - [ev()]. - -export_type([transfer/0]). -export_type([posting/0]). -export_type([status/0]). --export_type([ev/0]). +-export_type([event/0]). --export([trxid/1]). +-export([id/1]). -export([postings/1]). -export([status/1]). @@ -62,38 +58,42 @@ %% --spec trxid(transfer()) -> trxid(). --spec postings(transfer()) -> [posting()]. --spec status(transfer()) -> status(). +-spec id(transfer()) -> + id(). +-spec postings(transfer()) -> + [posting()]. +-spec status(transfer()) -> + status(). -trxid(#{trxid := V}) -> V. -postings(#{postings := V}) -> V. -status(#{status := V}) -> V. +id(#{id := V}) -> + V. +postings(#{postings := V}) -> + V. +status(#{status := V}) -> + V. %% --spec create(trxid(), [posting()]) -> - {ok, outcome()} | +-spec create(id(), [posting()]) -> + {ok, [event()]} | {error, empty | - {wallet, - {inaccessible, blocked | suspended} | - {currency, invalid} | - {provider, invalid} - } + {account, notfound} | + {account, ff_party:inaccessibility()} | + {currency, invalid} | + {provider, invalid} }. -create(TrxID, Postings = [_ | _]) -> +create(ID, Postings = [_ | _]) -> do(fun () -> - Wallets = gather_wallets(Postings), - accessible = unwrap(wallet, validate_accessible(Wallets)), - valid = unwrap(wallet, validate_currencies(Wallets)), - valid = unwrap(wallet, validate_identities(Wallets)), + Accounts = maps:values(gather_accounts(Postings)), + valid = validate_currencies(Accounts), + valid = validate_identities(Accounts), + accessible = validate_accessible(Accounts), [ {created, #{ - trxid => TrxID, - postings => Postings, - status => created + id => ID, + postings => Postings }}, {status_changed, created @@ -103,54 +103,60 @@ create(TrxID, Postings = [_ | _]) -> create(_TrxID, []) -> {error, empty}. -gather_wallets(Postings) -> - lists:usort(lists:flatten([[S, D] || {S, D, _} <- Postings])). +gather_accounts(Postings) -> + maps:from_list([ + {AccountID, get_account(AccountID)} || + AccountID <- lists:usort(lists:flatten([[S, D] || {S, D, _} <- Postings])) + ]). -validate_accessible(Wallets) -> - do(fun () -> - _ = [accessible = unwrap(ff_wallet:is_accessible(W)) || W <- Wallets], - accessible - end). +%% TODO +%% - Not the right place. +get_account({wallet, ID}) -> + St = unwrap(account, ff_wallet_machine:get(ID)), + ff_wallet:account(ff_wallet_machine:wallet(St)); +get_account({destination, ID}) -> + St = unwrap(account, ff_destination_machine:get(ID)), + ff_destination:account(ff_destination_machine:destination(St)). -validate_currencies([W0 | Wallets]) -> - do(fun () -> - Currency = ff_wallet:currency(W0), - _ = [ok = unwrap(currency, valid(Currency, ff_wallet:currency(W))) || W <- Wallets], - valid - end). +validate_accessible(Accounts) -> + _ = [accessible = unwrap(account, ff_account:is_accessible(A)) || A <- Accounts], + accessible. -validate_identities([W0 | Wallets]) -> - do(fun () -> - Provider = ff_identity:provider(ff_wallet:identity(W0)), - _ = [ - ok = unwrap(provider, valid(Provider, ff_identity:provider(ff_wallet:identity(W)))) || - W <- Wallets - ], - valid - end). +validate_currencies([A0 | Accounts]) -> + Currency = ff_account:currency(A0), + _ = [ok = unwrap(currency, valid(Currency, ff_account:currency(A))) || A <- Accounts], + valid. + +validate_identities([A0 | Accounts]) -> + {ok, IdentitySt} = ff_identity_machine:get(ff_account:identity(A0)), + Identity0 = ff_identity_machine:identity(IdentitySt), + ProviderID0 = ff_identity:provider(Identity0), + _ = [ + ok = unwrap(provider, valid(ProviderID0, ff_identity:provider(ff_identity_machine:identity(Identity)))) || + Account <- Accounts, + {ok, Identity} <- [ff_identity_machine:get(ff_account:identity(Account))] + ], + valid. %% -spec prepare(transfer()) -> - {ok, outcome()} | + {ok, [event()]} | {error, - balance | - {status, committed | cancelled} | - {wallet, {inaccessible, blocked | suspended}} + {status, committed | cancelled} }. prepare(Transfer = #{status := created}) -> - TrxID = trxid(Transfer), + ID = id(Transfer), Postings = postings(Transfer), do(fun () -> - accessible = unwrap(wallet, validate_accessible(gather_wallets(Postings))), - _Affected = unwrap(ff_transaction:prepare(TrxID, construct_trx_postings(Postings))), + _Affected = unwrap(ff_transaction:prepare(ID, construct_trx_postings(Postings))), [{status_changed, prepared}] end); -prepare(_Transfer = #{status := prepared}) -> +prepare(#{status := prepared}) -> {ok, []}; prepare(#{status := Status}) -> - {error, {status, Status}}. + {error, Status}. %% TODO % validate_balances(Affected) -> @@ -159,17 +165,17 @@ prepare(#{status := Status}) -> %% -spec commit(transfer()) -> - {ok, outcome()} | + {ok, [event()]} | {error, {status, created | cancelled}}. commit(Transfer = #{status := prepared}) -> - TrxID = trxid(Transfer), - Postings = postings(Transfer), + ID = id(Transfer), + Postings = postings(Transfer), do(fun () -> - _Affected = unwrap(ff_transaction:commit(TrxID, construct_trx_postings(Postings))), + _Affected = unwrap(ff_transaction:commit(ID, construct_trx_postings(Postings))), [{status_changed, committed}] end); -commit(_Transfer = #{status := committed}) -> +commit(#{status := committed}) -> {ok, []}; commit(#{status := Status}) -> {error, Status}. @@ -177,31 +183,38 @@ commit(#{status := Status}) -> %% -spec cancel(transfer()) -> - {ok, outcome()} | + {ok, [event()]} | {error, {status, created | committed}}. cancel(Transfer = #{status := prepared}) -> + ID = id(Transfer), + Postings = postings(Transfer), do(fun () -> - Postings = construct_trx_postings(postings(Transfer)), - _Affected = unwrap(ff_transaction:cancel(trxid(Transfer), Postings)), + _Affected = unwrap(ff_transaction:cancel(ID, construct_trx_postings(Postings))), [{status_changed, cancelled}] end); -cancel(_Transfer = #{status := cancelled}) -> +cancel(#{status := cancelled}) -> {ok, []}; cancel(#{status := Status}) -> {error, {status, Status}}. %% +-spec apply_event(event(), ff_maybe:maybe(account())) -> + account(). + apply_event({created, Transfer}, undefined) -> Transfer; apply_event({status_changed, S}, Transfer) -> - Transfer#{status := S}. + Transfer#{status => S}. %% construct_trx_postings(Postings) -> + Accounts = gather_accounts(Postings), [ - {unwrap(ff_wallet:account(Source)), unwrap(ff_wallet:account(Destination)), Body} || - {Source, Destination, Body} <- Postings + {SourceAccount, DestinationAccount, Body} || + {Source, Destination, Body} <- Postings, + SourceAccount <- [ff_account:pm_account(maps:get(Source, Accounts))], + DestinationAccount <- [ff_account:pm_account(maps:get(Destination, Accounts))] ]. diff --git a/apps/fistful/src/ff_wallet.erl b/apps/fistful/src/ff_wallet.erl index b195bdf..8241889 100644 --- a/apps/fistful/src/ff_wallet.erl +++ b/apps/fistful/src/ff_wallet.erl @@ -4,136 +4,87 @@ -module(ff_wallet). --type identity() :: ff_identity:identity(). +-type account() :: ff_account:id(). + +-type id(T) :: T. +-type identity() :: ff_identity:id(). -type currency() :: ff_currency:id(). --type wid() :: ff_party:wallet(). --type id(T) :: T. -type wallet() :: #{ - id := id(_), - identity := identity(), - name := binary(), - currency := currency(), - wid => wid() + account := account(), + name := binary() }. --type ev() :: +-type event() :: {created, wallet()} | - {wid_set, wid()}. - --type outcome() :: [ev()]. + {account, ff_account:event()}. -export_type([wallet/0]). --export_type([ev/0]). +-export_type([event/0]). +-export([account/1]). -export([id/1]). -export([identity/1]). -export([name/1]). -export([currency/1]). --export([account/1]). -export([create/4]). --export([setup_wallet/1]). -export([is_accessible/1]). -export([close/1]). --export([collapse_events/1]). --export([apply_events/2]). -export([apply_event/2]). --export([dehydrate/1]). --export([hydrate/2]). - %% Pipeline --import(ff_pipeline, [do/1, unwrap/1, unwrap/2]). +-import(ff_pipeline, [do/1, unwrap/1]). %% Accessors --spec id(wallet()) -> id(_). --spec identity(wallet()) -> identity(). --spec name(wallet()) -> binary(). --spec currency(wallet()) -> currency(). +-spec account(wallet()) -> account(). -id(#{id := V}) -> V. -identity(#{identity := V}) -> V. -name(#{name := V}) -> V. -currency(#{currency := V}) -> V. +-spec id(wallet()) -> + id(_). +-spec identity(wallet()) -> + identity(). +-spec name(wallet()) -> + binary(). +-spec currency(wallet()) -> + currency(). --spec wid(wallet()) -> ff_map:result(wid()). +account(#{account := V}) -> + V. -wid(Wallet) -> - ff_map:find(wid, Wallet). - --spec account(wallet()) -> - {ok, ff_transaction:account()} | - {error, notfound}. - -account(Wallet) -> - do(fun () -> - WID = unwrap(wid(Wallet)), - unwrap(ff_party:get_wallet_account(ff_identity:party(identity(Wallet)), WID)) - end). +id(Wallet) -> + ff_account:id(account(Wallet)). +identity(Wallet) -> + ff_account:identity(account(Wallet)). +name(Wallet) -> + maps:get(name, Wallet, <<>>). +currency(Wallet) -> + ff_account:currency(account(Wallet)). %% -spec create(id(_), identity(), binary(), currency()) -> - {ok, outcome()}. + {ok, [event()]}. -create(ID, Identity, Name, Currency) -> +create(ID, IdentityID, Name, CurrencyID) -> do(fun () -> - [{created, #{ - id => ID, - identity => Identity, - name => Name, - currency => Currency - }}] - end). - --spec setup_wallet(wallet()) -> - {ok, outcome()} | - {error, - {inaccessible, blocked | suspended} | - {contract, notfound} | - invalid - }. - -setup_wallet(Wallet) -> - do(fun () -> - Identity = identity(Wallet), - accessible = unwrap(ff_identity:is_accessible(Identity)), - Contract = unwrap(contract, ff_identity:contract(Identity)), - Prototype = #{ - name => name(Wallet), - currency => currency(Wallet) - }, - % TODO - % - There is an opportunity for a race where someone can block party - % right before we create a party wallet. - WID = unwrap(ff_party:create_wallet(ff_identity:party(Identity), Contract, Prototype)), - [{wid_set, WID}] + [{created, #{name => Name}}] ++ + [{account, Ev} || Ev <- unwrap(ff_account:create(ID, IdentityID, CurrencyID))] end). -spec is_accessible(wallet()) -> {ok, accessible} | - {error, - {wid, notfound} | - {inaccessible, suspended | blocked} - }. + {error, ff_party:inaccessibility()}. is_accessible(Wallet) -> - do(fun () -> - Identity = identity(Wallet), - WID = unwrap(wid, wid(Wallet)), - accessible = unwrap(ff_identity:is_accessible(Identity)), - accessible = unwrap(ff_party:is_wallet_accessible(ff_identity:party(Identity), WID)), - accessible - end). + ff_account:is_accessible(account(Wallet)). -spec close(wallet()) -> - {ok, outcome()} | + {ok, [event()]} | {error, - {inaccessible, blocked | suspended} | + ff_party:inaccessibility() | {account, pending} }. @@ -146,51 +97,12 @@ close(Wallet) -> %% --spec collapse_events([ev(), ...]) -> - wallet(). - -collapse_events(Evs) when length(Evs) > 0 -> - apply_events(Evs, undefined). - --spec apply_events([ev()], undefined | wallet()) -> - undefined | wallet(). - -apply_events(Evs, Identity) -> - lists:foldl(fun apply_event/2, Identity, Evs). - --spec apply_event(ev(), undefined | wallet()) -> +-spec apply_event(event(), undefined | wallet()) -> wallet(). apply_event({created, Wallet}, undefined) -> Wallet; -apply_event({wid_set, WID}, Wallet) -> - Wallet#{wid => WID}. - -%% - --spec dehydrate(ev()) -> - term(). - --spec hydrate(term(), undefined | wallet()) -> - ev(). - -dehydrate({created, Wallet}) -> - {created, #{ - id => id(Wallet), - name => name(Wallet), - identity => ff_identity:id(identity(Wallet)), - currency => currency(Wallet) - }}; -dehydrate({wid_set, WID}) -> - {wid_set, WID}. - -hydrate({created, V}, undefined) -> - {ok, IdentitySt} = ff_identity_machine:get(maps:get(identity, V)), - {created, #{ - id => maps:get(id, V), - name => maps:get(name, V), - identity => ff_identity_machine:identity(IdentitySt), - currency => maps:get(currency, V) - }}; -hydrate({wid_set, WID}, _) -> - {wid_set, WID}. +apply_event({account, Ev}, Wallet = #{account := Account}) -> + Wallet#{account := ff_account:apply_event(Ev, Account)}; +apply_event({account, Ev}, Wallet) -> + apply_event({account, Ev}, Wallet#{account => undefined}). diff --git a/apps/fistful/src/ff_wallet_machine.erl b/apps/fistful/src/ff_wallet_machine.erl index 23c4eb1..3af62c9 100644 --- a/apps/fistful/src/ff_wallet_machine.erl +++ b/apps/fistful/src/ff_wallet_machine.erl @@ -10,15 +10,10 @@ -module(ff_wallet_machine). -type id() :: machinery:id(). --type timestamp() :: machinery:timestamp(). -type wallet() :: ff_wallet:wallet(). -type ctx() :: ff_ctx:ctx(). --type st() :: #{ - wallet := wallet(), - ctx := ctx(), - times => {timestamp(), timestamp()} -}. +-type st() :: ff_machine:st(wallet()). -export_type([id/0]). @@ -28,9 +23,6 @@ %% Accessors -export([wallet/1]). --export([ctx/1]). --export([created/1]). --export([updated/1]). %% Machinery @@ -42,22 +34,14 @@ %% Pipeline --import(ff_pipeline, [do/1, unwrap/1, unwrap/2]). +-import(ff_pipeline, [do/1, unwrap/1]). %% Accessors -spec wallet(st()) -> wallet(). --spec ctx(st()) -> ctx(). --spec created(st()) -> timestamp() | undefined. --spec updated(st()) -> timestamp() | undefined. -wallet(#{wallet := V}) -> V. -ctx(#{ctx := V}) -> V. -created(St) -> erlang:element(1, times(St)). -updated(St) -> erlang:element(2, times(St)). - -times(St) -> - genlib_map:get(times, St, {undefined, undefined}). +wallet(St) -> + ff_machine:model(St). %% @@ -72,19 +56,14 @@ times(St) -> -spec create(id(), params(), ctx()) -> ok | {error, - {identity, notfound} | - {currency, notfound} | - _WalletError | + _WalletCreateError | exists }. -create(ID, #{identity := IdentityID, name := Name, currency := Currency}, Ctx) -> +create(ID, #{identity := IdentityID, name := Name, currency := CurrencyID}, Ctx) -> do(fun () -> - Identity = ff_identity_machine:identity(unwrap(identity, ff_identity_machine:get(IdentityID))), - _ = unwrap(currency, ff_currency:get(Currency)), - Events0 = unwrap(ff_wallet:create(ID, Identity, Name, Currency)), - Events1 = unwrap(ff_wallet:setup_wallet(ff_wallet:collapse_events(Events0))), - unwrap(machinery:start(?NS, ID, {Events0 ++ Events1, Ctx}, backend())) + Events = unwrap(ff_wallet:create(ID, IdentityID, Name, CurrencyID)), + unwrap(machinery:start(?NS, ID, {Events, Ctx}, fistful:backend(?NS))) end). -spec get(id()) -> @@ -92,32 +71,23 @@ create(ID, #{identity := IdentityID, name := Name, currency := Currency}, Ctx) - {error, notfound} . get(ID) -> - do(fun () -> - collapse(unwrap(machinery:get(?NS, ID, backend()))) - end). - -backend() -> - fistful:backend(?NS). + ff_machine:get(ff_wallet, ?NS, ID). %% machinery --type ev() :: - ff_wallet:ev(). +-type event() :: + ff_wallet:event(). --type auxst() :: - #{ctx => ctx()}. - --type ts_ev(T) :: {ev, timestamp(), T}. --type machine() :: machinery:machine(ts_ev(ev()), auxst()). --type result() :: machinery:result(ts_ev(ev()), auxst()). +-type machine() :: ff_machine:machine(event()). +-type result() :: ff_machine:result(event()). -type handler_opts() :: machinery:handler_opts(_). --spec init({[ev()], ctx()}, machine(), _, handler_opts()) -> +-spec init({[event()], ctx()}, machine(), _, handler_opts()) -> result(). init({Events, Ctx}, #{}, _, _Opts) -> #{ - events => emit_ts_events(Events), + events => ff_machine:emit_events(Events), aux_state => #{ctx => Ctx} }. @@ -132,36 +102,3 @@ process_timeout(#{}, _, _Opts) -> process_call(_CallArgs, #{}, _, _Opts) -> {ok, #{}}. - -%% - -collapse(#{history := History, aux_state := #{ctx := Ctx}}) -> - collapse_history(History, #{ctx => Ctx}). - -collapse_history(History, St) -> - lists:foldl(fun merge_event/2, St, History). - -merge_event({_ID, _Ts, TsEv}, St0) -> - {EvBody, St1} = merge_ts_event(TsEv, St0), - merge_event_body(ff_wallet:hydrate(EvBody, maybe(wallet, St1)), St1). - -merge_event_body(Ev, St) -> - St#{ - wallet => ff_wallet:apply_event(Ev, maybe(wallet, St)) - }. - -maybe(Key, St) -> - maps:get(Key, St, undefined). - -%% - -emit_ts_events(Es) -> - emit_ts_events(Es, machinery_time:now()). - -emit_ts_events(Es, Ts) -> - [{ev, Ts, ff_wallet:dehydrate(Body)} || Body <- Es]. - -merge_ts_event({ev, Ts, Body}, St = #{times := {Created, _Updated}}) -> - {Body, St#{times => {Created, Ts}}}; -merge_ts_event({ev, Ts, Body}, St = #{}) -> - {Body, St#{times => {Ts, Ts}}}. diff --git a/apps/fistful/test/ff_wallet_SUITE.erl b/apps/fistful/test/ff_wallet_SUITE.erl index abca460..e2d16c4 100644 --- a/apps/fistful/test/ff_wallet_SUITE.erl +++ b/apps/fistful/test/ff_wallet_SUITE.erl @@ -161,9 +161,9 @@ create_wallet_ok(C) -> }, ff_ctx:new() ), - W = ff_wallet_machine:wallet(unwrap(ff_wallet_machine:get(ID))), - {ok, accessible} = ff_wallet:is_accessible(W), - {ok, Account} = ff_wallet:account(W), + Wallet = ff_wallet_machine:wallet(unwrap(ff_wallet_machine:get(ID))), + {ok, accessible} = ff_wallet:is_accessible(Wallet), + Account = ff_account:pm_account(ff_wallet:account(Wallet)), {ok, {Amount, <<"RUB">>}} = ff_transaction:balance(Account), 0 = ff_indef:current(Amount), ok. @@ -252,16 +252,8 @@ get_domain_config(C) -> get_default_termset() -> #domain_TermSet{ - % TODO - % - Strangely enough, hellgate checks wallet currency against _payments_ - % terms. - payments = #domain_PaymentsServiceTerms{ + wallets = #domain_WalletServiceTerms{ currencies = {value, ?ordset([?cur(<<"RUB">>)])}, - categories = {value, ?ordset([?cat(1)])}, - payment_methods = {value, ?ordset([ - ?pmt(bank_card, visa), - ?pmt(bank_card, mastercard) - ])}, cash_limit = {decisions, [ #domain_CashLimitDecision{ if_ = {condition, {currency_is, ?cur(<<"RUB">>)}}, @@ -270,18 +262,6 @@ get_default_termset() -> {exclusive, ?cash(10000000, <<"RUB">>)} )} } - ]}, - fees = {decisions, [ - #domain_CashFlowDecision{ - if_ = {condition, {currency_is, ?cur(<<"RUB">>)}}, - then_ = {value, [ - ?cfpost( - {merchant, settlement}, - {system, settlement}, - ?share(3, 100, operation_amount) - ) - ]} - } ]} } }. diff --git a/apps/wapi/src/wapi_swagger_server.erl b/apps/wapi/src/wapi_swagger_server.erl index 00799e5..638fb17 100644 --- a/apps/wapi/src/wapi_swagger_server.erl +++ b/apps/wapi/src/wapi_swagger_server.erl @@ -18,14 +18,13 @@ child_spec({HealthRoutes, LogicHandlers}) -> {Transport, TransportOpts} = get_socket_transport(), CowboyOpts = get_cowboy_config(HealthRoutes, LogicHandlers), - AcceptorsPool = genlib_app:env(?APP, acceptors_poolsize, ?DEFAULT_ACCEPTORS_POOLSIZE), - ranch:child_spec(?MODULE, AcceptorsPool, - Transport, TransportOpts, cowboy_protocol, CowboyOpts). + ranch:child_spec(?MODULE, Transport, TransportOpts, cowboy_protocol, CowboyOpts). get_socket_transport() -> {ok, IP} = inet:parse_address(genlib_app:env(?APP, ip, ?DEFAULT_IP_ADDR)), Port = genlib_app:env(?APP, port, ?DEFAULT_PORT), - {ranch_tcp, [{ip, IP}, {port, Port}]}. + NumAcceptors = genlib_app:env(?APP, acceptors_poolsize, ?DEFAULT_ACCEPTORS_POOLSIZE), + {ranch_tcp, [{ip, IP}, {port, Port}, {num_acceptors, NumAcceptors}]}. get_cowboy_config(HealthRoutes, LogicHandlers) -> Dispatch = diff --git a/apps/wapi/src/wapi_wallet_ff_backend.erl b/apps/wapi/src/wapi_wallet_ff_backend.erl index bd6b168..1602c5f 100644 --- a/apps/wapi/src/wapi_wallet_ff_backend.erl +++ b/apps/wapi/src/wapi_wallet_ff_backend.erl @@ -1,5 +1,3 @@ -%% Temporary stab for wallet handler - -module(wapi_wallet_ff_backend). -include_lib("dmsl/include/dmsl_payment_processing_thrift.hrl"). @@ -53,14 +51,14 @@ -spec get_providers([binary()], ctx()) -> [map()]. get_providers(Residences, _Context) -> - ResidenceSet = ordsets:from_list(from_swag(list, {residence, Residences})), - to_swag(list, {provider, [P || + ResidenceSet = ordsets:from_list(from_swag({list, residence}, Residences)), + to_swag({list, provider}, [P || P <- ff_provider:list(), ordsets:is_subset( ResidenceSet, ordsets:from_list(ff_provider:residences(P)) ) - ]}). + ]). -spec get_provider(id(), ctx()) -> result(). get_provider(ProviderId, _Context) -> @@ -133,7 +131,7 @@ get_identity_challenges(IdentityId, Statuses, Context) -> Challenges0 = maps:to_list(ff_identity:challenges( ff_identity_machine:identity(get_state(identity, IdentityId, Context)) )), - to_swag(list, {identity_challenge, [ + to_swag({list, identity_challenge}, [ {Id, C, enrich_proofs(ff_identity_challenge:proofs(C), Context)} || {Id, C} <- Challenges0, Status <- [ff_identity_challenge:status(C)], @@ -141,7 +139,7 @@ get_identity_challenges(IdentityId, Statuses, Context) -> fun (F) -> filter_identity_challenge_status(F, Status) end, Statuses ) - ]}) + ]) end). -spec create_identity_challenge(id(), params(), ctx()) -> result(map(), @@ -363,10 +361,10 @@ get_event(Type, ResourceId, EventId, Mapper, Context) -> get_events(Type = {Resource, _}, ResourceId, Limit, Cursor, Filter, Context) -> do(fun() -> _ = check_resource(Resource, ResourceId, Context), - to_swag(list, { - get_event_type(Type), + to_swag( + {list, get_event_type(Type)}, collect_events(get_collector(Type, ResourceId), Filter, Cursor, Limit) - }) + ) end). get_event_type({identity, challenge_event}) -> identity_challenge_event; @@ -484,6 +482,17 @@ next_id(Type) -> ). %% Marshalling + +-type swag_term() :: + #{binary() => swag_term()} | + [swag_term()] | + number() | + binary() | + boolean() . + +-spec from_swag(_Type, swag_term()) -> + _Term. + from_swag(identity_params, Params) -> #{ provider => maps:get(<<"provider">>, Params), @@ -495,7 +504,7 @@ from_swag(identity_challenge_params, Params) -> proofs => from_swag(proofs, maps:get(<<"proofs">>, Params)) }; from_swag(proofs, Proofs) -> - from_swag(list, {proof, Proofs}); + from_swag({list, proof}, Proofs); from_swag(proof, #{<<"token">> := WapiToken}) -> try #{<<"type">> := Type, <<"token">> := Token} = wapi_utils:base64url_to_map(WapiToken), @@ -554,21 +563,23 @@ from_swag(residence, V) -> undefined end; -from_swag(list, {Type, List}) -> +from_swag({list, Type}, List) -> lists:map(fun(V) -> from_swag(Type, V) end, List). +-spec to_swag(_Type, _Value) -> + swag_term() | undefined. to_swag(_, undefined) -> undefined; to_swag(providers, Providers) -> - to_swag(list, {provider, Providers}); + to_swag({list, provider}, Providers); to_swag(provider, Provider) -> to_swag(map, #{ <<"id">> => ff_provider:id(Provider), <<"name">> => ff_provider:name(Provider), - <<"residences">> => to_swag(list, {residence, + <<"residences">> => to_swag({list, residence}, ordsets:to_list(ff_provider:residences(Provider)) - }) + ) }); to_swag(residence, Residence) -> genlib_string:to_upper(genlib:to_binary(Residence)); @@ -586,10 +597,10 @@ to_swag(identity, State) -> to_swag(map, #{ <<"id">> => ff_identity:id(Identity), <<"name">> => maps:get(<<"name">>, WapiCtx), - <<"createdAt">> => to_swag(timestamp, ff_identity_machine:created(State)), - <<"provider">> => ff_provider:id(ff_identity:provider(Identity)), - <<"class">> => ff_identity_class:id(ff_identity:class(Identity)), - <<"level">> => ff_identity_class:level_id(ff_identity:level(Identity)), + <<"createdAt">> => to_swag(timestamp, ff_machine:created(State)), + <<"provider">> => ff_identity:provider(Identity), + <<"class">> => ff_identity:class(Identity), + <<"level">> => ff_identity:level(Identity), <<"effectiveChallenge">> => to_swag(identity_effective_challenge, ff_identity:effective_challenge(Identity)), <<"isBlocked">> => to_swag(is_blocked, ff_identity:is_accessible(Identity)), <<"metadata">> => maps:get(<<"metadata">>, WapiCtx, undefined) @@ -604,7 +615,7 @@ to_swag(identity_challenge, {ChallengeId, Challenge, Proofs}) -> <<"id">> => ChallengeId, %% TODO add createdAt when it is available on the backend %% <<"createdAt">> => _, - <<"type">> => ff_identity_class:challenge_class_id(ChallengeClass), + <<"type">> => ChallengeClass, <<"proofs">> => Proofs }, to_swag(challenge_status, ff_identity_challenge:status(Challenge)))); to_swag(challenge_status, pending) -> @@ -648,7 +659,7 @@ to_swag(wallet, State) -> <<"name">> => ff_wallet:name(Wallet), <<"createdAt">> => to_swag(timestamp, ff_machine:created(State)), <<"isBlocked">> => to_swag(is_blocked, ff_wallet:is_accessible(Wallet)), - <<"identity">> => ff_identity:id(ff_wallet:identity(Wallet)), + <<"identity">> => ff_wallet:identity(Wallet), <<"currency">> => to_swag(currency, ff_wallet:currency(Wallet)), <<"metadata">> => genlib_map:get(<<"metadata">>, get_ctx(State)) }); @@ -666,15 +677,14 @@ to_swag(wallet_account, {OwnAmount, AvailableAmount, Currency}) -> }; to_swag(destination, State) -> Destination = ff_destination_machine:destination(State), - Wallet = ff_destination:wallet(Destination), to_swag(map, maps:merge( #{ <<"id">> => ff_destination:id(Destination), - <<"name">> => ff_wallet:name(Wallet), + <<"name">> => ff_destination:name(Destination), <<"createdAt">> => to_swag(timestamp, ff_machine:created(State)), - <<"isBlocked">> => to_swag(is_blocked, ff_wallet:is_accessible(Wallet)), - <<"identity">> => ff_identity:id(ff_wallet:identity(Wallet)), - <<"currency">> => to_swag(currency, ff_wallet:currency(Wallet)), + <<"isBlocked">> => to_swag(is_blocked, ff_destination:is_accessible(Destination)), + <<"identity">> => ff_destination:identity(Destination), + <<"currency">> => to_swag(currency, ff_destination:currency(Destination)), <<"resource">> => to_swag(destination_resource, ff_destination:resource(Destination)), <<"metadata">> => genlib_map:get(<<"metadata">>, get_ctx(State)) }, @@ -705,10 +715,10 @@ to_swag(withdrawal, State) -> to_swag(map, maps:merge( #{ <<"id">> => ff_withdrawal:id(Withdrawal), - <<"createdAt">> => to_swag(timestamp, ff_withdrawal_machine:created(State)), + <<"createdAt">> => to_swag(timestamp, ff_machine:created(State)), <<"metadata">> => genlib_map:get(<<"metadata">>, get_ctx(State)), - <<"wallet">> => ff_wallet:id(ff_withdrawal:source(Withdrawal)), - <<"destination">> => ff_destination:id(ff_withdrawal:destination(Withdrawal)), + <<"wallet">> => ff_withdrawal:source(Withdrawal), + <<"destination">> => ff_withdrawal:destination(Withdrawal), <<"body">> => to_swag(withdrawal_body, ff_withdrawal:body(Withdrawal)) }, to_swag(withdrawal_status, ff_withdrawal:status(Withdrawal)) @@ -757,7 +767,7 @@ to_swag(is_blocked, {ok, accessible}) -> to_swag(is_blocked, _) -> true; -to_swag(list, {Type, List}) -> +to_swag({list, Type}, List) -> lists:map(fun(V) -> to_swag(Type, V) end, List); to_swag(map, Map) -> genlib_map:compact(Map); diff --git a/docker-compose.sh b/docker-compose.sh index 032d0a8..13be578 100755 --- a/docker-compose.sh +++ b/docker-compose.sh @@ -27,7 +27,7 @@ services: condition: service_healthy hellgate: - image: dr.rbkmoney.com/rbkmoney/hellgate:88abe5c9c3febf567e7269e81b2b808c01500b43 + image: dr.rbkmoney.com/rbkmoney/hellgate:eae821f2a1f3f2b948390d922c5eb3cde885757d command: /opt/hellgate/bin/hellgate foreground depends_on: machinegun: @@ -82,7 +82,7 @@ services: retries: 20 dominant: - image: dr.rbkmoney.com/rbkmoney/dominant:1756bbac6999fa46fbe44a72c74c02e616eda0f6 + image: dr.rbkmoney.com/rbkmoney/dominant:4e296b03cd4adba4bd0f1cf85425b9514728107c command: /opt/dominant/bin/dominant foreground depends_on: machinegun: