Handle transfers through noion of accounts (yeah, again) (#9)

* Drop (de)hydration for now, we'll think about it later.
* Reduce boilerplate w/ the help of `ff_machine` though much to be done still.
* Drop half-baked `ff_machine` from ff_core
* Supply missing specs + fix marshalling types
* Update test fixtures
This commit is contained in:
Andrew Mayorov 2018-07-16 17:21:17 +03:00 committed by GitHub
parent ce160a7783
commit 7c7ad60e1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1010 additions and 1131 deletions

View File

@ -10,20 +10,25 @@
* [x] Реализовать честный identity challenge
* [x] Запилить 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 объектов. В идеале подобное должно быть точечно конфигурируемым.

View File

@ -1,34 +0,0 @@
%%%
%%% Fistful machine generic accessors.
%%%
-module(ff_machine).
-type timestamp() :: machinery:timestamp().
-type ctx() :: ff_ctx:ctx().
-type st() ::#{
ctx := ctx(),
times => {timestamp(), timestamp()},
_ => _
}.
-export_type([st/0]).
%% Accessors API
-export([ctx/1]).
-export([created/1]).
-export([updated/1]).
%% Accessors
-spec ctx(st()) -> ctx().
-spec created(st()) -> timestamp() | undefined.
-spec updated(st()) -> timestamp() | undefined.
ctx(#{ctx := V}) -> V.
created(St) -> erlang:element(1, times(St)).
updated(St) -> erlang:element(2, times(St)).
times(St) ->
genlib_map:get(times, St, {undefined, undefined}).

View File

@ -7,6 +7,9 @@
-include_lib("identdocstore_proto/include/identdocstore_identity_document_storage_thrift.hrl").
-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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()))))

View File

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

View File

@ -0,0 +1,129 @@
%%%
%%% Account
%%%
%%% Responsible for, at least:
%%% - managing partymgmt-related wallet stuff,
%%% - acknowledging transfer postings,
%%% - accounting and checking limits.
%%%
-module(ff_account).
-type id(T) :: T.
-type identity() :: ff_identity:id().
-type currency() :: ff_currency:id().
-type account() :: #{
id := id(binary()),
identity := identity(),
currency := currency(),
pm_wallet := ff_party:wallet_id()
}.
-type event() ::
{created, account()}.
-export_type([account/0]).
-export_type([event/0]).
-export([id/1]).
-export([identity/1]).
-export([currency/1]).
-export([pm_wallet/1]).
-export([pm_account/1]).
-export([create/3]).
-export([is_accessible/1]).
-export([apply_event/2]).
%% Pipeline
-import(ff_pipeline, [do/1, unwrap/1, unwrap/2]).
%% Accessors
-spec id(account()) ->
id(binary()).
-spec identity(account()) ->
identity().
-spec currency(account()) ->
currency().
-spec pm_wallet(account()) ->
ff_party:wallet_id().
id(#{id := ID}) ->
ID.
identity(#{identity := IdentityID}) ->
IdentityID.
currency(#{currency := CurrencyID}) ->
CurrencyID.
pm_wallet(#{pm_wallet := PMWalletID}) ->
PMWalletID.
-spec pm_account(account()) ->
ff_transaction:account().
pm_account(Account) ->
{ok, Identity} = ff_identity_machine:get(identity(Account)),
{ok, PMAccount} = ff_party:get_wallet_account(
ff_identity:party(ff_identity_machine:identity(Identity)),
pm_wallet(Account)
),
PMAccount.
%% Actuators
-spec create(id(_), identity(), currency()) ->
{ok, [event()]} |
{error,
{identity, notfound} |
{currency, notfound} |
{contract, notfound} |
ff_party:inaccessibility() |
invalid
}.
create(ID, IdentityID, CurrencyID) ->
do(fun () ->
Identity = ff_identity_machine:identity(unwrap(identity, ff_identity_machine:get(IdentityID))),
_Currency = unwrap(currency, ff_currency:get(CurrencyID)),
PMWalletID = unwrap(ff_party:create_wallet(
ff_identity:party(Identity),
ff_identity:contract(Identity),
#{
name => ff_string:join($/, [<<"ff/account">>, ID]),
currency => CurrencyID
}
)),
[{created, #{
id => ID,
identity => IdentityID,
currency => CurrencyID,
pm_wallet => PMWalletID
}}]
end).
-spec is_accessible(account()) ->
{ok, accessible} |
{error, ff_party:inaccessibility()}.
is_accessible(Account) ->
do(fun () ->
Identity = get_identity(Account),
accessible = unwrap(ff_identity:is_accessible(Identity)),
accessible = unwrap(ff_party:is_wallet_accessible(ff_identity:party(Identity), pm_wallet(Account)))
end).
get_identity(Account) ->
{ok, V} = ff_identity_machine:get(identity(Account)),
ff_identity_machine:identity(V).
%% State
-spec apply_event(event(), ff_maybe:maybe(account())) ->
account().
apply_event({created, Account}, undefined) ->
Account.

View File

@ -17,11 +17,11 @@
-type id(T) :: T.
-type 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)}.

