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
This commit is contained in:
Andrew Mayorov 2018-07-16 17:21:17 +03:00 committed by GitHub
parent ce160a7783
commit 7c7ad60e1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1010 additions and 1131 deletions

View File

@ -10,20 +10,25 @@
* [x] Реализовать честный identity challenge * [x] Реализовать честный identity challenge
* [x] Запилить payment provider interface * [x] Запилить payment provider interface
* [ ] Запилить контактные данные личности * [ ] Запилить контактные данные личности
* [ ] Запилить отмену identity challenge * [x] Запилить нормально трансферы
* [ ] Запилить авторизацию по активной идентификации * [ ] Заворачивать изменения в единственный ивент в рамках операции
* [.] Компактизировать состояние сессий
* [ ] Запилить контроль лимитов по кошелькам * [ ] Запилить контроль лимитов по кошелькам
* [ ] Запилить авторизацию по активной идентификации
* [ ] Запилить отмену identity challenge
* [ ] Запускать выводы через оплату инвойса провайдеру выводов * [ ] Запускать выводы через оплату инвойса провайдеру выводов
* [ ] Обслуживать выводы по факту оплаты инвойса * [ ] Обслуживать выводы по факту оплаты инвойса
### Корректность ### Корректность
* [.] Схема хранения моделей * [.] Схема хранения моделей
* [ ] [Дегидратация](#дегидратация)
* [ ] [Поддержка checkout](#поддержка-checkout) * [ ] [Поддержка checkout](#поддержка-checkout)
* [ ] [Коммуналка](#коммуналка) * [ ] [Коммуналка](#коммуналка)
### Удобство поддержки ### Удобство поддержки
* [ ] Добавить [служебные лимиты](#служебные-лимиты) в рамках одного party
* [ ] Добавить ручную прополку для всех асинхронных процессов * [ ] Добавить ручную прополку для всех асинхронных процессов
* [ ] Вынести _ff_withdraw_ в отдельный сервис * [ ] Вынести _ff_withdraw_ в отдельный сервис
* [ ] Разделить _development_, _release_ и _test_ зависимости * [ ] Разделить _development_, _release_ и _test_ зависимости
@ -36,3 +41,11 @@
## Коммуналка ## Коммуналка
Сервис должен давать возможность работать ескольким_ клиентам, которые возможно не знают ничего друг о друге кроме того, что у них разные _tenant id_. В идеале _tenant_ должен иметь возможность давать знать о себе _динамически_, в рантайме, однако это довольно трудоёмкая задача. Если приводить аналогию с _Riak KV_, клиенты к нему могут: создать новый _bucket type_ с необходимыми характеристиками, создать новый _bucket_ с требуемыми параметрами N/R/W и так далее. Сервис должен давать возможность работать ескольким_ клиентам, которые возможно не знают ничего друг о друге кроме того, что у них разные _tenant id_. В идеале _tenant_ должен иметь возможность давать знать о себе _динамически_, в рантайме, однако это довольно трудоёмкая задача. Если приводить аналогию с _Riak KV_, клиенты к нему могут: создать новый _bucket type_ с необходимыми характеристиками, создать новый _bucket_ с требуемыми параметрами N/R/W и так далее.
## Дегидратация
В итоге как будто бы не самая здравая идея. Есть ощущение, что проще и дешевле хранить и оперировать идентификаторами, и разыменовывать их каждый раз по необходимости.
## Служебные лимиты
Нужно уметь _ограничивать_ максимальное _ожидаемое_ количество тех или иных объектов, превышение которого может негативно влиять на качество обслуживания системы. Например, мы можем считать количество _выводов_ одним участником неограниченным, однако при этом неограниченное количество созданных _личностей_ мы совершенно не ожидаем. В этом случае возможно будет разумно ограничить их количество сверху труднодостижимой для подавляющего большинства планкой, например, в 1000 объектов. В идеале подобное должно быть точечно конфигурируемым.

View File

@ -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}).

View File

@ -7,6 +7,9 @@
-include_lib("identdocstore_proto/include/identdocstore_identity_document_storage_thrift.hrl"). -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) -> rus_domestic_passport(C) ->
Document = { Document = {
russian_domestic_passport, russian_domestic_passport,
@ -31,6 +34,9 @@ rus_domestic_passport(C) ->
{rus_domestic_passport, Token} {rus_domestic_passport, Token}
end. end.
-spec rus_retiree_insurance_cert(_Number :: binary(), ct_helper:config()) ->
{rus_retiree_insurance_cert, binary()}.
rus_retiree_insurance_cert(Number, C) -> rus_retiree_insurance_cert(Number, C) ->
Document = { Document = {
russian_retiree_insurance_certificate, russian_retiree_insurance_certificate,

View File

@ -9,11 +9,14 @@
-module(ff_destination). -module(ff_destination).
-type id() :: machinery:id(). -type account() :: ff_account:account().
-type wallet() :: ff_wallet:wallet().
-type resource() :: -type resource() ::
{bank_card, resource_bank_card()}. {bank_card, resource_bank_card()}.
-type id(T) :: T.
-type identity() :: ff_identity:id().
-type currency() :: ff_currency:id().
-type resource_bank_card() :: #{ -type resource_bank_card() :: #{
token := binary(), token := binary(),
payment_system => atom(), % TODO payment_system => atom(), % TODO
@ -26,75 +29,92 @@
authorized. authorized.
-type destination() :: #{ -type destination() :: #{
id := id(), account := account() | undefined,
resource := resource(), resource := resource(),
wallet => wallet(), name := binary(),
status => status() status := status()
}. }.
-type ev() :: -type event() ::
{created, destination()} | {created, destination()} |
{wallet, ff_wallet:ev()} | {account, ff_account:ev()} |
{status_changed, status()}. {status_changed, status()}.
-type outcome() :: [ev()].
-export_type([destination/0]). -export_type([destination/0]).
-export_type([status/0]). -export_type([status/0]).
-export_type([resource/0]). -export_type([resource/0]).
-export_type([ev/0]). -export_type([event/0]).
-export([account/1]).
-export([id/1]). -export([id/1]).
-export([wallet/1]). -export([name/1]).
-export([identity/1]).
-export([currency/1]).
-export([resource/1]). -export([resource/1]).
-export([status/1]). -export([status/1]).
-export([create/5]). -export([create/5]).
-export([authorize/1]). -export([authorize/1]).
-export([apply_event/2]). -export([is_accessible/1]).
-export([dehydrate/1]). -export([apply_event/2]).
-export([hydrate/2]).
%% Pipeline %% Pipeline
-import(ff_pipeline, [do/1, unwrap/1, unwrap/2]). -import(ff_pipeline, [do/1, unwrap/1]).
%% Accessors %% Accessors
-spec id(destination()) -> id(). -spec account(destination()) ->
-spec wallet(destination()) -> wallet(). account().
-spec resource(destination()) -> resource().
-spec status(destination()) -> status().
id(#{id := V}) -> V. account(#{account := V}) ->
wallet(#{wallet := V}) -> V. V.
resource(#{resource := V}) -> V.
status(#{status := 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()) -> -spec create(id(_), identity(), binary(), currency(), resource()) ->
{ok, outcome()} | {ok, [event()]} |
{error, _WalletError}. {error, _WalletError}.
create(ID, Identity, Name, Currency, Resource) -> create(ID, IdentityID, Name, CurrencyID, Resource) ->
do(fun () -> do(fun () ->
WalletEvents1 = unwrap(ff_wallet:create(ID, Identity, Name, Currency)), Events = unwrap(ff_account:create(ID, IdentityID, CurrencyID)),
WalletEvents2 = unwrap(ff_wallet:setup_wallet(ff_wallet:collapse_events(WalletEvents1))), [{created, #{name => Name, resource => Resource}}] ++
[ [{account, Ev} || Ev <- Events] ++
{created, #{
id => ID,
resource => Resource
}}
] ++
[{wallet, Ev} || Ev <- WalletEvents1 ++ WalletEvents2] ++
[{status_changed, unauthorized}] [{status_changed, unauthorized}]
end). end).
-spec authorize(destination()) -> -spec authorize(destination()) ->
{ok, outcome()} | {ok, [event()]} |
{error, _TODO}. {error, _TODO}.
authorize(#{status := unauthorized}) -> authorize(#{status := unauthorized}) ->
@ -104,40 +124,23 @@ authorize(#{status := unauthorized}) ->
authorize(#{status := authorized}) -> authorize(#{status := authorized}) ->
{ok, []}. {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(). destination().
apply_event({created, D}, undefined) -> apply_event({created, Destination}, undefined) ->
D; Destination;
apply_event({status_changed, S}, D) -> apply_event({status_changed, S}, Destination) ->
D#{status => S}; Destination#{status => S};
apply_event({wallet, Ev}, D) -> apply_event({account, Ev}, Destination = #{account := Account}) ->
D#{wallet => ff_wallet:apply_event(Ev, genlib_map:get(wallet, D))}. Destination#{account => ff_account:apply_event(Ev, Account)};
apply_event({account, Ev}, Destination) ->
-spec dehydrate(ev()) -> apply_event({account, Ev}, Destination#{account => undefined}).
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}.

View File

@ -7,20 +7,11 @@
%% API %% API
-type id() :: machinery:id(). -type id() :: machinery:id().
-type timestamp() :: machinery:timestamp().
-type ctx() :: ff_ctx:ctx(). -type ctx() :: ff_ctx:ctx().
-type destination() :: ff_destination:destination(). -type destination() :: ff_destination:destination().
-type activity() :: -type st() ::
undefined | ff_machine:st(destination()).
authorize .
-type st() :: #{
activity := activity(),
destination := destination(),
ctx := ctx(),
times => {timestamp(), timestamp()}
}.
-export_type([id/0]). -export_type([id/0]).
@ -30,10 +21,6 @@
%% Accessors %% Accessors
-export([destination/1]). -export([destination/1]).
-export([activity/1]).
-export([ctx/1]).
-export([created/1]).
-export([updated/1]).
%% Machinery %% Machinery
@ -45,14 +32,14 @@
%% Pipeline %% Pipeline
-import(ff_pipeline, [do/1, unwrap/1, unwrap/2]). -import(ff_pipeline, [do/1, unwrap/1]).
%% %%
-define(NS, 'ff/destination'). -define(NS, 'ff/destination').
-type params() :: #{ -type params() :: #{
identity := ff_identity_machine:id(), identity := ff_identity:id(),
name := binary(), name := binary(),
currency := ff_currency:id(), currency := ff_currency:id(),
resource := ff_destination:resource() resource := ff_destination:resource()
@ -61,18 +48,14 @@
-spec create(id(), params(), ctx()) -> -spec create(id(), params(), ctx()) ->
ok | ok |
{error, {error,
{identity, notfound} | _DestinationCreateError |
{currency, notfound} |
_DestinationError |
exists 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 () -> do(fun () ->
Identity = ff_identity_machine:identity(unwrap(identity, ff_identity_machine:get(IdentityID))), Events = unwrap(ff_destination:create(ID, IdentityID, Name, CurrencyID, Resource)),
_ = unwrap(currency, ff_currency:get(Currency)), unwrap(machinery:start(?NS, ID, {Events, Ctx}, fistful:backend(?NS)))
Events = unwrap(ff_destination:create(ID, Identity, Name, Currency, Resource)),
unwrap(machinery:start(?NS, ID, {Events, Ctx}, backend()))
end). end).
-spec get(id()) -> -spec get(id()) ->
@ -80,113 +63,62 @@ create(ID, #{identity := IdentityID, name := Name, currency := Currency, resourc
{error, notfound} . {error, notfound} .
get(ID) -> get(ID) ->
do(fun () -> ff_machine:get(ff_destination, ?NS, ID).
collapse(unwrap(machinery:get(?NS, ID, backend())))
end).
backend() ->
fistful:backend(?NS).
%% Accessors %% Accessors
-spec destination(st()) -> destination(). -spec destination(st()) ->
-spec activity(st()) -> activity(). destination().
-spec ctx(st()) -> ctx().
-spec created(st()) -> timestamp() | undefined.
-spec updated(st()) -> timestamp() | undefined.
destination(#{destination := V}) -> V. destination(St) ->
activity(#{activity := V}) -> V. ff_machine:model(St).
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}).
%% Machinery %% Machinery
-type ts_ev(T) :: -type event() ::
{ev, timestamp(), T}. ff_destination:event().
-type ev() :: -type machine() :: ff_machine:machine(event()).
ff_destination:ev(). -type result() :: ff_machine:result(event()).
-type auxst() ::
#{ctx => ctx()}.
-type machine() :: machinery:machine(ts_ev(ev()), auxst()).
-type result() :: machinery:result(ts_ev(ev()), auxst()).
-type handler_opts() :: machinery:handler_opts(_). -type handler_opts() :: machinery:handler_opts(_).
-spec init({[ev()], ctx()}, machine(), _, handler_opts()) -> -spec init({[event()], ctx()}, machine(), _, handler_opts()) ->
result(). result().
init({Events, Ctx}, #{}, _, _Opts) -> init({Events, Ctx}, #{}, _, _Opts) ->
#{ #{
events => emit_ts_events(Events), events => ff_machine:emit_events(Events),
action => continue, action => continue,
aux_state => #{ctx => Ctx} aux_state => #{ctx => Ctx}
}. }.
%%
-spec process_timeout(machine(), _, handler_opts()) -> -spec process_timeout(machine(), _, handler_opts()) ->
result(). result().
process_timeout(Machine, _, _Opts) -> 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), D0 = destination(St),
case ff_destination:authorize(D0) of case ff_destination:authorize(D0) of
{ok, Events} -> {ok, Events} ->
#{ #{
events => emit_ts_events(Events) events => ff_machine:emit_events(Events)
} }
end. end.
deduce_activity(#{status := unauthorized}) ->
authorize;
deduce_activity(#{}) ->
undefined.
%%
-spec process_call(_CallArgs, machine(), _, handler_opts()) -> -spec process_call(_CallArgs, machine(), _, handler_opts()) ->
{ok, result()}. {ok, result()}.
process_call(_CallArgs, #{}, _, _Opts) -> process_call(_CallArgs, #{}, _, _Opts) ->
{ok, #{}}. {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}}}.

View File

@ -5,19 +5,19 @@
-module(ff_withdrawal). -module(ff_withdrawal).
-type id(T) :: T. -type id(T) :: T.
-type wallet() :: ff_wallet:wallet(). -type wallet() :: ff_wallet:id(_).
-type destination() :: ff_destination:destination(). -type destination() :: ff_destination:id(_).
-type body() :: ff_transaction:body(). -type body() :: ff_transaction:body().
-type provider() :: ff_withdrawal_provider:provider(). -type provider() :: ff_withdrawal_provider:id().
-type transfer() :: ff_transfer:transfer(). -type transfer() :: ff_transfer:transfer().
-type withdrawal() :: #{ -type withdrawal() :: #{
id := id(_), id := id(binary()),
source := wallet(), source := wallet(),
destination := destination(), destination := destination(),
body := body(), body := body(),
provider := provider(), provider := provider(),
transfer => transfer(), transfer := ff_maybe:maybe(transfer()),
session => session(), session => session(),
status => status() status => status()
}. }.
@ -30,17 +30,15 @@
succeeded | succeeded |
{failed, _TODO} . {failed, _TODO} .
-type ev() :: -type event() ::
{created, withdrawal()} | {created, withdrawal()} |
{transfer, ff_transfer:ev()} | {transfer, ff_transfer:ev()} |
{session_started, session()} | {session_started, session()} |
{session_finished, session()} | {session_finished, session()} |
{status_changed, status()} . {status_changed, status()} .
-type outcome() :: [ev()].
-export_type([withdrawal/0]). -export_type([withdrawal/0]).
-export_type([ev/0]). -export_type([event/0]).
-export([id/1]). -export([id/1]).
-export([source/1]). -export([source/1]).
@ -50,8 +48,7 @@
-export([transfer/1]). -export([transfer/1]).
-export([status/1]). -export([status/1]).
-export([create/5]). -export([create/4]).
-export([create_transfer/1]).
-export([prepare_transfer/1]). -export([prepare_transfer/1]).
-export([commit_transfer/1]). -export([commit_transfer/1]).
-export([cancel_transfer/1]). -export([cancel_transfer/1]).
@ -60,25 +57,21 @@
%% Event source %% Event source
-export([collapse_events/1]).
-export([apply_event/2]). -export([apply_event/2]).
-export([dehydrate/1]).
-export([hydrate/2]).
%% Pipeline %% 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 %% Accessors
-spec id(withdrawal()) -> id(_). -spec id(withdrawal()) -> id(binary()).
-spec source(withdrawal()) -> wallet(). -spec source(withdrawal()) -> wallet().
-spec destination(withdrawal()) -> destination(). -spec destination(withdrawal()) -> destination().
-spec body(withdrawal()) -> body(). -spec body(withdrawal()) -> body().
-spec provider(withdrawal()) -> provider(). -spec provider(withdrawal()) -> provider().
-spec status(withdrawal()) -> status(). -spec status(withdrawal()) -> status().
-spec transfer(withdrawal()) -> {ok, transfer()} | {error | notfound}. -spec transfer(withdrawal()) -> transfer().
id(#{id := V}) -> V. id(#{id := V}) -> V.
source(#{source := V}) -> V. source(#{source := V}) -> V.
@ -86,70 +79,98 @@ destination(#{destination := V}) -> V.
body(#{body := V}) -> V. body(#{body := V}) -> V.
provider(#{provider := V}) -> V. provider(#{provider := V}) -> V.
status(#{status := V}) -> V. status(#{status := V}) -> V.
transfer(W) -> ff_map:find(transfer, W). transfer(#{transfer := V}) -> V.
%% %%
-spec create(id(_), wallet(), destination(), body(), provider()) -> -spec create(id(_), wallet(), destination(), body()) ->
{ok, outcome()}. {ok, [event()]} |
{error,
{source, notfound} |
{destination, notfound | unauthorized} |
{provider, notfound} |
_TransferError
}.
create(ID, Source, Destination, Body, Provider) -> create(ID, SourceID, DestinationID, Body) ->
do(fun () -> do(fun () ->
[ Source = ff_wallet_machine:wallet(
{created, #{ unwrap(source, ff_wallet_machine:get(SourceID))
id => ID, ),
source => Source, Destination = ff_destination_machine:destination(
destination => Destination, unwrap(destination, ff_destination_machine:get(DestinationID))
body => Body, ),
provider => Provider ok = unwrap(destination, valid(authorized, ff_destination:status(Destination))),
}}, ProviderID = unwrap(provider, ff_withdrawal_provider:choose(Source, Destination, Body)),
{status_changed, TransferEvents = unwrap(ff_transfer:create(
pending 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). end).
create_transfer(Withdrawal) -> construct_transfer_id(ID) ->
Source = source(Withdrawal), ID.
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(TrxID) -> -spec prepare_transfer(withdrawal()) ->
ff_string:join($/, [TrxID, transfer]). {ok, [event()]} |
{error, _TransferError}.
prepare_transfer(Withdrawal) -> prepare_transfer(Withdrawal) ->
with(transfer, Withdrawal, fun ff_transfer:prepare/1). with(transfer, Withdrawal, fun ff_transfer:prepare/1).
-spec commit_transfer(withdrawal()) ->
{ok, [event()]} |
{error, _TransferError}.
commit_transfer(Withdrawal) -> commit_transfer(Withdrawal) ->
with(transfer, Withdrawal, fun ff_transfer:commit/1). with(transfer, Withdrawal, fun ff_transfer:commit/1).
-spec cancel_transfer(withdrawal()) ->
{ok, [event()]} |
{error, _TransferError}.
cancel_transfer(Withdrawal) -> cancel_transfer(Withdrawal) ->
with(transfer, Withdrawal, fun ff_transfer:cancel/1). with(transfer, Withdrawal, fun ff_transfer:cancel/1).
-spec create_session(withdrawal()) ->
{ok, [event()]} |
{error, _SessionError}.
create_session(Withdrawal) -> create_session(Withdrawal) ->
SID = construct_session_id(id(Withdrawal)), ID = construct_session_id(id(Withdrawal)),
Source = source(Withdrawal), {ok, SourceSt} = ff_wallet_machine:get(source(Withdrawal)),
Destination = destination(Withdrawal), Source = ff_wallet_machine:wallet(SourceSt),
Provider = provider(Withdrawal), {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 = #{ WithdrawalParams = #{
id => SID, id => ID,
destination => Destination, destination => Destination,
cash => body(Withdrawal), cash => body(Withdrawal),
sender => ff_wallet:identity(Source), sender => ff_identity_machine:identity(SenderSt),
receiver => ff_wallet:identity(ff_destination:wallet(Destination)) receiver => ff_identity_machine:identity(ReceiverSt)
}, },
do(fun () -> do(fun () ->
ok = unwrap(ff_withdrawal_provider:create_session(SID, WithdrawalParams, Provider)), ok = unwrap(ff_withdrawal_provider:create_session(ID, WithdrawalParams, Provider)),
[{session_started, SID}] [{session_started, ID}]
end). end).
construct_session_id(TrxID) -> construct_session_id(ID) ->
TrxID. ID.
-spec poll_session_completion(withdrawal()) ->
{ok, [event()]}.
poll_session_completion(_Withdrawal = #{session := SID}) -> poll_session_completion(_Withdrawal = #{session := SID}) ->
{ok, Session} = ff_withdrawal_session_machine:get(SID), {ok, Session} = ff_withdrawal_session_machine:get(SID),
@ -174,77 +195,18 @@ poll_session_completion(_Withdrawal) ->
%% %%
-spec collapse_events([ev(), ...]) -> -spec apply_event(event(), ff_maybe:maybe(withdrawal())) ->
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()) ->
withdrawal(). withdrawal().
apply_event({created, W}, undefined) -> apply_event({created, W}, undefined) ->
W; W;
apply_event({status_changed, S}, W) -> apply_event({status_changed, S}, W) ->
maps:put(status, S, W); maps:put(status, S, W);
apply_event({transfer, Ev}, W = #{transfer := T}) ->
W#{transfer := ff_transfer:apply_event(Ev, T)};
apply_event({transfer, Ev}, W) -> apply_event({transfer, Ev}, W) ->
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) -> apply_event({session_started, S}, W) ->
maps:put(session, S, W); maps:put(session, S, W);
apply_event({session_finished, S}, W = #{session := S}) -> apply_event({session_finished, S}, W = #{session := S}) ->
maps:remove(session, W). 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}.

View File

@ -7,7 +7,6 @@
%% API %% API
-type id() :: machinery:id(). -type id() :: machinery:id().
-type timestamp() :: machinery:timestamp().
-type ctx() :: ff_ctx:ctx(). -type ctx() :: ff_ctx:ctx().
-type withdrawal() :: ff_withdrawal:withdrawal(). -type withdrawal() :: ff_withdrawal:withdrawal().
@ -19,12 +18,8 @@
cancel_transfer | cancel_transfer |
undefined . undefined .
-type st() :: #{ -type st() ::
activity := activity(), ff_machine:st(withdrawal()).
withdrawal := withdrawal(),
ctx := ctx(),
times => {timestamp(), timestamp()}
}.
-export_type([id/0]). -export_type([id/0]).
@ -35,10 +30,6 @@
%% Accessors %% Accessors
-export([withdrawal/1]). -export([withdrawal/1]).
-export([activity/1]).
-export([ctx/1]).
-export([created/1]).
-export([updated/1]).
%% Machinery %% Machinery
@ -50,7 +41,7 @@
%% Pipeline %% 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()) -> -spec create(id(), params(), ctx()) ->
ok | ok |
{error, {error,
{source, notfound} | _WithdrawalError |
{destination, notfound | unauthorized} |
{provider, notfound} |
_TransferError |
exists exists
}. }.
create(ID, #{source := SourceID, destination := DestinationID, body := Body}, Ctx) -> create(ID, #{source := SourceID, destination := DestinationID, body := Body}, Ctx) ->
do(fun () -> do(fun () ->
Source = ff_wallet_machine:wallet(unwrap(source,ff_wallet_machine:get(SourceID))), Events = unwrap(ff_withdrawal:create(ID, SourceID, DestinationID, Body)),
Destination = ff_destination_machine:destination( unwrap(machinery:start(?NS, ID, {Events, Ctx}, backend()))
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()))
end). end).
-spec get(id()) -> -spec get(id()) ->
@ -90,12 +71,10 @@ create(ID, #{source := SourceID, destination := DestinationID, body := Body}, Ct
{error, notfound}. {error, notfound}.
get(ID) -> get(ID) ->
do(fun () -> ff_machine:get(ff_withdrawal, ?NS, ID).
collapse(unwrap(machinery:get(?NS, ID, backend())))
end).
-spec events(id(), machinery:range()) -> -spec events(id(), machinery:range()) ->
{ok, [{integer(), ts_ev(ev())}]} | {ok, [{integer(), ff_machine:timestamped_event(event())}]} |
{error, notfound}. {error, notfound}.
events(ID, Range) -> events(ID, Range) ->
@ -109,98 +88,92 @@ backend() ->
%% Accessors %% Accessors
-spec withdrawal(st()) -> withdrawal(). -spec withdrawal(st()) ->
-spec activity(st()) -> activity(). withdrawal().
-spec ctx(st()) -> ctx().
-spec created(st()) -> timestamp() | undefined.
-spec updated(st()) -> timestamp() | undefined.
withdrawal(#{withdrawal := V}) -> V. withdrawal(St) ->
activity(#{activity := V}) -> V. ff_machine:model(St).
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}).
%% Machinery %% Machinery
-type ts_ev(T) :: -type event() ::
{ev, timestamp(), T}. ff_withdrawal:event().
-type ev() :: -type machine() :: ff_machine:machine(event()).
ff_withdrawal:ev(). -type result() :: ff_machine:result(event()).
-type auxst() ::
#{ctx => ctx()}.
-type machine() :: machinery:machine(ts_ev(ev()), auxst()).
-type result() :: machinery:result(ts_ev(ev()), auxst()).
-type handler_opts() :: machinery:handler_opts(_). -type handler_opts() :: machinery:handler_opts(_).
-spec init({[ev()], ctx()}, machine(), _, handler_opts()) -> -spec init({[event()], ctx()}, machine(), _, handler_opts()) ->
result(). result().
init({Events, Ctx}, #{}, _, _Opts) -> init({Events, Ctx}, #{}, _, _Opts) ->
#{ #{
events => emit_events(Events), events => ff_machine:emit_events(Events),
action => continue, action => continue,
aux_state => #{ctx => Ctx} aux_state => #{ctx => Ctx}
}. }.
-spec process_timeout(machine(), _, handler_opts()) -> -spec process_timeout(machine(), _, handler_opts()) ->
%% result(). 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()).
process_timeout(Machine, _, _Opts) -> process_timeout(Machine, _, _Opts) ->
St = collapse(Machine), St = ff_machine:collapse(ff_withdrawal, Machine),
process_activity(activity(St), St). process_activity(deduce_activity(withdrawal(St)), St).
process_activity(prepare_transfer, St) -> process_activity(prepare_transfer, St) ->
case ff_withdrawal:prepare_transfer(withdrawal(St)) of case ff_withdrawal:prepare_transfer(withdrawal(St)) of
{ok, Events} -> {ok, Events} ->
#{events => emit_events(Events), action => continue}; #{
events => ff_machine:emit_events(Events),
action => continue
};
{error, Reason} -> {error, Reason} ->
#{events => emit_failure(Reason)} #{
events => emit_failure(Reason)
}
end; end;
process_activity(create_session, St) -> process_activity(create_session, St) ->
case ff_withdrawal:create_session(withdrawal(St)) of case ff_withdrawal:create_session(withdrawal(St)) of
{ok, Events} -> {ok, Events} ->
#{ #{
events => emit_events(Events), events => ff_machine:emit_events(Events),
action => set_poll_timer(St) action => set_poll_timer(St)
}; };
{error, Reason} -> {error, Reason} ->
#{events => emit_failure(Reason)} #{
events => emit_failure(Reason)
}
end; end;
process_activity(await_session_completion, St) -> process_activity(await_session_completion, St) ->
case ff_withdrawal:poll_session_completion(withdrawal(St)) of case ff_withdrawal:poll_session_completion(withdrawal(St)) of
{ok, Events} when length(Events) > 0 -> {ok, Events} when length(Events) > 0 ->
#{events => emit_events(Events), action => continue}; #{
events => ff_machine:emit_events(Events),
action => continue
};
{ok, []} -> {ok, []} ->
#{action => set_poll_timer(St)} #{
action => set_poll_timer(St)
}
end; end;
process_activity(commit_transfer, St) -> process_activity(commit_transfer, St) ->
{ok, Events} = ff_withdrawal:commit_transfer(withdrawal(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) -> process_activity(cancel_transfer, St) ->
{ok, Events} = ff_withdrawal:cancel_transfer(withdrawal(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) -> set_poll_timer(St) ->
Now = machinery_time:now(), 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}}. {set_timer, {timeout, Timeout}}.
-spec process_call(_CallArgs, machine(), _, handler_opts()) -> -spec process_call(_CallArgs, machine(), _, handler_opts()) ->
@ -210,62 +183,22 @@ process_call(_CallArgs, #{}, _, _Opts) ->
{ok, #{}}. {ok, #{}}.
emit_failure(Reason) -> emit_failure(Reason) ->
emit_event({status_changed, {failed, Reason}}). ff_machine:emit_event({status_changed, {failed, Reason}}).
%% %%
collapse(#{history := History, aux_state := #{ctx := Ctx}}) -> -spec deduce_activity(withdrawal()) ->
collapse_history(History, #{activity => idle, ctx => Ctx}). activity().
collapse_history(History, St) -> deduce_activity(#{status := {failed, _}}) ->
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, _}}) ->
cancel_transfer; cancel_transfer;
deduce_activity({transfer, {status_changed, committed}}) -> deduce_activity(#{status := succeeded}) ->
undefined; commit_transfer;
deduce_activity({transfer, {status_changed, cancelled}}) -> deduce_activity(#{session := _}) ->
undefined; await_session_completion;
deduce_activity({status_changed, _}) -> deduce_activity(#{transfer := #{status := prepared}}) ->
create_session;
deduce_activity(#{transfer := #{status := created}}) ->
prepare_transfer;
deduce_activity(_) ->
undefined. 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}}}.

View File

@ -16,13 +16,15 @@
-export([id/1]). -export([id/1]).
-export([get/1]). -export([get/1]).
-export([choose/2]). -export([choose/3]).
-export([create_session/3]). -export([create_session/3]).
%% %%
adapter(#{adapter := V}) -> V. adapter(#{adapter := V}) ->
adapter_opts(P) -> maps:get(adapter_opts, P, #{}). V.
adapter_opts(P) ->
maps:get(adapter_opts, P, #{}).
%% %%
@ -43,11 +45,11 @@ get(_) ->
{error, notfound} {error, notfound}
end. end.
-spec choose(ff_destination:destination(), ff_transaction:body()) -> -spec choose(ff_wallet:wallet(), ff_destination:destination(), ff_transaction:body()) ->
{ok, provider()} | {ok, provider()} |
{error, notfound}. {error, notfound}.
choose(_Destination, _Body) -> choose(_Source, _Destination, _Body) ->
case genlib_app:env(ff_withdraw, provider) of case genlib_app:env(ff_withdraw, provider) of
V when V /= undefined -> V when V /= undefined ->
{ok, V}; {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) -> create_session(ID, Withdrawal, Provider) ->
Adapter = {adapter(Provider), adapter_opts(Provider)}, Adapter = {adapter(Provider), adapter_opts(Provider)},
ff_withdrawal_session_machine:create(ID, Adapter, Withdrawal). ff_withdrawal_session_machine:create(ID, Adapter, Withdrawal).

View File

@ -76,6 +76,9 @@
%% API %% API
%% %%
-spec status(session()) ->
session_status().
status(#{status := V}) -> V. status(#{status := V}) -> V.
%% %%
@ -91,7 +94,8 @@ create(ID, Adapter, Withdrawal) ->
unwrap(machinery:start(?NS, ID, Session, backend())) unwrap(machinery:start(?NS, ID, Session, backend()))
end). end).
-spec get(id()) -> {ok, session()} | {error, notfound}. -spec get(id()) ->
ff_map:result(session()).
get(ID) -> get(ID) ->
do(fun () -> do(fun () ->
session(collapse(unwrap(machinery:get(?NS, ID, backend())))) session(collapse(unwrap(machinery:get(?NS, ID, backend()))))

View File

@ -295,11 +295,17 @@ get_domain_config(C) ->
get_default_termset() -> get_default_termset() ->
#domain_TermSet{ #domain_TermSet{
% TODO wallets = #domain_WalletServiceTerms{
% - Strangely enough, hellgate checks wallet currency against _payments_ currencies = {value, ?ordset([?cur(<<"RUB">>)])},
% terms. cash_limit = {decisions, [
payments = #domain_PaymentsServiceTerms{ #domain_CashLimitDecision{
currencies = {value, ?ordset([?cur(<<"RUB">>)])} if_ = {condition, {currency_is, ?cur(<<"RUB">>)}},
then_ = {value, ?cashrng(
{inclusive, ?cash( 0, <<"RUB">>)},
{exclusive, ?cash(10000000, <<"RUB">>)}
)}
}
]}
} }
}. }.

View File

@ -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.

View File

@ -17,11 +17,11 @@
-type id(T) :: T. -type id(T) :: T.
-type party() :: ff_party:id(). -type party() :: ff_party:id().
-type provider() :: ff_provider:provider(). -type provider() :: ff_provider:id().
-type contract() :: ff_party:contract(). -type contract() :: ff_party:contract().
-type class() :: ff_identity_class:class(). -type class() :: ff_identity_class:id().
-type level() :: ff_identity_class:level(). -type level() :: ff_identity_class:level_id().
-type challenge_class() :: ff_identity_class:challenge_class(). -type challenge_class() :: ff_identity_class:challenge_class_id().
-type challenge_id() :: id(_). -type challenge_id() :: id(_).
-type identity() :: #{ -type identity() :: #{
@ -38,18 +38,14 @@
-type challenge() :: -type challenge() ::
ff_identity_challenge:challenge(). ff_identity_challenge:challenge().
-type ev() :: -type event() ::
{created , identity()} | {created , identity()} |
{contract_set , contract()} |
{level_changed , level()} | {level_changed , level()} |
{effective_challenge_changed, challenge_id()} | {effective_challenge_changed, challenge_id()} |
{challenge , challenge_id(), ff_identity_challenge:ev()} . {challenge , challenge_id(), ff_identity_challenge:ev()} .
-type outcome() ::
[ev()].
-export_type([identity/0]). -export_type([identity/0]).
-export_type([ev/0]). -export_type([event/0]).
-export([id/1]). -export([id/1]).
-export([provider/1]). -export([provider/1]).
@ -64,44 +60,49 @@
-export([is_accessible/1]). -export([is_accessible/1]).
-export([create/4]). -export([create/4]).
-export([setup_contract/1]).
-export([start_challenge/4]). -export([start_challenge/4]).
-export([poll_challenge_completion/2]). -export([poll_challenge_completion/2]).
-export([collapse_events/1]).
-export([apply_events/2]).
-export([apply_event/2]). -export([apply_event/2]).
-export([dehydrate/1]).
-export([hydrate/2]).
%% Pipeline %% Pipeline
-import(ff_pipeline, [do/1, unwrap/1, unwrap/2, expect/2, flip/1, valid/2]). -import(ff_pipeline, [do/1, unwrap/1, unwrap/2, expect/2, flip/1, valid/2]).
%% Accessors %% Accessors
-spec id(identity()) -> id(_). -spec id(identity()) ->
id(#{id := V}) -> V. id(_).
-spec provider(identity()) ->
-spec provider(identity()) -> provider(). provider().
provider(#{provider := V}) -> V. -spec class(identity()) ->
class().
-spec class(identity()) -> class(). -spec level(identity()) ->
class(#{class := V}) -> V. level().
-spec party(identity()) ->
-spec level(identity()) -> level(). party().
level(#{level := V}) -> V.
-spec party(identity()) -> party().
party(#{party := V}) -> V.
-spec contract(identity()) -> -spec contract(identity()) ->
ff_map:result(contract()). contract().
contract(V) -> id(#{id := V}) ->
ff_map:find(contract, 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()) -> -spec challenges(identity()) ->
#{challenge_id() => challenge()}. #{challenge_id() => challenge()}.
@ -123,7 +124,7 @@ challenge(ChallengeID, Identity) ->
-spec is_accessible(identity()) -> -spec is_accessible(identity()) ->
{ok, accessible} | {ok, accessible} |
{error, {inaccessible, suspended | blocked}}. {error, ff_party:inaccessibility()}.
is_accessible(Identity) -> is_accessible(Identity) ->
ff_party:is_accessible(party(Identity)). ff_party:is_accessible(party(Identity)).
@ -131,61 +132,72 @@ is_accessible(Identity) ->
%% Constructor %% Constructor
-spec create(id(_), party(), provider(), class()) -> -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 () -> 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, #{ {created, #{
id => ID, id => ID,
party => Party, party => Party,
provider => Provider, provider => ProviderID,
class => Class class => ClassID,
contract => Contract
}}, }},
{level_changed, {level_changed,
ff_identity_class:initial_level(Class) LevelID
} }
] ]
end). 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()) -> -spec start_challenge(challenge_id(), challenge_class(), [ff_identity_challenge:proof()], identity()) ->
{ok, outcome()} | {ok, [event()]} |
{error, {error,
exists | exists |
{challenge_class, notfound} |
{level, ff_identity_class:level()} | {level, ff_identity_class:level()} |
_CreateChallengeError _CreateChallengeError
}. }.
start_challenge(ChallengeID, ChallengeClass, Proofs, Identity) -> start_challenge(ChallengeID, ChallengeClassID, Proofs, Identity) ->
do(fun () -> do(fun () ->
BaseLevel = ff_identity_class:base_level(ChallengeClass), notfound = expect(exists, flip(challenge(ChallengeID, Identity))),
notfound = expect(exists, flip(challenge(ChallengeID, Identity))), IdentityClass = get_identity_class(Identity),
ok = unwrap(level, valid(BaseLevel, level(Identity))), ChallengeClass = unwrap(challenge_class, ff_identity_class:challenge_class(
Events = unwrap(ff_identity_challenge:create(id(Identity), ChallengeClass, Proofs)), ChallengeClassID,
[{challenge, ChallengeID, Ev} || Ev <- Events] 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). end).
-spec poll_challenge_completion(challenge_id(), identity()) -> -spec poll_challenge_completion(challenge_id(), identity()) ->
{ok, outcome()} | {ok, [event()]} |
{error, {error,
notfound | notfound |
ff_identity_challenge:status() ff_identity_challenge:status()
@ -198,44 +210,49 @@ poll_challenge_completion(ChallengeID, Identity) ->
[] -> [] ->
[]; [];
Events = [_ | _] -> Events = [_ | _] ->
{ok, Contract} = contract(Identity), Contract = contract(Identity),
TargetLevel = ff_identity_class:target_level(ff_identity_challenge:class(Challenge)), IdentityClass = get_identity_class(Identity),
ContractorLevel = ff_identity_class:contractor_level(TargetLevel), ChallengeClass = get_challenge_class(Challenge, Identity),
ok = unwrap(ff_party:change_contractor_level(party(Identity), Contract, ContractorLevel)), TargetLevelID = ff_identity_class:target_level(ChallengeClass),
[{challenge, ChallengeID, Ev} || Ev <- Events] ++ {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} {effective_challenge_changed, ChallengeID}
] ]
end end
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(), ...]) -> -spec apply_event(event(), ff_maybe:maybe(identity())) ->
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()) ->
identity(). identity().
apply_event({created, Identity}, undefined) -> apply_event({created, Identity}, undefined) ->
Identity; Identity;
apply_event({contract_set, C}, Identity) ->
Identity#{contract => C};
apply_event({level_changed, L}, Identity) -> apply_event({level_changed, L}, Identity) ->
Identity#{level => L}; Identity#{level => L};
apply_event({effective_challenge_changed, ID}, Identity) -> apply_event({effective_challenge_changed, ID}, Identity) ->
Identity#{effective => ID}; Identity#{effective => ID};
apply_event({challenge, ID, Ev}, Identity) -> apply_event({{challenge, ID}, Ev}, Identity) ->
with_challenges( with_challenges(
fun (Cs) -> fun (Cs) ->
with_challenge( with_challenge(
@ -252,47 +269,3 @@ with_challenges(Fun, Identity) ->
with_challenge(ID, Fun, Challenges) -> with_challenge(ID, Fun, Challenges) ->
maps:update_with(ID, Fun, maps:merge(#{ID => undefined}, 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)}.

View File

@ -1,24 +1,36 @@
%%% %%%
%%% Identity challenge activity %%% Identity challenge activity
%%% %%%
%%% TODOs
%%%
%%% - `ProviderID` + `IdentityClassID` + `ChallengeClassID` easily replaceable
%%% with a _single_ identifier if we drop strictly hierarchical provider
%%% definition.
%%%
-module(ff_identity_challenge). -module(ff_identity_challenge).
%% API %% API
-type id(T) :: T. -type id(T) :: T.
-type claimant() :: id(binary()).
-type timestamp() :: machinery:timestamp(). -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 master_id() :: id(binary()).
-type claim_id() :: id(binary()). -type claim_id() :: id(binary()).
-type challenge() :: #{ -type challenge() :: #{
class := class(), id := id(_),
claimant := id(_), claimant := claimant(),
proofs := [proof()], provider := provider(),
master_id := master_id(), identity_class := identity_class(),
claim_id := claim_id(), challenge_class := challenge_class(),
status => status() proofs := [proof()],
master_id := master_id(),
claim_id := claim_id(),
status => status()
}. }.
-type proof() :: -type proof() ::
@ -49,15 +61,12 @@
-type failure() :: -type failure() ::
_TODO. _TODO.
-type ev() :: -type event() ::
{created, challenge()} | {created, challenge()} |
{status_changed, status()}. {status_changed, status()}.
-type outcome() ::
[ev()].
-export_type([challenge/0]). -export_type([challenge/0]).
-export_type([ev/0]). -export_type([event/0]).
-export([claimant/1]). -export([claimant/1]).
-export([status/1]). -export([status/1]).
@ -67,15 +76,11 @@
-export([claim_id/1]). -export([claim_id/1]).
-export([master_id/1]). -export([master_id/1]).
-export([create/3]). -export([create/6]).
-export([poll_completion/1]). -export([poll_completion/1]).
-export([apply_events/2]).
-export([apply_event/2]). -export([apply_event/2]).
-export([dehydrate/1]).
-export([hydrate/2]).
%% Pipeline %% Pipeline
-import(ff_pipeline, [do/1, unwrap/1, unwrap/2, valid/2]). -import(ff_pipeline, [do/1, unwrap/1, unwrap/2, valid/2]).
@ -89,15 +94,15 @@ status(#{status := V}) ->
V. V.
-spec claimant(challenge()) -> -spec claimant(challenge()) ->
id(_). claimant().
claimant(#{claimant := V}) -> claimant(#{claimant := V}) ->
V. V.
-spec class(challenge()) -> -spec class(challenge()) ->
class(). challenge_class().
class(#{class := V}) -> class(#{challenge_class := V}) ->
V. V.
-spec proofs(challenge()) -> -spec proofs(challenge()) ->
@ -132,25 +137,32 @@ claim_id(#{claim_id := V}) ->
%% %%
-spec create(id(_), class(), [proof()]) -> -spec create(id(_), claimant(), provider(), identity_class(), challenge_class(), [proof()]) ->
{ok, outcome()} | {ok, [event()]} |
{error, {error,
{proof, notfound | insufficient} | {proof, notfound | insufficient} |
_StartError _StartError
}. }.
create(Claimant, Class, Proofs) -> create(ID, Claimant, ProviderID, IdentityClassID, ChallengeClassID, Proofs) ->
do(fun () -> 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)), MasterID = unwrap(deduce_identity_id(Proofs)),
ClaimID = unwrap(create_claim(MasterID, TargetLevel, Claimant, Proofs)), ClaimID = unwrap(create_claim(MasterID, TargetLevel, Claimant, Proofs)),
[ [
{created, #{ {created, #{
class => Class, id => ID,
claimant => Claimant, claimant => Claimant,
proofs => Proofs, provider => ProviderID,
master_id => MasterID, identity_class => IdentityClassID,
claim_id => ClaimID challenge_class => ChallengeClassID,
proofs => Proofs,
master_id => MasterID,
claim_id => ClaimID
}}, }},
{status_changed, {status_changed,
pending pending
@ -159,7 +171,7 @@ create(Claimant, Class, Proofs) ->
end). end).
-spec poll_completion(challenge()) -> -spec poll_completion(challenge()) ->
{ok, outcome()} | {ok, [event()]} |
{error, {error,
notfound | notfound |
status() status()
@ -185,13 +197,7 @@ poll_completion(Challenge) ->
%% %%
-spec apply_events([ev()], undefined | challenge()) -> -spec apply_event(event(), ff_maybe:maybe(challenge())) ->
undefined | challenge().
apply_events(Evs, Challenge) ->
lists:foldl(fun apply_event/2, Challenge, Evs).
-spec apply_event(ev(), undefined | challenge()) ->
challenge(). challenge().
apply_event({created, Challenge}, undefined) -> apply_event({created, Challenge}, undefined) ->
@ -199,18 +205,6 @@ apply_event({created, Challenge}, undefined) ->
apply_event({status_changed, S}, Challenge) -> apply_event({status_changed, S}, Challenge) ->
Challenge#{status => S}. 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"). -include_lib("id_proto/include/id_proto_identification_thrift.hrl").

View File

@ -49,12 +49,10 @@
-export([initial_level/1]). -export([initial_level/1]).
-export([level/2]). -export([level/2]).
-export([level_id/1]).
-export([level_name/1]). -export([level_name/1]).
-export([contractor_level/1]). -export([contractor_level/1]).
-export([challenge_class/2]). -export([challenge_class/2]).
-export([challenge_class_id/1]).
-export([base_level/1]). -export([base_level/1]).
-export([target_level/1]). -export([target_level/1]).
-export([challenge_class_name/1]). -export([challenge_class_name/1]).
@ -87,7 +85,7 @@ contract_template(#{contract_template_ref := V}) ->
V. V.
-spec initial_level(class()) -> -spec initial_level(class()) ->
level(). level_id().
initial_level(#{initial_level := V}) -> initial_level(#{initial_level := V}) ->
V. V.
@ -108,12 +106,6 @@ challenge_class(ID, #{challenge_classes := ChallengeClasses}) ->
%% Level %% Level
-spec level_id(level()) ->
level_id().
level_id(#{id := V}) ->
V.
-spec level_name(level()) -> -spec level_name(level()) ->
binary(). binary().
@ -128,12 +120,6 @@ contractor_level(#{contractor_level := V}) ->
%% Challenge %% Challenge
-spec challenge_class_id(challenge_class()) ->
challenge_class_id().
challenge_class_id(#{id := V}) ->
V.
-spec challenge_class_name(challenge_class()) -> -spec challenge_class_name(challenge_class()) ->
binary(). binary().
@ -141,13 +127,13 @@ challenge_class_name(#{name := V}) ->
V. V.
-spec base_level(challenge_class()) -> -spec base_level(challenge_class()) ->
level(). level_id().
base_level(#{base_level := V}) -> base_level(#{base_level := V}) ->
V. V.
-spec target_level(challenge_class()) -> -spec target_level(challenge_class()) ->
level(). level_id().
target_level(#{target_level := V}) -> target_level(#{target_level := V}) ->
V. V.

View File

@ -20,19 +20,9 @@
-type id() :: machinery:id(). -type id() :: machinery:id().
-type identity() :: ff_identity:identity(). -type identity() :: ff_identity:identity().
-type timestamp() :: machinery:timestamp().
-type ctx() :: ff_ctx:ctx(). -type ctx() :: ff_ctx:ctx().
-type activity() :: -type st() :: ff_machine:st(identity()).
{challenge, challenge_id()} |
undefined .
-type st() :: #{
activity := activity(),
identity := identity(),
ctx := ctx(),
times => {timestamp(), timestamp()}
}.
-type challenge_id() :: -type challenge_id() ::
machinery:id(). machinery:id().
@ -42,15 +32,12 @@
-export([create/3]). -export([create/3]).
-export([get/1]). -export([get/1]).
-export([events/2]). -export([events/2]).
-export([start_challenge/2]). -export([start_challenge/2]).
%% Accessors %% Accessors
-export([identity/1]). -export([identity/1]).
-export([activity/1]).
-export([ctx/1]).
-export([created/1]).
-export([updated/1]).
%% Machinery %% Machinery
@ -62,7 +49,7 @@
%% Pipeline %% 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'). -define(NS, 'ff/identity').
@ -75,20 +62,14 @@
-spec create(id(), params(), ctx()) -> -spec create(id(), params(), ctx()) ->
ok | ok |
{error, {error,
{provider, notfound} | _IdentityCreateError |
{identity_class, notfound} |
_SetupContractError |
exists exists
}. }.
create(ID, #{party := Party, provider := ProviderID, class := IdentityClassID}, Ctx) -> create(ID, #{party := Party, provider := ProviderID, class := IdentityClassID}, Ctx) ->
do(fun () -> do(fun () ->
Provider = unwrap(provider, ff_provider:get(ProviderID)), Events = unwrap(ff_identity:create(ID, Party, ProviderID, IdentityClassID)),
IdentityClass = unwrap(identity_class, ff_provider:get_identity_class(IdentityClassID, Provider)), unwrap(machinery:start(?NS, ID, {Events, Ctx}, backend()))
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()))
end). end).
-spec get(id()) -> -spec get(id()) ->
@ -96,12 +77,10 @@ create(ID, #{party := Party, provider := ProviderID, class := IdentityClassID},
{error, notfound} . {error, notfound} .
get(ID) -> get(ID) ->
do(fun () -> ff_machine:get(ff_identity, ?NS, ID).
collapse(unwrap(machinery:get(?NS, ID, backend())))
end).
-spec events(id(), machinery:range()) -> -spec events(id(), machinery:range()) ->
{ok, [{integer(), ts_ev(ev())}]} | {ok, [{integer(), ff_machine:timestamped_event(event())}]} |
{error, notfound}. {error, notfound}.
events(ID, Range) -> events(ID, Range) ->
@ -120,11 +99,7 @@ events(ID, Range) ->
ok | ok |
{error, {error,
notfound | notfound |
{challenge, _IdentityChallengeError
{pending, challenge_id()} |
{class, notfound} |
_IdentityChallengeError
}
}. }.
start_challenge(ID, Params) -> start_challenge(ID, Params) ->
@ -140,42 +115,27 @@ backend() ->
%% Accessors %% Accessors
-spec identity(st()) -> identity(). -spec identity(st()) ->
-spec activity(st()) -> activity(). identity().
-spec ctx(st()) -> ctx().
-spec created(st()) -> timestamp() | undefined.
-spec updated(st()) -> timestamp() | undefined.
identity(#{identity := V}) -> V. identity(St) ->
activity(#{activity := V}) -> V. ff_machine:model(St).
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}).
%% Machinery %% Machinery
-type ts_ev(T) :: -type event() ::
{ev, timestamp(), T}. ff_identity:event().
-type ev() :: -type machine() :: ff_machine:machine(event()).
ff_identity:ev(). -type result() :: ff_machine:result(event()).
-type auxst() ::
#{ctx => ctx()}.
-type machine() :: machinery:machine(ts_ev(ev()), auxst()).
-type result() :: machinery:result(ts_ev(ev()), auxst()).
-type handler_opts() :: machinery:handler_opts(_). -type handler_opts() :: machinery:handler_opts(_).
-spec init({[ev()], ctx()}, machine(), _, handler_opts()) -> -spec init({[event()], ctx()}, machine(), _, handler_opts()) ->
result(). result().
init({Events, Ctx}, #{}, _, _Opts) -> init({Events, Ctx}, #{}, _, _Opts) ->
#{ #{
events => emit_ts_events(Events), events => ff_machine:emit_events(Events),
aux_state => #{ctx => Ctx} aux_state => #{ctx => Ctx}
}. }.
@ -185,21 +145,22 @@ init({Events, Ctx}, #{}, _, _Opts) ->
result(). result().
process_timeout(Machine, _, _Opts) -> 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), Identity = identity(St),
{ok, Events} = ff_identity:poll_challenge_completion(ChallengeID, Identity), {ok, Events} = ff_identity:poll_challenge_completion(ChallengeID, Identity),
case Events of case Events of
[] -> [] ->
#{action => set_poll_timer(St)}; #{action => set_poll_timer(St)};
_Some -> _Some ->
#{events => emit_ts_events(Events)} #{events => ff_machine:emit_events(Events)}
end. end.
set_poll_timer(St) -> set_poll_timer(St) ->
Now = machinery_time:now(), 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}}. {set_timer, {timeout, Timeout}}.
%% %%
@ -211,9 +172,15 @@ set_poll_timer(St) ->
{_TODO, result()}. {_TODO, result()}.
process_call({start_challenge, Params}, Machine, _, _Opts) -> 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), Identity = identity(St),
handle_result(do(challenge, fun () -> handle_result(do(challenge, fun () ->
#{ #{
@ -221,16 +188,12 @@ do_start_challenge(Params, #{activity := undefined} = St) ->
class := ChallengeClassID, class := ChallengeClassID,
proofs := Proofs proofs := Proofs
} = Params, } = Params,
Class = ff_identity:class(Identity), Events = unwrap(ff_identity:start_challenge(ChallengeID, ChallengeClassID, Proofs, Identity)),
ChallengeClass = unwrap(class, ff_identity_class:challenge_class(ChallengeClassID, Class)),
Events = unwrap(ff_identity:start_challenge(ChallengeID, ChallengeClass, Proofs, Identity)),
#{ #{
events => emit_ts_events(Events), events => ff_machine:emit_events(Events),
action => continue action => continue
} }
end)); end)).
do_start_challenge(_Params, #{activity := {challenge, ChallengeID}}) ->
handle_result({error, {challenge, {pending, ChallengeID}}}).
handle_result({ok, R}) -> handle_result({ok, R}) ->
{ok, R}; {ok, R};
@ -239,46 +202,13 @@ handle_result({error, _} = Error) ->
%% %%
collapse(#{history := History, aux_state := #{ctx := Ctx}}) -> deduce_activity(#{challenges := Challenges}) ->
apply_events(History, #{ctx => Ctx}). Filter = fun (_, Challenge) -> ff_identity_challenge:status(Challenge) == pending end,
case maps:keys(maps:filter(Filter, Challenges)) of
apply_events(History, St) -> [ChallengeID] ->
lists:foldl(fun apply_event/2, St, History). {challenge, ChallengeID};
[] ->
apply_event({_ID, _Ts, TsEv}, St0) -> undefined
{EvBody, St1} = apply_ts_event(TsEv, St0), end;
apply_event_body(ff_identity:hydrate(EvBody, maps:get(identity, St1, undefined)), St1). deduce_activity(#{}) ->
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, _}}) ->
undefined. 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}}}.

View File

@ -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)
}}.

View File

@ -24,6 +24,11 @@
-export_type([wallet/0]). -export_type([wallet/0]).
-export_type([party_params/0]). -export_type([party_params/0]).
-type inaccessiblity() ::
{inaccessible, blocked | suspended}.
-export_type([inaccessiblity/0]).
-export([create/1]). -export([create/1]).
-export([create/2]). -export([create/2]).
-export([is_accessible/1]). -export([is_accessible/1]).
@ -36,7 +41,7 @@
%% Pipeline %% 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()) -> -spec is_accessible(id()) ->
{ok, accessible} | {ok, accessible} |
{error, {inaccessible, suspended | blocked}}. {error, inaccessiblity()}.
is_accessible(ID) -> is_accessible(ID) ->
case do_get_party(ID) of case do_get_party(ID) of
@ -117,6 +122,7 @@ is_wallet_accessible(ID, WalletID) ->
-spec create_contract(id(), contract_prototype()) -> -spec create_contract(id(), contract_prototype()) ->
{ok, contract()} | {ok, contract()} |
{error, inaccessiblity()} |
{error, invalid}. {error, invalid}.
create_contract(ID, Prototype) -> create_contract(ID, Prototype) ->
@ -135,6 +141,7 @@ generate_contract_id() ->
-spec change_contractor_level(id(), contract(), dmsl_domain_thrift:'ContractorIdentificationLevel'()) -> -spec change_contractor_level(id(), contract(), dmsl_domain_thrift:'ContractorIdentificationLevel'()) ->
ok | ok |
{error, inaccessiblity()} |
{error, invalid}. {error, invalid}.
change_contractor_level(ID, ContractID, ContractorLevel) -> change_contractor_level(ID, ContractID, ContractorLevel) ->
@ -154,6 +161,7 @@ change_contractor_level(ID, ContractID, ContractorLevel) ->
-spec create_wallet(id(), contract(), wallet_prototype()) -> -spec create_wallet(id(), contract(), wallet_prototype()) ->
{ok, wallet()} | {ok, wallet()} |
{error, inaccessiblity()} |
{error, invalid}. {error, invalid}.
create_wallet(ID, ContractID, Prototype) -> create_wallet(ID, ContractID, Prototype) ->
@ -217,8 +225,12 @@ do_create_claim(ID, Changeset) ->
case call('CreateClaim', [construct_userinfo(), ID, Changeset]) of case call('CreateClaim', [construct_userinfo(), ID, Changeset]) of
{ok, Claim} -> {ok, Claim} ->
{ok, Claim}; {ok, Claim};
{exception, #payproc_InvalidChangeset{reason = _Reason}} -> {exception, #payproc_InvalidChangeset{
reason = {invalid_wallet, #payproc_InvalidWallet{reason = {contract_terms_violated, _}}}
}} ->
{error, invalid}; {error, invalid};
{exception, #payproc_InvalidPartyStatus{status = Status}} ->
{error, construct_inaccessibilty(Status)};
{exception, Unexpected} -> {exception, Unexpected} ->
error(Unexpected) error(Unexpected)
end. end.
@ -238,6 +250,11 @@ do_accept_claim(ID, Claim) ->
error(Unexpected) error(Unexpected)
end. end.
construct_inaccessibilty({blocking, _}) ->
{inaccessible, blocked};
construct_inaccessibilty({suspension, _}) ->
{inaccessible, suspended}.
%% %%
-define(contractor_mod(ID, Mod), -define(contractor_mod(ID, Mod),

View File

@ -47,17 +47,23 @@
%% %%
-spec id(provider()) -> id(). -spec id(provider()) ->
id(#{id := ID}) -> ID. id().
-spec name(provider()) ->
binary().
-spec residences(provider()) ->
[ff_residence:id()].
-spec payinst(provider()) ->
payinst_ref().
-spec name(provider()) -> binary(). id(#{id := ID}) ->
name(#{payinst := PI}) -> PI#domain_PaymentInstitution.name. ID.
name(#{payinst := PI}) ->
-spec residences(provider()) -> [ff_residence:id()]. PI#domain_PaymentInstitution.name.
residences(#{payinst := PI}) -> PI#domain_PaymentInstitution.residences. residences(#{payinst := PI}) ->
PI#domain_PaymentInstitution.residences.
-spec payinst(provider()) -> payinst_ref(). payinst(#{payinst_ref := V}) ->
payinst(#{payinst_ref := V}) -> V. V.
%% %%
@ -106,24 +112,24 @@ get(ID) ->
CCName = maps:get(name, CCC, CCID), CCName = maps:get(name, CCC, CCID),
BaseLevelID = maps:get(base, CCC), BaseLevelID = maps:get(base, CCC),
TargetLevelID = maps:get(target, CCC), TargetLevelID = maps:get(target, CCC),
{ok, BaseLevel} = maps:find(BaseLevelID, Levels), {ok, _} = maps:find(BaseLevelID, Levels),
{ok, TargetLevel} = maps:find(TargetLevelID, Levels), {ok, _} = maps:find(TargetLevelID, Levels),
#{ #{
id => CCID, id => CCID,
name => CCName, name => CCName,
base_level => BaseLevel, base_level => BaseLevelID,
target_level => TargetLevel target_level => TargetLevelID
} }
end, end,
maps:get(challenges, ICC, #{}) maps:get(challenges, ICC, #{})
), ),
InitialLevelID = maps:get(initial_level, ICC), InitialLevelID = maps:get(initial_level, ICC),
{ok, InitialLevel} = maps:find(InitialLevelID, Levels), {ok, _} = maps:find(InitialLevelID, Levels),
#{ #{
id => ICID, id => ICID,
name => Name, name => Name,
contract_template_ref => ContractTemplateRef, contract_template_ref => ContractTemplateRef,
initial_level => InitialLevel, initial_level => InitialLevelID,
levels => Levels, levels => Levels,
challenge_classes => ChallengeClasses challenge_classes => ChallengeClasses
} }

View File

@ -1,6 +1,8 @@
%%% %%%
%%% Financial transaction between accounts %%% Financial transaction between accounts
%%% %%%
%%% - Rename to `ff_posting_plan`?
%%%
-module(ff_transaction). -module(ff_transaction).

View File

@ -6,7 +6,6 @@
%%% - We must synchronise any transfers on wallet machine, as one may request %%% - We must synchronise any transfers on wallet machine, as one may request
%%% us to close wallet concurrently. Moreover, we should probably check any %%% us to close wallet concurrently. Moreover, we should probably check any
%%% limits there too. %%% limits there too.
%%% - Well, we will need `cancel` soon too.
%%% - What if we get rid of some failures in `prepare`, specifically those %%% - What if we get rid of some failures in `prepare`, specifically those
%%% which related to wallet blocking / suspension? It would be great to get %%% 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. %%% rid of the `wallet closed` failure but I see no way to do so.
@ -14,10 +13,10 @@
-module(ff_transfer). -module(ff_transfer).
-type wallet() :: ff_wallet:wallet(). -type id() :: ff_transaction:id().
-type account() :: ff_account:id().
-type body() :: ff_transaction:body(). -type body() :: ff_transaction:body().
-type trxid() :: ff_transaction:id(). -type posting() :: {account(), account(), body()}.
-type posting() :: {wallet(), wallet(), body()}.
-type status() :: -type status() ::
created | created |
@ -26,24 +25,21 @@
cancelled . cancelled .
-type transfer() :: #{ -type transfer() :: #{
trxid := trxid(), id := id(),
postings := [posting()], postings := [posting()],
status => status() status => status()
}. }.
-type ev() :: -type event() ::
{created, transfer()} | {created, transfer()} |
{status_changed, status()}. {status_changed, status()}.
-type outcome() ::
[ev()].
-export_type([transfer/0]). -export_type([transfer/0]).
-export_type([posting/0]). -export_type([posting/0]).
-export_type([status/0]). -export_type([status/0]).
-export_type([ev/0]). -export_type([event/0]).
-export([trxid/1]). -export([id/1]).
-export([postings/1]). -export([postings/1]).
-export([status/1]). -export([status/1]).
@ -62,38 +58,42 @@
%% %%
-spec trxid(transfer()) -> trxid(). -spec id(transfer()) ->
-spec postings(transfer()) -> [posting()]. id().
-spec status(transfer()) -> status(). -spec postings(transfer()) ->
[posting()].
-spec status(transfer()) ->
status().
trxid(#{trxid := V}) -> V. id(#{id := V}) ->
postings(#{postings := V}) -> V. V.
status(#{status := V}) -> V. postings(#{postings := V}) ->
V.
status(#{status := V}) ->
V.
%% %%
-spec create(trxid(), [posting()]) -> -spec create(id(), [posting()]) ->
{ok, outcome()} | {ok, [event()]} |
{error, {error,
empty | empty |
{wallet, {account, notfound} |
{inaccessible, blocked | suspended} | {account, ff_party:inaccessibility()} |
{currency, invalid} | {currency, invalid} |
{provider, invalid} {provider, invalid}
}
}. }.
create(TrxID, Postings = [_ | _]) -> create(ID, Postings = [_ | _]) ->
do(fun () -> do(fun () ->
Wallets = gather_wallets(Postings), Accounts = maps:values(gather_accounts(Postings)),
accessible = unwrap(wallet, validate_accessible(Wallets)), valid = validate_currencies(Accounts),
valid = unwrap(wallet, validate_currencies(Wallets)), valid = validate_identities(Accounts),
valid = unwrap(wallet, validate_identities(Wallets)), accessible = validate_accessible(Accounts),
[ [
{created, #{ {created, #{
trxid => TrxID, id => ID,
postings => Postings, postings => Postings
status => created
}}, }},
{status_changed, {status_changed,
created created
@ -103,54 +103,60 @@ create(TrxID, Postings = [_ | _]) ->
create(_TrxID, []) -> create(_TrxID, []) ->
{error, empty}. {error, empty}.
gather_wallets(Postings) -> gather_accounts(Postings) ->
lists:usort(lists:flatten([[S, D] || {S, D, _} <- Postings])). maps:from_list([
{AccountID, get_account(AccountID)} ||
AccountID <- lists:usort(lists:flatten([[S, D] || {S, D, _} <- Postings]))
]).
validate_accessible(Wallets) -> %% TODO
do(fun () -> %% - Not the right place.
_ = [accessible = unwrap(ff_wallet:is_accessible(W)) || W <- Wallets], get_account({wallet, ID}) ->
accessible St = unwrap(account, ff_wallet_machine:get(ID)),
end). 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]) -> validate_accessible(Accounts) ->
do(fun () -> _ = [accessible = unwrap(account, ff_account:is_accessible(A)) || A <- Accounts],
Currency = ff_wallet:currency(W0), accessible.
_ = [ok = unwrap(currency, valid(Currency, ff_wallet:currency(W))) || W <- Wallets],
valid
end).
validate_identities([W0 | Wallets]) -> validate_currencies([A0 | Accounts]) ->
do(fun () -> Currency = ff_account:currency(A0),
Provider = ff_identity:provider(ff_wallet:identity(W0)), _ = [ok = unwrap(currency, valid(Currency, ff_account:currency(A))) || A <- Accounts],
_ = [ valid.
ok = unwrap(provider, valid(Provider, ff_identity:provider(ff_wallet:identity(W)))) ||
W <- Wallets validate_identities([A0 | Accounts]) ->
], {ok, IdentitySt} = ff_identity_machine:get(ff_account:identity(A0)),
valid Identity0 = ff_identity_machine:identity(IdentitySt),
end). 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()) -> -spec prepare(transfer()) ->
{ok, outcome()} | {ok, [event()]} |
{error, {error,
balance | {status, committed | cancelled}
{status, committed | cancelled} |
{wallet, {inaccessible, blocked | suspended}}
}. }.
prepare(Transfer = #{status := created}) -> prepare(Transfer = #{status := created}) ->
TrxID = trxid(Transfer), ID = id(Transfer),
Postings = postings(Transfer), Postings = postings(Transfer),
do(fun () -> do(fun () ->
accessible = unwrap(wallet, validate_accessible(gather_wallets(Postings))), _Affected = unwrap(ff_transaction:prepare(ID, construct_trx_postings(Postings))),
_Affected = unwrap(ff_transaction:prepare(TrxID, construct_trx_postings(Postings))),
[{status_changed, prepared}] [{status_changed, prepared}]
end); end);
prepare(_Transfer = #{status := prepared}) -> prepare(#{status := prepared}) ->
{ok, []}; {ok, []};
prepare(#{status := Status}) -> prepare(#{status := Status}) ->
{error, {status, Status}}. {error, Status}.
%% TODO %% TODO
% validate_balances(Affected) -> % validate_balances(Affected) ->
@ -159,17 +165,17 @@ prepare(#{status := Status}) ->
%% %%
-spec commit(transfer()) -> -spec commit(transfer()) ->
{ok, outcome()} | {ok, [event()]} |
{error, {status, created | cancelled}}. {error, {status, created | cancelled}}.
commit(Transfer = #{status := prepared}) -> commit(Transfer = #{status := prepared}) ->
TrxID = trxid(Transfer), ID = id(Transfer),
Postings = postings(Transfer), Postings = postings(Transfer),
do(fun () -> 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}] [{status_changed, committed}]
end); end);
commit(_Transfer = #{status := committed}) -> commit(#{status := committed}) ->
{ok, []}; {ok, []};
commit(#{status := Status}) -> commit(#{status := Status}) ->
{error, Status}. {error, Status}.
@ -177,31 +183,38 @@ commit(#{status := Status}) ->
%% %%
-spec cancel(transfer()) -> -spec cancel(transfer()) ->
{ok, outcome()} | {ok, [event()]} |
{error, {status, created | committed}}. {error, {status, created | committed}}.
cancel(Transfer = #{status := prepared}) -> cancel(Transfer = #{status := prepared}) ->
ID = id(Transfer),
Postings = postings(Transfer),
do(fun () -> do(fun () ->
Postings = construct_trx_postings(postings(Transfer)), _Affected = unwrap(ff_transaction:cancel(ID, construct_trx_postings(Postings))),
_Affected = unwrap(ff_transaction:cancel(trxid(Transfer), Postings)),
[{status_changed, cancelled}] [{status_changed, cancelled}]
end); end);
cancel(_Transfer = #{status := cancelled}) -> cancel(#{status := cancelled}) ->
{ok, []}; {ok, []};
cancel(#{status := Status}) -> cancel(#{status := Status}) ->
{error, {status, Status}}. {error, {status, Status}}.
%% %%
-spec apply_event(event(), ff_maybe:maybe(account())) ->
account().
apply_event({created, Transfer}, undefined) -> apply_event({created, Transfer}, undefined) ->
Transfer; Transfer;
apply_event({status_changed, S}, Transfer) -> apply_event({status_changed, S}, Transfer) ->
Transfer#{status := S}. Transfer#{status => S}.
%% %%
construct_trx_postings(Postings) -> construct_trx_postings(Postings) ->
Accounts = gather_accounts(Postings),
[ [
{unwrap(ff_wallet:account(Source)), unwrap(ff_wallet:account(Destination)), Body} || {SourceAccount, DestinationAccount, Body} ||
{Source, Destination, Body} <- Postings {Source, Destination, Body} <- Postings,
SourceAccount <- [ff_account:pm_account(maps:get(Source, Accounts))],
DestinationAccount <- [ff_account:pm_account(maps:get(Destination, Accounts))]
]. ].

View File

@ -4,136 +4,87 @@
-module(ff_wallet). -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 currency() :: ff_currency:id().
-type wid() :: ff_party:wallet().
-type id(T) :: T.
-type wallet() :: #{ -type wallet() :: #{
id := id(_), account := account(),
identity := identity(), name := binary()
name := binary(),
currency := currency(),
wid => wid()
}. }.
-type ev() :: -type event() ::
{created, wallet()} | {created, wallet()} |
{wid_set, wid()}. {account, ff_account:event()}.
-type outcome() :: [ev()].
-export_type([wallet/0]). -export_type([wallet/0]).
-export_type([ev/0]). -export_type([event/0]).
-export([account/1]).
-export([id/1]). -export([id/1]).
-export([identity/1]). -export([identity/1]).
-export([name/1]). -export([name/1]).
-export([currency/1]). -export([currency/1]).
-export([account/1]).
-export([create/4]). -export([create/4]).
-export([setup_wallet/1]).
-export([is_accessible/1]). -export([is_accessible/1]).
-export([close/1]). -export([close/1]).
-export([collapse_events/1]).
-export([apply_events/2]).
-export([apply_event/2]). -export([apply_event/2]).
-export([dehydrate/1]).
-export([hydrate/2]).
%% Pipeline %% Pipeline
-import(ff_pipeline, [do/1, unwrap/1, unwrap/2]). -import(ff_pipeline, [do/1, unwrap/1]).
%% Accessors %% Accessors
-spec id(wallet()) -> id(_). -spec account(wallet()) -> account().
-spec identity(wallet()) -> identity().
-spec name(wallet()) -> binary().
-spec currency(wallet()) -> currency().
id(#{id := V}) -> V. -spec id(wallet()) ->
identity(#{identity := V}) -> V. id(_).
name(#{name := V}) -> V. -spec identity(wallet()) ->
currency(#{currency := V}) -> V. identity().
-spec name(wallet()) ->
binary().
-spec currency(wallet()) ->
currency().
-spec wid(wallet()) -> ff_map:result(wid()). account(#{account := V}) ->
V.
wid(Wallet) -> id(Wallet) ->
ff_map:find(wid, Wallet). ff_account:id(account(Wallet)).
identity(Wallet) ->
-spec account(wallet()) -> ff_account:identity(account(Wallet)).
{ok, ff_transaction:account()} | name(Wallet) ->
{error, notfound}. maps:get(name, Wallet, <<>>).
currency(Wallet) ->
account(Wallet) -> ff_account:currency(account(Wallet)).
do(fun () ->
WID = unwrap(wid(Wallet)),
unwrap(ff_party:get_wallet_account(ff_identity:party(identity(Wallet)), WID))
end).
%% %%
-spec create(id(_), identity(), binary(), currency()) -> -spec create(id(_), identity(), binary(), currency()) ->
{ok, outcome()}. {ok, [event()]}.
create(ID, Identity, Name, Currency) -> create(ID, IdentityID, Name, CurrencyID) ->
do(fun () -> do(fun () ->
[{created, #{ [{created, #{name => Name}}] ++
id => ID, [{account, Ev} || Ev <- unwrap(ff_account:create(ID, IdentityID, CurrencyID))]
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}]
end). end).
-spec is_accessible(wallet()) -> -spec is_accessible(wallet()) ->
{ok, accessible} | {ok, accessible} |
{error, {error, ff_party:inaccessibility()}.
{wid, notfound} |
{inaccessible, suspended | blocked}
}.
is_accessible(Wallet) -> is_accessible(Wallet) ->
do(fun () -> ff_account:is_accessible(account(Wallet)).
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).
-spec close(wallet()) -> -spec close(wallet()) ->
{ok, outcome()} | {ok, [event()]} |
{error, {error,
{inaccessible, blocked | suspended} | ff_party:inaccessibility() |
{account, pending} {account, pending}
}. }.
@ -146,51 +97,12 @@ close(Wallet) ->
%% %%
-spec collapse_events([ev(), ...]) -> -spec apply_event(event(), undefined | wallet()) ->
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()) ->
wallet(). wallet().
apply_event({created, Wallet}, undefined) -> apply_event({created, Wallet}, undefined) ->
Wallet; Wallet;
apply_event({wid_set, WID}, Wallet) -> apply_event({account, Ev}, Wallet = #{account := Account}) ->
Wallet#{wid => WID}. Wallet#{account := ff_account:apply_event(Ev, Account)};
apply_event({account, Ev}, Wallet) ->
%% apply_event({account, Ev}, Wallet#{account => undefined}).
-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}.

View File

@ -10,15 +10,10 @@
-module(ff_wallet_machine). -module(ff_wallet_machine).
-type id() :: machinery:id(). -type id() :: machinery:id().
-type timestamp() :: machinery:timestamp().
-type wallet() :: ff_wallet:wallet(). -type wallet() :: ff_wallet:wallet().
-type ctx() :: ff_ctx:ctx(). -type ctx() :: ff_ctx:ctx().
-type st() :: #{ -type st() :: ff_machine:st(wallet()).
wallet := wallet(),
ctx := ctx(),
times => {timestamp(), timestamp()}
}.
-export_type([id/0]). -export_type([id/0]).
@ -28,9 +23,6 @@
%% Accessors %% Accessors
-export([wallet/1]). -export([wallet/1]).
-export([ctx/1]).
-export([created/1]).
-export([updated/1]).
%% Machinery %% Machinery
@ -42,22 +34,14 @@
%% Pipeline %% Pipeline
-import(ff_pipeline, [do/1, unwrap/1, unwrap/2]). -import(ff_pipeline, [do/1, unwrap/1]).
%% Accessors %% Accessors
-spec wallet(st()) -> wallet(). -spec wallet(st()) -> wallet().
-spec ctx(st()) -> ctx().
-spec created(st()) -> timestamp() | undefined.
-spec updated(st()) -> timestamp() | undefined.
wallet(#{wallet := V}) -> V. wallet(St) ->
ctx(#{ctx := V}) -> V. ff_machine:model(St).
created(St) -> erlang:element(1, times(St)).
updated(St) -> erlang:element(2, times(St)).
times(St) ->
genlib_map:get(times, St, {undefined, undefined}).
%% %%
@ -72,19 +56,14 @@ times(St) ->
-spec create(id(), params(), ctx()) -> -spec create(id(), params(), ctx()) ->
ok | ok |
{error, {error,
{identity, notfound} | _WalletCreateError |
{currency, notfound} |
_WalletError |
exists exists
}. }.
create(ID, #{identity := IdentityID, name := Name, currency := Currency}, Ctx) -> create(ID, #{identity := IdentityID, name := Name, currency := CurrencyID}, Ctx) ->
do(fun () -> do(fun () ->
Identity = ff_identity_machine:identity(unwrap(identity, ff_identity_machine:get(IdentityID))), Events = unwrap(ff_wallet:create(ID, IdentityID, Name, CurrencyID)),
_ = unwrap(currency, ff_currency:get(Currency)), unwrap(machinery:start(?NS, ID, {Events, Ctx}, fistful:backend(?NS)))
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()))
end). end).
-spec get(id()) -> -spec get(id()) ->
@ -92,32 +71,23 @@ create(ID, #{identity := IdentityID, name := Name, currency := Currency}, Ctx) -
{error, notfound} . {error, notfound} .
get(ID) -> get(ID) ->
do(fun () -> ff_machine:get(ff_wallet, ?NS, ID).
collapse(unwrap(machinery:get(?NS, ID, backend())))
end).
backend() ->
fistful:backend(?NS).
%% machinery %% machinery
-type ev() :: -type event() ::
ff_wallet:ev(). ff_wallet:event().
-type auxst() :: -type machine() :: ff_machine:machine(event()).
#{ctx => ctx()}. -type result() :: ff_machine:result(event()).
-type ts_ev(T) :: {ev, timestamp(), T}.
-type machine() :: machinery:machine(ts_ev(ev()), auxst()).
-type result() :: machinery:result(ts_ev(ev()), auxst()).
-type handler_opts() :: machinery:handler_opts(_). -type handler_opts() :: machinery:handler_opts(_).
-spec init({[ev()], ctx()}, machine(), _, handler_opts()) -> -spec init({[event()], ctx()}, machine(), _, handler_opts()) ->
result(). result().
init({Events, Ctx}, #{}, _, _Opts) -> init({Events, Ctx}, #{}, _, _Opts) ->
#{ #{
events => emit_ts_events(Events), events => ff_machine:emit_events(Events),
aux_state => #{ctx => Ctx} aux_state => #{ctx => Ctx}
}. }.
@ -132,36 +102,3 @@ process_timeout(#{}, _, _Opts) ->
process_call(_CallArgs, #{}, _, _Opts) -> process_call(_CallArgs, #{}, _, _Opts) ->
{ok, #{}}. {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}}}.

View File

@ -161,9 +161,9 @@ create_wallet_ok(C) ->
}, },
ff_ctx:new() ff_ctx:new()
), ),
W = ff_wallet_machine:wallet(unwrap(ff_wallet_machine:get(ID))), Wallet = ff_wallet_machine:wallet(unwrap(ff_wallet_machine:get(ID))),
{ok, accessible} = ff_wallet:is_accessible(W), {ok, accessible} = ff_wallet:is_accessible(Wallet),
{ok, Account} = ff_wallet:account(W), Account = ff_account:pm_account(ff_wallet:account(Wallet)),
{ok, {Amount, <<"RUB">>}} = ff_transaction:balance(Account), {ok, {Amount, <<"RUB">>}} = ff_transaction:balance(Account),
0 = ff_indef:current(Amount), 0 = ff_indef:current(Amount),
ok. ok.
@ -252,16 +252,8 @@ get_domain_config(C) ->
get_default_termset() -> get_default_termset() ->
#domain_TermSet{ #domain_TermSet{
% TODO wallets = #domain_WalletServiceTerms{
% - Strangely enough, hellgate checks wallet currency against _payments_
% terms.
payments = #domain_PaymentsServiceTerms{
currencies = {value, ?ordset([?cur(<<"RUB">>)])}, 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, [ cash_limit = {decisions, [
#domain_CashLimitDecision{ #domain_CashLimitDecision{
if_ = {condition, {currency_is, ?cur(<<"RUB">>)}}, if_ = {condition, {currency_is, ?cur(<<"RUB">>)}},
@ -270,18 +262,6 @@ get_default_termset() ->
{exclusive, ?cash(10000000, <<"RUB">>)} {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)
)
]}
}
]} ]}
} }
}. }.

View File

@ -18,14 +18,13 @@
child_spec({HealthRoutes, LogicHandlers}) -> child_spec({HealthRoutes, LogicHandlers}) ->
{Transport, TransportOpts} = get_socket_transport(), {Transport, TransportOpts} = get_socket_transport(),
CowboyOpts = get_cowboy_config(HealthRoutes, LogicHandlers), CowboyOpts = get_cowboy_config(HealthRoutes, LogicHandlers),
AcceptorsPool = genlib_app:env(?APP, acceptors_poolsize, ?DEFAULT_ACCEPTORS_POOLSIZE), ranch:child_spec(?MODULE, Transport, TransportOpts, cowboy_protocol, CowboyOpts).
ranch:child_spec(?MODULE, AcceptorsPool,
Transport, TransportOpts, cowboy_protocol, CowboyOpts).
get_socket_transport() -> get_socket_transport() ->
{ok, IP} = inet:parse_address(genlib_app:env(?APP, ip, ?DEFAULT_IP_ADDR)), {ok, IP} = inet:parse_address(genlib_app:env(?APP, ip, ?DEFAULT_IP_ADDR)),
Port = genlib_app:env(?APP, port, ?DEFAULT_PORT), 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) -> get_cowboy_config(HealthRoutes, LogicHandlers) ->
Dispatch = Dispatch =

View File

@ -1,5 +1,3 @@
%% Temporary stab for wallet handler
-module(wapi_wallet_ff_backend). -module(wapi_wallet_ff_backend).
-include_lib("dmsl/include/dmsl_payment_processing_thrift.hrl"). -include_lib("dmsl/include/dmsl_payment_processing_thrift.hrl").
@ -53,14 +51,14 @@
-spec get_providers([binary()], ctx()) -> [map()]. -spec get_providers([binary()], ctx()) -> [map()].
get_providers(Residences, _Context) -> get_providers(Residences, _Context) ->
ResidenceSet = ordsets:from_list(from_swag(list, {residence, Residences})), ResidenceSet = ordsets:from_list(from_swag({list, residence}, Residences)),
to_swag(list, {provider, [P || to_swag({list, provider}, [P ||
P <- ff_provider:list(), P <- ff_provider:list(),
ordsets:is_subset( ordsets:is_subset(
ResidenceSet, ResidenceSet,
ordsets:from_list(ff_provider:residences(P)) ordsets:from_list(ff_provider:residences(P))
) )
]}). ]).
-spec get_provider(id(), ctx()) -> result(). -spec get_provider(id(), ctx()) -> result().
get_provider(ProviderId, _Context) -> get_provider(ProviderId, _Context) ->
@ -133,7 +131,7 @@ get_identity_challenges(IdentityId, Statuses, Context) ->
Challenges0 = maps:to_list(ff_identity:challenges( Challenges0 = maps:to_list(ff_identity:challenges(
ff_identity_machine:identity(get_state(identity, IdentityId, Context)) 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, enrich_proofs(ff_identity_challenge:proofs(C), Context)} ||
{Id, C} <- Challenges0, {Id, C} <- Challenges0,
Status <- [ff_identity_challenge:status(C)], 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, fun (F) -> filter_identity_challenge_status(F, Status) end,
Statuses Statuses
) )
]}) ])
end). end).
-spec create_identity_challenge(id(), params(), ctx()) -> result(map(), -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) -> get_events(Type = {Resource, _}, ResourceId, Limit, Cursor, Filter, Context) ->
do(fun() -> do(fun() ->
_ = check_resource(Resource, ResourceId, Context), _ = check_resource(Resource, ResourceId, Context),
to_swag(list, { to_swag(
get_event_type(Type), {list, get_event_type(Type)},
collect_events(get_collector(Type, ResourceId), Filter, Cursor, Limit) collect_events(get_collector(Type, ResourceId), Filter, Cursor, Limit)
}) )
end). end).
get_event_type({identity, challenge_event}) -> identity_challenge_event; get_event_type({identity, challenge_event}) -> identity_challenge_event;
@ -484,6 +482,17 @@ next_id(Type) ->
). ).
%% Marshalling %% Marshalling
-type swag_term() ::
#{binary() => swag_term()} |
[swag_term()] |
number() |
binary() |
boolean() .
-spec from_swag(_Type, swag_term()) ->
_Term.
from_swag(identity_params, Params) -> from_swag(identity_params, Params) ->
#{ #{
provider => maps:get(<<"provider">>, Params), provider => maps:get(<<"provider">>, Params),
@ -495,7 +504,7 @@ from_swag(identity_challenge_params, Params) ->
proofs => from_swag(proofs, maps:get(<<"proofs">>, Params)) proofs => from_swag(proofs, maps:get(<<"proofs">>, Params))
}; };
from_swag(proofs, Proofs) -> from_swag(proofs, Proofs) ->
from_swag(list, {proof, Proofs}); from_swag({list, proof}, Proofs);
from_swag(proof, #{<<"token">> := WapiToken}) -> from_swag(proof, #{<<"token">> := WapiToken}) ->
try try
#{<<"type">> := Type, <<"token">> := Token} = wapi_utils:base64url_to_map(WapiToken), #{<<"type">> := Type, <<"token">> := Token} = wapi_utils:base64url_to_map(WapiToken),
@ -554,21 +563,23 @@ from_swag(residence, V) ->
undefined undefined
end; end;
from_swag(list, {Type, List}) -> from_swag({list, Type}, List) ->
lists:map(fun(V) -> from_swag(Type, V) end, List). lists:map(fun(V) -> from_swag(Type, V) end, List).
-spec to_swag(_Type, _Value) ->
swag_term() | undefined.
to_swag(_, undefined) -> to_swag(_, undefined) ->
undefined; undefined;
to_swag(providers, Providers) -> to_swag(providers, Providers) ->
to_swag(list, {provider, Providers}); to_swag({list, provider}, Providers);
to_swag(provider, Provider) -> to_swag(provider, Provider) ->
to_swag(map, #{ to_swag(map, #{
<<"id">> => ff_provider:id(Provider), <<"id">> => ff_provider:id(Provider),
<<"name">> => ff_provider:name(Provider), <<"name">> => ff_provider:name(Provider),
<<"residences">> => to_swag(list, {residence, <<"residences">> => to_swag({list, residence},
ordsets:to_list(ff_provider:residences(Provider)) ordsets:to_list(ff_provider:residences(Provider))
}) )
}); });
to_swag(residence, Residence) -> to_swag(residence, Residence) ->
genlib_string:to_upper(genlib:to_binary(Residence)); genlib_string:to_upper(genlib:to_binary(Residence));
@ -586,10 +597,10 @@ to_swag(identity, State) ->
to_swag(map, #{ to_swag(map, #{
<<"id">> => ff_identity:id(Identity), <<"id">> => ff_identity:id(Identity),
<<"name">> => maps:get(<<"name">>, WapiCtx), <<"name">> => maps:get(<<"name">>, WapiCtx),
<<"createdAt">> => to_swag(timestamp, ff_identity_machine:created(State)), <<"createdAt">> => to_swag(timestamp, ff_machine:created(State)),
<<"provider">> => ff_provider:id(ff_identity:provider(Identity)), <<"provider">> => ff_identity:provider(Identity),
<<"class">> => ff_identity_class:id(ff_identity:class(Identity)), <<"class">> => ff_identity:class(Identity),
<<"level">> => ff_identity_class:level_id(ff_identity:level(Identity)), <<"level">> => ff_identity:level(Identity),
<<"effectiveChallenge">> => to_swag(identity_effective_challenge, ff_identity:effective_challenge(Identity)), <<"effectiveChallenge">> => to_swag(identity_effective_challenge, ff_identity:effective_challenge(Identity)),
<<"isBlocked">> => to_swag(is_blocked, ff_identity:is_accessible(Identity)), <<"isBlocked">> => to_swag(is_blocked, ff_identity:is_accessible(Identity)),
<<"metadata">> => maps:get(<<"metadata">>, WapiCtx, undefined) <<"metadata">> => maps:get(<<"metadata">>, WapiCtx, undefined)
@ -604,7 +615,7 @@ to_swag(identity_challenge, {ChallengeId, Challenge, Proofs}) ->
<<"id">> => ChallengeId, <<"id">> => ChallengeId,
%% TODO add createdAt when it is available on the backend %% TODO add createdAt when it is available on the backend
%% <<"createdAt">> => _, %% <<"createdAt">> => _,
<<"type">> => ff_identity_class:challenge_class_id(ChallengeClass), <<"type">> => ChallengeClass,
<<"proofs">> => Proofs <<"proofs">> => Proofs
}, to_swag(challenge_status, ff_identity_challenge:status(Challenge)))); }, to_swag(challenge_status, ff_identity_challenge:status(Challenge))));
to_swag(challenge_status, pending) -> to_swag(challenge_status, pending) ->
@ -648,7 +659,7 @@ to_swag(wallet, State) ->
<<"name">> => ff_wallet:name(Wallet), <<"name">> => ff_wallet:name(Wallet),
<<"createdAt">> => to_swag(timestamp, ff_machine:created(State)), <<"createdAt">> => to_swag(timestamp, ff_machine:created(State)),
<<"isBlocked">> => to_swag(is_blocked, ff_wallet:is_accessible(Wallet)), <<"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)), <<"currency">> => to_swag(currency, ff_wallet:currency(Wallet)),
<<"metadata">> => genlib_map:get(<<"metadata">>, get_ctx(State)) <<"metadata">> => genlib_map:get(<<"metadata">>, get_ctx(State))
}); });
@ -666,15 +677,14 @@ to_swag(wallet_account, {OwnAmount, AvailableAmount, Currency}) ->
}; };
to_swag(destination, State) -> to_swag(destination, State) ->
Destination = ff_destination_machine:destination(State), Destination = ff_destination_machine:destination(State),
Wallet = ff_destination:wallet(Destination),
to_swag(map, maps:merge( to_swag(map, maps:merge(
#{ #{
<<"id">> => ff_destination:id(Destination), <<"id">> => ff_destination:id(Destination),
<<"name">> => ff_wallet:name(Wallet), <<"name">> => ff_destination:name(Destination),
<<"createdAt">> => to_swag(timestamp, ff_machine:created(State)), <<"createdAt">> => to_swag(timestamp, ff_machine:created(State)),
<<"isBlocked">> => to_swag(is_blocked, ff_wallet:is_accessible(Wallet)), <<"isBlocked">> => to_swag(is_blocked, ff_destination:is_accessible(Destination)),
<<"identity">> => ff_identity:id(ff_wallet:identity(Wallet)), <<"identity">> => ff_destination:identity(Destination),
<<"currency">> => to_swag(currency, ff_wallet:currency(Wallet)), <<"currency">> => to_swag(currency, ff_destination:currency(Destination)),
<<"resource">> => to_swag(destination_resource, ff_destination:resource(Destination)), <<"resource">> => to_swag(destination_resource, ff_destination:resource(Destination)),
<<"metadata">> => genlib_map:get(<<"metadata">>, get_ctx(State)) <<"metadata">> => genlib_map:get(<<"metadata">>, get_ctx(State))
}, },
@ -705,10 +715,10 @@ to_swag(withdrawal, State) ->
to_swag(map, maps:merge( to_swag(map, maps:merge(
#{ #{
<<"id">> => ff_withdrawal:id(Withdrawal), <<"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)), <<"metadata">> => genlib_map:get(<<"metadata">>, get_ctx(State)),
<<"wallet">> => ff_wallet:id(ff_withdrawal:source(Withdrawal)), <<"wallet">> => ff_withdrawal:source(Withdrawal),
<<"destination">> => ff_destination:id(ff_withdrawal:destination(Withdrawal)), <<"destination">> => ff_withdrawal:destination(Withdrawal),
<<"body">> => to_swag(withdrawal_body, ff_withdrawal:body(Withdrawal)) <<"body">> => to_swag(withdrawal_body, ff_withdrawal:body(Withdrawal))
}, },
to_swag(withdrawal_status, ff_withdrawal:status(Withdrawal)) to_swag(withdrawal_status, ff_withdrawal:status(Withdrawal))
@ -757,7 +767,7 @@ to_swag(is_blocked, {ok, accessible}) ->
to_swag(is_blocked, _) -> to_swag(is_blocked, _) ->
true; true;
to_swag(list, {Type, List}) -> to_swag({list, Type}, List) ->
lists:map(fun(V) -> to_swag(Type, V) end, List); lists:map(fun(V) -> to_swag(Type, V) end, List);
to_swag(map, Map) -> to_swag(map, Map) ->
genlib_map:compact(Map); genlib_map:compact(Map);

View File

@ -27,7 +27,7 @@ services:
condition: service_healthy condition: service_healthy
hellgate: hellgate:
image: dr.rbkmoney.com/rbkmoney/hellgate:88abe5c9c3febf567e7269e81b2b808c01500b43 image: dr.rbkmoney.com/rbkmoney/hellgate:eae821f2a1f3f2b948390d922c5eb3cde885757d
command: /opt/hellgate/bin/hellgate foreground command: /opt/hellgate/bin/hellgate foreground
depends_on: depends_on:
machinegun: machinegun:
@ -82,7 +82,7 @@ services:
retries: 20 retries: 20
dominant: dominant:
image: dr.rbkmoney.com/rbkmoney/dominant:1756bbac6999fa46fbe44a72c74c02e616eda0f6 image: dr.rbkmoney.com/rbkmoney/dominant:4e296b03cd4adba4bd0f1cf85425b9514728107c
command: /opt/dominant/bin/dominant foreground command: /opt/dominant/bin/dominant foreground
depends_on: depends_on:
machinegun: machinegun: