From 729611ff5d26bf4df24b2aea253196077b99d76c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Mon, 3 Apr 2023 09:16:06 +0300 Subject: [PATCH] TD-509: Claim commiter (#51) * added base * finished claim commiter code * added base tests * finished base tests * renamed cache * changed cache ref * changed damsel ref * reverted to checkout v2 * change to custom workflow * reverted to base workflow * added codecove secret * changed to custom workflow commit * changed * added full ref * changed to new action version, removed local action * changed action version * refactored run job * Revert "refactored run job" This reverts commit e215103bcee72a460cf912915b939044a0c348f7. * reverted commit id * updated workflow ref * reverted workflow change * added requested changes * added requested changes --- .github/workflows/erlang-checks.yml | 2 +- apps/ff_claim/include/ff_claim_management.hrl | 144 ++++++++++ apps/ff_claim/src/ff_claim.app.src | 15 ++ apps/ff_claim/src/ff_claim_committer.erl | 193 ++++++++++++++ apps/ff_claim/test/ff_claim_SUITE.erl | 250 ++++++++++++++++++ .../src/ff_claim_committer_handler.erl | 32 +++ apps/ff_server/src/ff_server.app.src | 3 +- apps/ff_server/src/ff_server.erl | 3 +- apps/ff_server/src/ff_services.erl | 8 +- .../src/ff_adapter_withdrawal_codec.erl | 17 ++ apps/fistful/src/ff_account.erl | 38 ++- apps/fistful/src/ff_identity.erl | 23 +- apps/fistful/src/ff_wallet.erl | 28 +- rebar.config | 1 + 14 files changed, 732 insertions(+), 25 deletions(-) create mode 100644 apps/ff_claim/include/ff_claim_management.hrl create mode 100644 apps/ff_claim/src/ff_claim.app.src create mode 100644 apps/ff_claim/src/ff_claim_committer.erl create mode 100644 apps/ff_claim/test/ff_claim_SUITE.erl create mode 100644 apps/ff_server/src/ff_claim_committer_handler.erl diff --git a/.github/workflows/erlang-checks.yml b/.github/workflows/erlang-checks.yml index 2a84d8d..53ea743 100644 --- a/.github/workflows/erlang-checks.yml +++ b/.github/workflows/erlang-checks.yml @@ -36,4 +36,4 @@ jobs: use-thrift: true thrift-version: ${{ needs.setup.outputs.thrift-version }} run-ct-with-compose: true - cache-version: v2 + cache-version: v100 diff --git a/apps/ff_claim/include/ff_claim_management.hrl b/apps/ff_claim/include/ff_claim_management.hrl new file mode 100644 index 0000000..9386f7a --- /dev/null +++ b/apps/ff_claim/include/ff_claim_management.hrl @@ -0,0 +1,144 @@ +-ifndef(__ff_claim_management_hrl__). +-define(__ff_claim_management_hrl__, included). + +-define(cm_modification_unit(ModID, Timestamp, Mod, UserInfo), #claimmgmt_ModificationUnit{ + modification_id = ModID, + created_at = Timestamp, + modification = Mod, + user_info = UserInfo +}). + +-define(cm_wallet_modification(ModID, Timestamp, Mod, UserInfo), + ?cm_modification_unit(ModID, Timestamp, {wallet_modification, Mod}, UserInfo) +). + +-define(cm_identity_modification(ModID, Timestamp, Mod, UserInfo), + ?cm_modification_unit(ModID, Timestamp, {identity_modification, Mod}, UserInfo) +). + +%%% Identity + +-define(cm_identity_creation(PartyID, IdentityID, Provider, Params), + {identity_modification, #claimmgmt_IdentityModificationUnit{ + id = IdentityID, + modification = + {creation, + Params = #claimmgmt_IdentityParams{ + party_id = PartyID, + provider = Provider + }} + }} +). + +%%% Wallet + +-define(cm_wallet_creation(IdentityID, WalletID, Currency, Params), + {wallet_modification, #claimmgmt_NewWalletModificationUnit{ + id = WalletID, + modification = + {creation, + Params = #claimmgmt_NewWalletParams{ + identity_id = IdentityID, + currency = Currency + }} + }} +). + +%%% Error + +-define(cm_invalid_changeset(Reason, InvalidChangeset), #claimmgmt_InvalidChangeset{ + reason = Reason, + invalid_changeset = InvalidChangeset +}). + +-define(cm_invalid_identity_already_exists(ID), + { + invalid_identity_changeset, + #claimmgmt_InvalidIdentityChangesetReason{ + id = ID, + reason = {already_exists, #claimmgmt_InvalidClaimConcreteReason{}} + } + } +). + +-define(cm_invalid_identity_provider_not_found(ID), + { + invalid_identity_changeset, + #claimmgmt_InvalidIdentityChangesetReason{ + id = ID, + reason = {provider_not_found, #claimmgmt_InvalidClaimConcreteReason{}} + } + } +). + +-define(cm_invalid_identity_party_not_found(ID), + { + invalid_identity_changeset, + #claimmgmt_InvalidIdentityChangesetReason{ + id = ID, + reason = {party_not_found, #claimmgmt_InvalidClaimConcreteReason{}} + } + } +). + +-define(cm_invalid_identity_party_inaccessible(ID), + { + invalid_identity_changeset, + #claimmgmt_InvalidIdentityChangesetReason{ + id = ID, + reason = {party_inaccessible, #claimmgmt_InvalidClaimConcreteReason{}} + } + } +). + +-define(cm_invalid_wallet_already_exists(ID), + { + invalid_wallet_changeset, + #claimmgmt_InvalidNewWalletChangesetReason{ + id = ID, + reason = {already_exists, #claimmgmt_InvalidClaimConcreteReason{}} + } + } +). + +-define(cm_invalid_wallet_identity_not_found(ID), + { + invalid_wallet_changeset, + #claimmgmt_InvalidNewWalletChangesetReason{ + id = ID, + reason = {identity_not_found, #claimmgmt_InvalidClaimConcreteReason{}} + } + } +). + +-define(cm_invalid_wallet_currency_not_found(ID), + { + invalid_wallet_changeset, + #claimmgmt_InvalidNewWalletChangesetReason{ + id = ID, + reason = {currency_not_found, #claimmgmt_InvalidClaimConcreteReason{}} + } + } +). + +-define(cm_invalid_wallet_currency_not_allowed(ID), + { + invalid_wallet_changeset, + #claimmgmt_InvalidNewWalletChangesetReason{ + id = ID, + reason = {currency_not_allowed, #claimmgmt_InvalidClaimConcreteReason{}} + } + } +). + +-define(cm_invalid_wallet_party_inaccessible(ID), + { + invalid_wallet_changeset, + #claimmgmt_InvalidNewWalletChangesetReason{ + id = ID, + reason = {party_inaccessible, #claimmgmt_InvalidClaimConcreteReason{}} + } + } +). + +-endif. diff --git a/apps/ff_claim/src/ff_claim.app.src b/apps/ff_claim/src/ff_claim.app.src new file mode 100644 index 0000000..302534f --- /dev/null +++ b/apps/ff_claim/src/ff_claim.app.src @@ -0,0 +1,15 @@ +{application, ff_claim, [ + {description, "Wallet claims"}, + {vsn, "1"}, + {registered, []}, + {applications, [ + kernel, + stdlib, + genlib, + damsel, + fistful, + ff_transfer + ]}, + {licenses, ["Apache 2.0"]}, + {links, ["https://github.com/rbkmoney/fistful-server"]} +]}. diff --git a/apps/ff_claim/src/ff_claim_committer.erl b/apps/ff_claim/src/ff_claim_committer.erl new file mode 100644 index 0000000..900451f --- /dev/null +++ b/apps/ff_claim/src/ff_claim_committer.erl @@ -0,0 +1,193 @@ +-module(ff_claim_committer). + +-include_lib("damsel/include/dmsl_claimmgmt_thrift.hrl"). +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). +-include_lib("damsel/include/dmsl_base_thrift.hrl"). +-include_lib("damsel/include/dmsl_payproc_thrift.hrl"). + +-include("ff_claim_management.hrl"). + +-export([filter_ff_modifications/1]). +-export([assert_modifications_applicable/1]). +-export([apply_modifications/1]). + +-type changeset() :: dmsl_claimmgmt_thrift:'ClaimChangeset'(). +-type modification() :: dmsl_claimmgmt_thrift:'PartyModification'(). +-type modifications() :: [modification()]. + +-export_type([modification/0]). +-export_type([modifications/0]). + +-spec filter_ff_modifications(changeset()) -> modifications(). +filter_ff_modifications(Changeset) -> + lists:filtermap( + fun + (?cm_identity_modification(_, _, Change, _)) -> + {true, {identity_modification, Change}}; + (?cm_wallet_modification(_, _, Change, _)) -> + {true, {wallet_modification, Change}}; + (_) -> + false + end, + Changeset + ). + +%% Used same checks as in identity/wallet create function +-spec assert_modifications_applicable(modifications()) -> ok | no_return(). +assert_modifications_applicable([FFChange | Others]) -> + ok = + case FFChange of + ?cm_identity_creation(PartyID, IdentityID, Provider, _Params) -> + case ff_identity_machine:get(IdentityID) of + {ok, _Machine} -> + raise_invalid_changeset(?cm_invalid_identity_already_exists(IdentityID), [FFChange]); + {error, notfound} -> + assert_identity_creation_applicable(PartyID, IdentityID, Provider, FFChange) + end; + ?cm_wallet_creation(IdentityID, WalletID, Currency, _Params) -> + case ff_wallet_machine:get(WalletID) of + {ok, _Machine} -> + raise_invalid_changeset(?cm_invalid_wallet_already_exists(WalletID), [FFChange]); + {error, notfound} -> + assert_wallet_creation_modification_applicable(IdentityID, WalletID, Currency, FFChange) + end + end, + assert_modifications_applicable(Others); +assert_modifications_applicable([]) -> + ok. + +-spec apply_modifications(modifications()) -> ok | no_return(). +apply_modifications([FFChange | Others]) -> + ok = + case FFChange of + ?cm_identity_creation(_PartyID, IdentityID, _Provider, Params) -> + #claimmgmt_IdentityParams{metadata = Metadata} = Params, + apply_identity_creation(IdentityID, Metadata, Params, FFChange); + ?cm_wallet_creation(_IdentityID, WalletID, _Currency, Params) -> + #claimmgmt_NewWalletParams{metadata = Metadata} = Params, + apply_wallet_creation(WalletID, Metadata, Params, FFChange) + end, + apply_modifications(Others); +apply_modifications([]) -> + ok. + +%%% Internal functions +assert_identity_creation_applicable(PartyID, IdentityID, Provider, Change) -> + case ff_identity:check_identity_creation(#{party => PartyID, provider => Provider}) of + {ok, _} -> + ok; + {error, {provider, notfound}} -> + raise_invalid_changeset(?cm_invalid_identity_provider_not_found(IdentityID), [Change]); + {error, {party, notfound}} -> + throw(#claimmgmt_PartyNotFound{}); + {error, {party, {inaccessible, _}}} -> + raise_invalid_changeset(?cm_invalid_identity_party_inaccessible(IdentityID), [Change]) + end. + +apply_identity_creation(IdentityID, Metadata, ChangeParams, Change) -> + Params = #{party := PartyID} = unmarshal_identity_params(IdentityID, ChangeParams), + case ff_identity_machine:create(Params, create_context(PartyID, Metadata)) of + ok -> + ok; + {error, {provider, notfound}} -> + raise_invalid_changeset(?cm_invalid_identity_provider_not_found(IdentityID), [Change]); + {error, {party, notfound}} -> + throw(#claimmgmt_PartyNotFound{}); + {error, {party, {inaccessible, _}}} -> + raise_invalid_changeset(?cm_invalid_identity_party_inaccessible(IdentityID), [Change]); + {error, exists} -> + raise_invalid_changeset(?cm_invalid_identity_already_exists(IdentityID), [Change]); + {error, Error} -> + woody_error:raise(system, {internal, result_unexpected, woody_error:format_details(Error)}) + end. + +assert_wallet_creation_modification_applicable(IdentityID, WalletID, DomainCurrency, Change) -> + #domain_CurrencyRef{symbolic_code = CurrencyID} = DomainCurrency, + case ff_wallet:check_creation(#{identity => IdentityID, currency => CurrencyID}) of + {ok, {Identity, Currency}} -> + case ff_account:check_account_creation(WalletID, Identity, Currency) of + {ok, valid} -> + ok; + %% not_allowed_currency + {error, {terms, _}} -> + raise_invalid_changeset(?cm_invalid_wallet_currency_not_allowed(WalletID), [Change]); + {error, {party, {inaccessible, _}}} -> + raise_invalid_changeset(?cm_invalid_wallet_party_inaccessible(WalletID), [Change]) + end; + {error, {identity, notfound}} -> + raise_invalid_changeset(?cm_invalid_wallet_identity_not_found(WalletID), [Change]); + {error, {currency, notfound}} -> + raise_invalid_changeset(?cm_invalid_wallet_currency_not_found(WalletID), [Change]) + end. + +apply_wallet_creation(WalletID, Metadata, ChangeParams, Change) -> + Params = #{identity := IdentityID} = unmarshal_wallet_params(WalletID, ChangeParams), + PartyID = + case ff_identity_machine:get(IdentityID) of + {ok, Machine} -> + Identity = ff_identity_machine:identity(Machine), + ff_identity:party(Identity); + {error, notfound} -> + raise_invalid_changeset(?cm_invalid_wallet_identity_not_found(WalletID), [Change]) + end, + case ff_wallet_machine:create(Params, create_context(PartyID, Metadata)) of + ok -> + ok; + {error, {identity, notfound}} -> + raise_invalid_changeset(?cm_invalid_wallet_identity_not_found(WalletID), [Change]); + {error, {currency, notfound}} -> + raise_invalid_changeset(?cm_invalid_wallet_currency_not_found(WalletID), [Change]); + {error, {party, _Inaccessible}} -> + raise_invalid_changeset(?cm_invalid_wallet_party_inaccessible(WalletID), [Change]); + {error, exists} -> + raise_invalid_changeset(?cm_invalid_wallet_already_exists(WalletID), [Change]); + {error, Error} -> + woody_error:raise(system, {internal, result_unexpected, woody_error:format_details(Error)}) + end. + +-spec raise_invalid_changeset(dmsl_claimmgmt_thrift:'InvalidChangesetReason'(), modifications()) -> no_return(). +raise_invalid_changeset(Reason, Modifications) -> + throw(?cm_invalid_changeset(Reason, Modifications)). + +unmarshal_identity_params(IdentityID, #claimmgmt_IdentityParams{ + name = Name, + party_id = PartyID, + provider = ProviderID, + metadata = Metadata +}) -> + genlib_map:compact(#{ + id => IdentityID, + name => Name, + party => PartyID, + provider => ProviderID, + metadata => maybe_unmarshal_metadata(Metadata) + }). + +unmarshal_wallet_params(WalletID, #claimmgmt_NewWalletParams{ + identity_id = IdentityID, + name = Name, + currency = DomainCurrency, + metadata = Metadata +}) -> + #domain_CurrencyRef{symbolic_code = CurrencyID} = DomainCurrency, + genlib_map:compact(#{ + id => WalletID, + name => Name, + identity => IdentityID, + currency => CurrencyID, + metadata => maybe_unmarshal_metadata(Metadata) + }). + +maybe_unmarshal_metadata(undefined) -> + undefined; +maybe_unmarshal_metadata(Metadata) when is_map(Metadata) -> + maps:map(fun(_NS, V) -> ff_adapter_withdrawal_codec:unmarshal_msgpack(V) end, Metadata). + +create_context(PartyID, Metadata) -> + #{ + %% same as used in wapi lib + <<"com.rbkmoney.wapi">> => genlib_map:compact(#{ + <<"owner">> => PartyID, + <<"metadata">> => maybe_unmarshal_metadata(Metadata) + }) + }. diff --git a/apps/ff_claim/test/ff_claim_SUITE.erl b/apps/ff_claim/test/ff_claim_SUITE.erl new file mode 100644 index 0000000..5908fb7 --- /dev/null +++ b/apps/ff_claim/test/ff_claim_SUITE.erl @@ -0,0 +1,250 @@ +-module(ff_claim_SUITE). + +-include_lib("stdlib/include/assert.hrl"). +-include_lib("damsel/include/dmsl_claimmgmt_thrift.hrl"). +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). + +-include("ff_claim_management.hrl"). + +%% Common test API + +-export([all/0]). +-export([groups/0]). +-export([init_per_suite/1]). +-export([end_per_suite/1]). +-export([init_per_group/2]). +-export([end_per_group/2]). +-export([init_per_testcase/2]). +-export([end_per_testcase/2]). + +-define(TEST_IDENTITY_CREATION(IdentityID, Params), #claimmgmt_IdentityModificationUnit{ + id = IdentityID, + modification = {creation, Params} +}). + +-define(TEST_WALLET_CREATION(WalletID, Params), #claimmgmt_NewWalletModificationUnit{ + id = WalletID, + modification = {creation, Params} +}). + +-define(USER_INFO, #claimmgmt_UserInfo{ + id = <<"id">>, + email = <<"email">>, + username = <<"username">>, + type = {internal_user, #claimmgmt_InternalUser{}} +}). + +-define(CLAIM(PartyID, Claim), #claimmgmt_Claim{ + id = 1, + party_id = PartyID, + status = {pending, #claimmgmt_ClaimPending{}}, + revision = 1, + created_at = <<"2026-03-22T06:12:27Z">>, + changeset = [Claim] +}). + +%% Tests + +-export([accept_identity_creation/1]). +-export([accept_identity_creation_already_exists/1]). +-export([apply_identity_creation/1]). + +-export([accept_wallet_creation/1]). +-export([accept_wallet_creation_already_exists/1]). +-export([apply_wallet_creation/1]). + +%% Internal types + +-type config() :: ct_helper:config(). +-type test_case_name() :: ct_helper:test_case_name(). +-type group_name() :: ct_helper:group_name(). +-type test_return() :: _ | no_return(). + +%% API + +-spec all() -> [test_case_name() | {group, group_name()}]. +all() -> + [{group, default}]. + +-spec groups() -> [{group_name(), list(), [test_case_name()]}]. +groups() -> + [ + {default, [parallel], [ + accept_identity_creation, + accept_identity_creation_already_exists, + apply_identity_creation, + accept_wallet_creation, + accept_wallet_creation_already_exists, + apply_wallet_creation + ]} + ]. + +-spec init_per_suite(config()) -> config(). +init_per_suite(C) -> + ct_helper:makeup_cfg( + [ + ct_helper:test_case_name(init), + ct_payment_system:setup() + ], + C + ). + +-spec end_per_suite(config()) -> _. +end_per_suite(C) -> + ok = ct_payment_system:shutdown(C). + +%% + +-spec init_per_group(group_name(), config()) -> config(). +init_per_group(_, C) -> + C. + +-spec end_per_group(group_name(), config()) -> _. +end_per_group(_, _) -> + ok. + +%% + +-spec init_per_testcase(test_case_name(), config()) -> config(). +init_per_testcase(Name, C) -> + C1 = ct_helper:makeup_cfg([ct_helper:test_case_name(Name), ct_helper:woody_ctx()], C), + ok = ct_helper:set_context(C1), + C1. + +-spec end_per_testcase(test_case_name(), config()) -> _. +end_per_testcase(_Name, _C) -> + ok = ct_helper:unset_context(). + +%% Tests + +-spec accept_identity_creation(config()) -> test_return(). +accept_identity_creation(_C) -> + #{party_id := PartyID} = prepare_standard_environment(), + IdentityID = genlib:bsuuid(), + Claim = make_identity_creation_claim(PartyID, IdentityID, <<"good-one">>), + {ok, ok} = call_service('Accept', {PartyID, ?CLAIM(PartyID, Claim)}), + ok. + +-spec accept_identity_creation_already_exists(config()) -> test_return(). +accept_identity_creation_already_exists(_C) -> + #{party_id := PartyID, identity_id := IdentityID} = prepare_standard_environment(), + Claim = make_identity_creation_claim(PartyID, IdentityID, <<"good-one">>), + ?assertMatch( + {exception, #claimmgmt_InvalidChangeset{reason = ?cm_invalid_identity_already_exists(IdentityID)}}, + call_service('Accept', {PartyID, ?CLAIM(PartyID, Claim)}) + ). + +-spec apply_identity_creation(config()) -> test_return(). +apply_identity_creation(_C) -> + #{party_id := PartyID} = prepare_standard_environment(), + IdentityID = genlib:bsuuid(), + Claim = make_identity_creation_claim(PartyID, IdentityID, <<"good-one">>), + {ok, ok} = call_service('Commit', {PartyID, ?CLAIM(PartyID, Claim)}), + _Identity = get_identity(IdentityID), + ok. + +-spec accept_wallet_creation(config()) -> test_return(). +accept_wallet_creation(_C) -> + #{ + party_id := PartyID, + identity_id := IdentityID + } = prepare_standard_environment(), + WalletID = genlib:bsuuid(), + Claim = make_wallet_creation_claim(WalletID, IdentityID, <<"RUB">>), + {ok, ok} = call_service('Accept', {PartyID, ?CLAIM(PartyID, Claim)}), + ok. + +-spec accept_wallet_creation_already_exists(config()) -> test_return(). +accept_wallet_creation_already_exists(_C) -> + #{ + party_id := PartyID, + identity_id := IdentityID, + wallet_id := WalletID + } = prepare_standard_environment(), + Claim = make_wallet_creation_claim(WalletID, IdentityID, <<"RUB">>), + ?assertMatch( + {exception, #claimmgmt_InvalidChangeset{reason = ?cm_invalid_wallet_already_exists(WalletID)}}, + call_service('Accept', {PartyID, ?CLAIM(PartyID, Claim)}) + ). + +-spec apply_wallet_creation(config()) -> test_return(). +apply_wallet_creation(_C) -> + #{ + party_id := PartyID, + identity_id := IdentityID + } = prepare_standard_environment(), + WalletID = genlib:bsuuid(), + Claim = make_wallet_creation_claim(WalletID, IdentityID, <<"RUB">>), + {ok, ok} = call_service('Commit', {PartyID, ?CLAIM(PartyID, Claim)}), + _Wallet = get_wallet(WalletID), + ok. + +%% Utils + +call_service(Fun, Args) -> + Service = {dmsl_claimmgmt_thrift, 'ClaimCommitter'}, + Request = {Service, Fun, Args}, + Client = ff_woody_client:new(#{ + url => <<"http://localhost:8022/v1/claim_committer">>, + event_handler => scoper_woody_event_handler + }), + ff_woody_client:call(Client, Request). + +prepare_standard_environment() -> + PartyID = create_party(), + IdentityID = create_identity(PartyID), + WalletID = create_wallet(IdentityID, <<"My wallet">>, <<"RUB">>), + #{ + wallet_id => WalletID, + identity_id => IdentityID, + party_id => PartyID + }. + +create_party() -> + ID = genlib:bsuuid(), + _ = ff_party:create(ID), + ID. +create_identity(Party) -> + Name = <<"Identity Name">>, + ID = genlib:unique(), + ok = ff_identity_machine:create( + #{id => ID, name => Name, party => Party, provider => <<"good-one">>}, + #{<<"com.rbkmoney.wapi">> => #{<<"name">> => Name, <<"owner">> => Party}} + ), + ID. + +get_identity(ID) -> + {ok, Machine} = ff_identity_machine:get(ID), + ff_identity_machine:identity(Machine). + +create_wallet(IdentityID, Name, Currency) -> + ID = genlib:unique(), + ok = ff_wallet_machine:create( + #{id => ID, identity => IdentityID, name => Name, currency => Currency}, + ff_entity_context:new() + ), + ID. + +get_wallet(ID) -> + {ok, Machine} = ff_wallet_machine:get(ID), + ff_wallet_machine:wallet(Machine). + +make_identity_creation_claim(PartyID, IdentityID, Provider) -> + Params = #claimmgmt_IdentityParams{ + name = <<"SomeName">>, + party_id = PartyID, + provider = Provider + }, + Mod = ?TEST_IDENTITY_CREATION(IdentityID, Params), + ?cm_identity_modification(1, <<"2026-03-22T06:12:27Z">>, Mod, ?USER_INFO). + +make_wallet_creation_claim(WalletID, IdentityID, CurrencyID) -> + Params = #claimmgmt_NewWalletParams{ + name = <<"SomeWalletName">>, + identity_id = IdentityID, + currency = #domain_CurrencyRef{ + symbolic_code = CurrencyID + } + }, + Mod = ?TEST_WALLET_CREATION(WalletID, Params), + ?cm_wallet_modification(1, <<"2026-03-22T06:12:27Z">>, Mod, ?USER_INFO). diff --git a/apps/ff_server/src/ff_claim_committer_handler.erl b/apps/ff_server/src/ff_claim_committer_handler.erl new file mode 100644 index 0000000..9755cb2 --- /dev/null +++ b/apps/ff_server/src/ff_claim_committer_handler.erl @@ -0,0 +1,32 @@ +-module(ff_claim_committer_handler). + +-include_lib("damsel/include/dmsl_claimmgmt_thrift.hrl"). + +-behaviour(ff_woody_wrapper). + +-export([handle_function/3]). + +-spec handle_function(woody:func(), woody:args(), woody:options()) -> {ok, woody:result()} | no_return(). +handle_function(Func, Args, Opts) -> + scoper:scope( + claims, + #{}, + fun() -> + handle_function_(Func, Args, Opts) + end + ). + +handle_function_('Accept', {PartyID, #claimmgmt_Claim{changeset = Changeset}}, _Opts) -> + ok = scoper:add_meta(#{party_id => PartyID}), + Modifications = ff_claim_committer:filter_ff_modifications(Changeset), + ok = ff_claim_committer:assert_modifications_applicable(Modifications), + {ok, ok}; +handle_function_('Commit', {PartyID, Claim}, _Opts) -> + #claimmgmt_Claim{ + id = ID, + changeset = Changeset + } = Claim, + ok = scoper:add_meta(#{party_id => PartyID, claim_id => ID}), + Modifications = ff_claim_committer:filter_ff_modifications(Changeset), + ff_claim_committer:apply_modifications(Modifications), + {ok, ok}. diff --git a/apps/ff_server/src/ff_server.app.src b/apps/ff_server/src/ff_server.app.src index e4b5aba..e348e43 100644 --- a/apps/ff_server/src/ff_server.app.src +++ b/apps/ff_server/src/ff_server.app.src @@ -14,7 +14,8 @@ fistful, ff_transfer, w2w, - thrift + thrift, + ff_claim ]}, {env, []}, {modules, []}, diff --git a/apps/ff_server/src/ff_server.erl b/apps/ff_server/src/ff_server.erl index 090b35b..2f9e447 100644 --- a/apps/ff_server/src/ff_server.erl +++ b/apps/ff_server/src/ff_server.erl @@ -96,7 +96,8 @@ init([]) -> {withdrawal_repairer, ff_withdrawal_repair}, {deposit_repairer, ff_deposit_repair}, {w2w_transfer_management, ff_w2w_transfer_handler}, - {w2w_transfer_repairer, ff_w2w_transfer_repair} + {w2w_transfer_repairer, ff_w2w_transfer_repair}, + {ff_claim_committer, ff_claim_committer_handler} ] ++ get_eventsink_handlers(), WoodyHandlers = [get_handler(Service, Handler, WrapperOpts) || {Service, Handler} <- Services], diff --git a/apps/ff_server/src/ff_services.erl b/apps/ff_server/src/ff_services.erl index dc56598..c0e3abf 100644 --- a/apps/ff_server/src/ff_services.erl +++ b/apps/ff_server/src/ff_services.erl @@ -60,7 +60,9 @@ get_service(w2w_transfer_event_sink) -> get_service(w2w_transfer_repairer) -> {fistful_w2w_transfer_thrift, 'Repairer'}; get_service(w2w_transfer_management) -> - {fistful_w2w_transfer_thrift, 'Management'}. + {fistful_w2w_transfer_thrift, 'Management'}; +get_service(ff_claim_committer) -> + {dmsl_claimmgmt_thrift, 'ClaimCommitter'}. -spec get_service_spec(service_name()) -> service_spec(). get_service_spec(Name) -> @@ -112,4 +114,6 @@ get_service_path(w2w_transfer_event_sink) -> get_service_path(w2w_transfer_repairer) -> "/v1/repair/w2w_transfer"; get_service_path(w2w_transfer_management) -> - "/v1/w2w_transfer". + "/v1/w2w_transfer"; +get_service_path(ff_claim_committer) -> + "/v1/claim_committer". diff --git a/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl b/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl index be4b5fa..f419c87 100644 --- a/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl +++ b/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl @@ -8,6 +8,8 @@ -export([marshal/2]). -export([unmarshal/2]). +-export([marshal_msgpack/1]). +-export([unmarshal_msgpack/1]). -type type_name() :: atom() | {list, atom()}. -type codec() :: module(). @@ -18,6 +20,19 @@ -type decoded_value() :: decoded_value(any()). -type decoded_value(T) :: T. +%% as stolen from `machinery_msgpack` +-type md() :: + nil + | boolean() + | integer() + | float() + %% string + | binary() + %% binary + | {binary, binary()} + | [md()] + | #{md() => md()}. + -export_type([codec/0]). -export_type([type_name/0]). -export_type([encoded_value/0]). @@ -377,6 +392,7 @@ maybe_unmarshal(_Type, undefined) -> maybe_unmarshal(Type, Value) -> unmarshal(Type, Value). +-spec marshal_msgpack(md()) -> tuple(). marshal_msgpack(nil) -> {nl, #msgpack_Nil{}}; marshal_msgpack(V) when is_boolean(V) -> @@ -395,6 +411,7 @@ marshal_msgpack(V) when is_list(V) -> marshal_msgpack(V) when is_map(V) -> {obj, maps:fold(fun(Key, Value, Map) -> Map#{marshal_msgpack(Key) => marshal_msgpack(Value)} end, #{}, V)}. +-spec unmarshal_msgpack(tuple()) -> md(). unmarshal_msgpack({nl, #msgpack_Nil{}}) -> nil; unmarshal_msgpack({b, V}) when is_boolean(V) -> diff --git a/apps/fistful/src/ff_account.erl b/apps/fistful/src/ff_account.erl index dba57c8..358b147 100644 --- a/apps/fistful/src/ff_account.erl +++ b/apps/fistful/src/ff_account.erl @@ -51,6 +51,7 @@ -export([create/3]). -export([is_accessible/1]). +-export([check_account_creation/3]). -export([apply_event/2]). @@ -89,21 +90,8 @@ accounter_account_id(#{accounter_account_id := AccounterID}) -> -spec create(id(), identity(), currency()) -> {ok, [event()]} | {error, create_error()}. create(ID, Identity, Currency) -> do(fun() -> - PartyID = ff_identity:party(Identity), - accessible = unwrap(party, ff_party:is_accessible(PartyID)), - TermVarset = #{ - wallet_id => ID, - currency => ff_currency:to_domain_ref(Currency) - }, - {ok, PartyRevision} = ff_party:get_revision(PartyID), - DomainRevision = ff_domain_config:head(), - Terms = ff_identity:get_terms(Identity, #{ - party_revision => PartyRevision, - domain_revision => DomainRevision, - varset => TermVarset - }), + unwrap(check_account_creation(ID, Identity, Currency)), CurrencyID = ff_currency:id(Currency), - valid = unwrap(terms, ff_party:validate_account_creation(Terms, CurrencyID)), CurrencyCode = ff_currency:symcode(Currency), Description = ff_string:join($/, [<<"ff/account">>, ID]), {ok, AccounterID} = ff_accounting:create_account(CurrencyCode, Description), @@ -126,6 +114,28 @@ is_accessible(Account) -> accessible = unwrap(ff_identity:is_accessible(Identity)) end). +-spec check_account_creation(id(), identity(), currency()) -> + {ok, valid} + | {error, create_error()}. +check_account_creation(ID, Identity, Currency) -> + do(fun() -> + DomainRevision = ff_domain_config:head(), + PartyID = ff_identity:party(Identity), + accessible = unwrap(party, ff_party:is_accessible(PartyID)), + TermVarset = #{ + wallet_id => ID, + currency => ff_currency:to_domain_ref(Currency) + }, + {ok, PartyRevision} = ff_party:get_revision(PartyID), + Terms = ff_identity:get_terms(Identity, #{ + party_revision => PartyRevision, + domain_revision => DomainRevision, + varset => TermVarset + }), + CurrencyID = ff_currency:id(Currency), + valid = unwrap(terms, ff_party:validate_account_creation(Terms, CurrencyID)) + end). + get_identity(Account) -> {ok, V} = ff_identity_machine:get(identity(Account)), ff_identity_machine:identity(V). diff --git a/apps/fistful/src/ff_identity.erl b/apps/fistful/src/ff_identity.erl index 48bfa24..0f4d654 100644 --- a/apps/fistful/src/ff_identity.erl +++ b/apps/fistful/src/ff_identity.erl @@ -71,11 +71,20 @@ metadata => metadata() }. +-type check_params() :: #{ + party := ff_party:id(), + provider := ff_provider:id() +}. + -type create_error() :: {provider, notfound} | {party, notfound | ff_party:inaccessibility()} | invalid. +-type check_error() :: + {provider, notfound} + | {party, notfound | ff_party:inaccessibility()}. + -type get_terms_params() :: #{ party_revision => ff_party:revision(), domain_revision => ff_domain_config:revision(), @@ -108,6 +117,7 @@ -export([get_withdrawal_methods/1]). -export([get_withdrawal_methods/2]). -export([get_terms/2]). +-export([check_identity_creation/1]). -export([apply_event/2]). @@ -178,8 +188,7 @@ set_blocking(Identity) -> | {error, create_error()}. create(Params = #{id := ID, name := Name, party := Party, provider := ProviderID}) -> do(fun() -> - accessible = unwrap(party, ff_party:is_accessible(Party)), - Provider = unwrap(provider, ff_provider:get(ProviderID)), + Provider = unwrap(check_identity_creation(#{party => Party, provider => ProviderID})), Contract = unwrap( ff_party:create_contract(Party, #{ payinst => ff_provider:payinst(Provider), @@ -203,6 +212,16 @@ create(Params = #{id := ID, name := Name, party := Party, provider := ProviderID ] end). +-spec check_identity_creation(check_params()) -> + {ok, ff_provider:provider()} + | {error, check_error()}. + +check_identity_creation(#{party := Party, provider := ProviderID}) -> + do(fun() -> + accessible = unwrap(party, ff_party:is_accessible(Party)), + unwrap(provider, ff_provider:get(ProviderID)) + end). + -spec get_withdrawal_methods(identity_state()) -> ordsets:ordset(ff_party:method_ref()). get_withdrawal_methods(Identity) -> diff --git a/apps/fistful/src/ff_wallet.erl b/apps/fistful/src/ff_wallet.erl index f9ff737..3a5f76d 100644 --- a/apps/fistful/src/ff_wallet.erl +++ b/apps/fistful/src/ff_wallet.erl @@ -41,11 +41,20 @@ metadata => metadata() }. +-type check_params() :: #{ + identity := ff_identity_machine:id(), + currency := ff_currency:id() +}. + -type create_error() :: {identity, notfound} | {currency, notfound} | ff_account:create_error(). +-type check_error() :: + {identity, notfound} + | {currency, notfound}. + -export_type([id/0]). -export_type([wallet/0]). -export_type([wallet_state/0]). @@ -72,6 +81,7 @@ -export([is_accessible/1]). -export([close/1]). -export([get_account_balance/1]). +-export([check_creation/1]). -export([apply_event/2]). @@ -133,11 +143,9 @@ metadata(Wallet) -> -spec create(params()) -> {ok, [event()]} | {error, create_error()}. -create(Params = #{id := ID, identity := IdentityID, name := Name, currency := CurrencyID}) -> +create(Params = #{id := ID, name := Name}) -> do(fun() -> - IdentityMachine = unwrap(identity, ff_identity_machine:get(IdentityID)), - Identity = ff_identity_machine:identity(IdentityMachine), - Currency = unwrap(currency, ff_currency:get(CurrencyID)), + {Identity, Currency} = unwrap(check_creation(maps:with([identity, currency], Params))), Wallet = genlib_map:compact(#{ version => ?ACTUAL_FORMAT_VERSION, name => Name, @@ -171,6 +179,18 @@ close(Wallet) -> [] end). +-spec check_creation(check_params()) -> + {ok, {ff_identity:identity_state(), ff_currency:currency()}} + | {error, check_error()}. + +check_creation(#{identity := IdentityID, currency := CurrencyID}) -> + do(fun() -> + IdentityMachine = unwrap(identity, ff_identity_machine:get(IdentityID)), + Identity = ff_identity_machine:identity(IdentityMachine), + Currency = unwrap(currency, ff_currency:get(CurrencyID)), + {Identity, Currency} + end). + %% -spec apply_event(event(), undefined | wallet_state()) -> wallet_state(). diff --git a/rebar.config b/rebar.config index 1f54cf1..944be9e 100644 --- a/rebar.config +++ b/rebar.config @@ -63,6 +63,7 @@ ]}. {project_app_dirs, [ + "apps/ff_claim", "apps/ff_core", "apps/ff_server", "apps/ff_transfer",