View File

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

View File

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

View File

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

View File

@ -0,0 +1,161 @@
%%%
%%% Generic machine
%%%
%%% TODOs
%%%
%%% - Split ctx and time tracking into different machine layers.
%%%
-module(ff_machine).
-type id() :: machinery:id().
-type namespace() :: machinery:namespace().
-type timestamp() :: machinery:timestamp().
-type ctx() :: ff_ctx:ctx().
-type st(Model) :: #{
model := Model,
ctx := ctx(),
times => {timestamp(), timestamp()}
}.
-type timestamped_event(T) ::
{ev, timestamp(), T}.
-type auxst() ::
#{ctx := ctx()}.
-type machine(T) ::
machinery:machine(timestamped_event(T), auxst()).
-type result(T) ::
machinery:result(timestamped_event(T), auxst()).
-export_type([st/1]).
-export_type([machine/1]).
-export_type([result/1]).
-export_type([timestamped_event/1]).
%% Accessors
-export([model/1]).
-export([ctx/1]).
-export([created/1]).
-export([updated/1]).
%%
-export([get/3]).
-export([collapse/2]).
-export([emit_event/1]).
-export([emit_events/1]).
%%
-export([init/4]).
-export([process_timeout/3]).
-export([process_call/4]).
%%
-import(ff_pipeline, [do/1, unwrap/1]).
%%
-spec model(st(Model)) ->
Model.
-spec ctx(st(_)) ->
ctx().
-spec created(st(_)) ->
timestamp() | undefined.
-spec updated(st(_)) ->
timestamp() | undefined.
model(#{model := V}) ->
V.
ctx(#{ctx := V}) ->
V.
created(St) ->
erlang:element(1, times(St)).
updated(St) ->
erlang:element(2, times(St)).
times(St) ->
genlib_map:get(times, St, {undefined, undefined}).
%%
-spec get(module(), namespace(), id()) ->
{ok, st(_)} |
{error, notfound}.
get(Mod, NS, ID) ->
do(fun () ->
collapse(Mod, unwrap(machinery:get(NS, ID, fistful:backend(NS))))
end).
-spec collapse(module(), machine(_)) ->
st(_).
collapse(Mod, #{history := History, aux_state := #{ctx := Ctx}}) ->
collapse_history(Mod, History, #{ctx => Ctx}).
collapse_history(Mod, History, St0) ->
lists:foldl(fun (Ev, St) -> merge_event(Mod, Ev, St) end, St0, History).
-spec emit_event(E) ->
[timestamped_event(E)].
emit_event(Event) ->
emit_events([Event]).
-spec emit_events([E]) ->
[timestamped_event(E)].
emit_events(Events) ->
emit_timestamped_events(Events, machinery_time:now()).
emit_timestamped_events(Events, Ts) ->
[{ev, Ts, Body} || Body <- Events].
merge_event(Mod, {_ID, _Ts, TsEvent}, St0) ->
{Ev, St1} = merge_timestamped_event(TsEvent, St0),
Model1 = Mod:apply_event(Ev, maps:get(model, St1, undefined)),
St1#{model => Model1}.
merge_timestamped_event({ev, Ts, Body}, St = #{times := {Created, _Updated}}) ->
{Body, St#{times => {Created, Ts}}};
merge_timestamped_event({ev, Ts, Body}, St = #{}) ->
{Body, St#{times => {Ts, Ts}}}.
%%
-spec init({machinery:args(_), ctx()}, machinery:machine(E, A), module(), _) ->
machinery:result(E, A).
init({Args, Ctx}, _Machine, Mod, _) ->
Events = Mod:init(Args),
#{
events => emit_events(Events),
aux_state => #{ctx => Ctx}
}.
-spec process_timeout(machinery:machine(E, A), module(), _) ->
machinery:result(E, A).
process_timeout(Machine, Mod, _) ->
Events = Mod:process_timeout(collapse(Mod, Machine)),
#{
events => emit_events(Events)
}.
-spec process_call(machinery:args(_), machinery:machine(E, A), module(), _) ->
{machinery:response(_), machinery:result(E, A)}.
process_call(Args, Machine, Mod, _) ->
{Response, Events} = Mod:process_call(Args, collapse(Mod, Machine)),
{Response, #{
events => emit_events(Events)
}}.

View File

@ -24,6 +24,11 @@
-export_type([wallet/0]).
-export_type([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),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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);

View File

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