mirror of
https://github.com/valitydev/fistful-server.git
synced 2024-11-06 02:35:18 +00:00
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:
parent
ce160a7783
commit
7c7ad60e1b
17
README.md
17
README.md
@ -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 объектов. В идеале подобное должно быть точечно конфигурируемым.
|
||||||
|
@ -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}).
|
|
@ -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,
|
||||||
|
@ -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}.
|
|
||||||
|
@ -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}}}.
|
|
||||||
|
@ -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}.
|
|
||||||
|
@ -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}}}.
|
|
||||||
|
@ -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).
|
||||||
|
@ -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()))))
|
||||||
|
@ -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">>)}
|
||||||
|
)}
|
||||||
|
}
|
||||||
|
]}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
|
129
apps/fistful/src/ff_account.erl
Normal file
129
apps/fistful/src/ff_account.erl
Normal 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.
|
@ -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)}.
|
|
||||||
|
@ -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").
|
||||||
|
@ -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.
|
||||||
|
@ -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}}}.
|
|
||||||
|
161
apps/fistful/src/ff_machine.erl
Normal file
161
apps/fistful/src/ff_machine.erl
Normal 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)
|
||||||
|
}}.
|
@ -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),
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
%%%
|
%%%
|
||||||
%%% Financial transaction between accounts
|
%%% Financial transaction between accounts
|
||||||
%%%
|
%%%
|
||||||
|
%%% - Rename to `ff_posting_plan`?
|
||||||
|
%%%
|
||||||
|
|
||||||
-module(ff_transaction).
|
-module(ff_transaction).
|
||||||
|
|
||||||
|
@ -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))]
|
||||||
].
|
].
|
||||||
|
@ -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}.
|
|
||||||
|
@ -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}}}.
|
|
||||||
|
@ -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)
|
|
||||||
)
|
|
||||||
]}
|
|
||||||
}
|
|
||||||
]}
|
]}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
@ -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 =
|
||||||
|
@ -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);
|
||||||
|
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user