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