diff --git a/.env b/.env index a689ddfc..b2116f2e 100644 --- a/.env +++ b/.env @@ -1,4 +1,3 @@ -SERVICE_NAME=hellgate OTP_VERSION=28.5.0 REBAR_VERSION=3.26 THRIFT_VERSION=0.14.2.3 diff --git a/.github/workflows/erlang-checks.yaml b/.github/workflows/erlang-checks.yaml new file mode 100644 index 00000000..37614ec9 --- /dev/null +++ b/.github/workflows/erlang-checks.yaml @@ -0,0 +1,41 @@ +name: Erlang CI Checks + +on: + push: + branches: + - "master" + - "epic/**" + pull_request: + branches: ["**"] + +jobs: + setup: + name: Load .env + runs-on: ubuntu-latest + outputs: + otp-version: ${{ steps.otp-version.outputs.version }} + rebar-version: ${{ steps.rebar-version.outputs.version }} + thrift-version: ${{ steps.thrift-version.outputs.version }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - run: grep -v '^#' .env >> $GITHUB_ENV + - id: otp-version + run: echo "::set-output name=version::$OTP_VERSION" + - id: rebar-version + run: echo "::set-output name=version::$REBAR_VERSION" + - id: thrift-version + run: echo "::set-output name=version::$THRIFT_VERSION" + + run: + name: Run checks + needs: setup + uses: valitydev/erlang-workflows/.github/workflows/erlang-parallel-build.yml@v1 + with: + otp-version: ${{ needs.setup.outputs.otp-version }} + rebar-version: ${{ needs.setup.outputs.rebar-version }} + use-thrift: true + thrift-version: ${{ needs.setup.outputs.thrift-version }} + run-ct-with-compose: true + cache-version: v4 + upload-coverage: false diff --git a/Makefile b/Makefile index 86242248..cf423fac 100644 --- a/Makefile +++ b/Makefile @@ -67,8 +67,8 @@ wdeps-%: dev-image # Database tasks ifeq (db,$(firstword $(MAKECMDGOALS))) - DATABASE_NAME := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) - $(eval $(DATABASE_NAME):;@:) + DATABASE_NAME := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) + $(eval $(DATABASE_NAME):;@:) endif db: diff --git a/README.md b/README.md index 7c064550..417bafa5 100644 --- a/README.md +++ b/README.md @@ -99,3 +99,25 @@ $ make wdeps-test ## Служебные лимиты Нужно уметь _ограничивать_ максимальное _ожидаемое_ количество тех или иных объектов, превышение которого может негативно влиять на качество обслуживания системы. Например, мы можем считать количество _выводов_ одним участником неограниченным, однако при этом неограниченное количество созданных _личностей_ мы совершенно не ожидаем. В этом случае возможно будет разумно ограничить их количество сверху труднодостижимой для подавляющего большинства планкой, например, в 1000 объектов. В идеале подобное должно быть точечно конфигурируемым. + +# Party Management + +Managing parties involved in payment processing. + +## Building + +We widelly use Thrift to define RPC protocols. +So it needs to have [our Thrift compiler](https://github.com/rbkmoney/thrift) in PATH to build this service. +The recommended way to achieve this is by using our [build image](https://github.com/rbkmoney/image-build-erlang). + +# Party management client + +Клиент для сервиса PartyManagement. Спецификацию сервиса можно найти в rbkmoney/damsel в proto/payment_processing.thrift. + +## API + +Низкоуровневый thrift интерфес предоставляется модулем `party_client_thrift`. Его функции принимают и возвращают thrift объекты. + +Более erlang-like интерфес будет предоставляться модулем `party_client`. Его функции должны принимать и возвращать не thrift объекты, а обычные map, абстрагируя пользователей библиотеки от транспортного протокола. На данный момент этот интерфейс не реализован. + +Большая часть функций библиотеки ожидает в аргументах получить клиент и текущий контекст. Клиент представляет собой объект, с параметрами для запуска служебных процессов и обращения к ним, ожидается что он будет создан единожды. Контекст описывает текущее окружение, хранит информацию о пользователе, woody context и т.д. diff --git a/apps/op_context/rebar.config b/apps/op_context/rebar.config deleted file mode 100644 index 1031f8fc..00000000 --- a/apps/op_context/rebar.config +++ /dev/null @@ -1,13 +0,0 @@ -{src_dirs, ["src"]}. - -{erl_opts, [ - debug_info, - warnings_as_errors, - warn_missing_spec -]}. - -{deps, [ - {gproc, "0.9.0"}, - {woody, {git, "https://github.com/valitydev/woody_erlang.git", {tag, "v1.1.2"}}}, - {party_client, {git, "https://github.com/valitydev/party-client-erlang.git", {tag, "v2.0.1"}}} -]}. diff --git a/apps/party_client/src/party_client.app.src b/apps/party_client/src/party_client.app.src new file mode 100644 index 00000000..b4e850ab --- /dev/null +++ b/apps/party_client/src/party_client.app.src @@ -0,0 +1,18 @@ +{application, party_client, [ + {description, "PartyManagement client"}, + {vsn, "1.0.0"}, + {registered, []}, + {applications, [ + kernel, + stdlib, + genlib, + damsel, + prometheus, + woody + ]}, + {env, [ + {services, #{ + party_management => "http://party-management:8022/v1/processing/partymgmt" + }} + ]} +]}. diff --git a/apps/party_client/src/party_client.erl b/apps/party_client/src/party_client.erl new file mode 100644 index 00000000..eb765435 --- /dev/null +++ b/apps/party_client/src/party_client.erl @@ -0,0 +1,47 @@ +-module(party_client). + +-export([create_client/0]). +-export([create_client/1]). +-export([create_context/0]). +-export([create_context/1]). +-export([child_spec/2]). +-export([start_link/1]). + +%% Client types + +-type context() :: party_client_thrift:context(). +-type context_options() :: party_client_context:options(). + +-type client() :: party_client_thrift:client(). +-type client_options() :: party_client_config:options(). + +-export_type([client/0]). +-export_type([client_options/0]). +-export_type([context/0]). +-export_type([context_options/0]). + +%% Client API + +-spec create_client() -> client(). +create_client() -> + create_client(#{}). + +-spec create_client(client_options()) -> client(). +create_client(Options) -> + party_client_config:create(Options). + +-spec create_context() -> context(). +create_context() -> + create_context(#{}). + +-spec create_context(context_options()) -> context(). +create_context(Options) -> + party_client_context:create(Options). + +-spec child_spec(atom(), client()) -> supervisor:child_spec(). +child_spec(ChildID, Client) -> + party_client_woody:child_spec(ChildID, Client). + +-spec start_link(client()) -> genlib_gen:start_ret(). +start_link(Client) -> + party_client_woody:start_link(Client). diff --git a/apps/party_client/src/party_client_config.erl b/apps/party_client/src/party_client_config.erl new file mode 100644 index 00000000..0fbb8522 --- /dev/null +++ b/apps/party_client/src/party_client_config.erl @@ -0,0 +1,133 @@ +-module(party_client_config). + +-export([create/1]). +-export([get_party_service/1]). +-export([get_cache_mode/1]). +-export([get_aggressive_caching_timeout/1]). +-export([get_woody_options/1]). +-export([get_deadline_timeout/1]). +-export([get_retries/1]). + +-opaque client() :: options(). + +-type options() :: #{ + party_service => woody_service(), + aggressive_caching_timeout => timeout(), + woody_options => map(), + deadline_timeout => timeout(), + retries => retries() +}. + +-type cache_mode() :: disabled | safe | aggressive. +-type retries() :: #{'_' | woody:func() => genlib_retry:strategy()}. + +-export_type([client/0]). +-export_type([options/0]). +-export_type([cache_mode/0]). +-export_type([retries/0]). + +-define(APPLICATION, party_client). +-define(DEFAULT_CACHE_NAME, party_client_default_cache). +-define(DEFAULT_WORKERS_NAME, party_client_default_workers). +-define(DEFAULT_AGGERSSIVE_CACHING_TIMEOUT, 30000). +-define(DEFAULT_CACHE_MODE, safe). +-define(DEFAULT_DEADLINE_TIMEOUT, infinity). + +%% Internal types + +-type config_path() :: atom() | [atom() | [any()]]. +-type woody_service() :: woody:service(). +-type woody_options() :: woody_caching_client:options(). + +%% API + +-spec create(options()) -> client(). +create(Options) -> + Options. + +-spec get_party_service(client()) -> woody:service(). +get_party_service(#{party_service := Service}) -> + Service; +get_party_service(_Client) -> + get_default([woody, party_service], {dmsl_payproc_thrift, 'PartyManagement'}). + +-spec get_cache_mode(client()) -> cache_mode(). +get_cache_mode(#{cache_mode := CacheMode}) -> + CacheMode; +get_cache_mode(_Client) -> + get_default([woody, cache_mode], ?DEFAULT_CACHE_MODE). + +-spec get_aggressive_caching_timeout(client()) -> timeout(). +get_aggressive_caching_timeout(#{aggressive_caching_timeout := Timeout}) -> + Timeout; +get_aggressive_caching_timeout(_Client) -> + get_default([woody, aggressive_caching_time], ?DEFAULT_AGGERSSIVE_CACHING_TIMEOUT). + +-spec get_woody_options(client()) -> woody_options(). +get_woody_options(Client) -> + DefaultOptions = #{ + cache => #{local_name => ?DEFAULT_CACHE_NAME}, + workers_name => ?DEFAULT_WORKERS_NAME, + woody_client => #{ + url => get_default([services, party_management]), + event_handler => woody_event_handler_default, + transport_opts => #{} + } + }, + EnvOptions = merge_nested_maps(DefaultOptions, get_default([woody, options], #{})), + merge_nested_maps(EnvOptions, maps:get(woody_options, Client, #{})). + +-spec get_deadline_timeout(client()) -> timeout(). +get_deadline_timeout(#{deadline_timeout := Timeout}) -> + Timeout; +get_deadline_timeout(_Client) -> + get_default([woody, deadline_timeout], ?DEFAULT_DEADLINE_TIMEOUT). + +-spec get_retries(client()) -> retries(). +get_retries(#{retries := Retries}) -> + Retries; +get_retries(_Client) -> + get_default([woody, retries], #{}). + +%% Internal functions + +-spec get_default(config_path()) -> any(). +get_default(Path) -> + Ref = erlang:make_ref(), + case get_default(Path, Ref) of + Ref -> + erlang:error({badpath, Path}); + Other -> + Other + end. + +-spec get_default(config_path(), any()) -> any(). +get_default(Key, Default) when is_atom(Key) -> + genlib_app:env(?APPLICATION, Key, Default); +get_default([Key | Rest] = Path, Default) when is_list(Path) -> + ConfigItem = get_default(Key, #{}), + get_nested_map(Rest, ConfigItem, Default). + +-spec get_nested_map(Path :: [any()], map(), any()) -> any(). +get_nested_map([Key], Map, Default) -> + maps:get(Key, Map, Default); +get_nested_map([Key | Path], Map, Default) -> + NextMap = maps:get(Key, Map, Default), + get_nested_map(Path, NextMap, Default). + +merge_nested_maps(Map1, Map2) when map_size(Map2) =:= 0 -> + Map1; +merge_nested_maps(Map1, Map2) -> + maps:fold(fun merge_map_item/3, Map1, Map2). + +merge_map_item(K, V, Acc) when is_map(V) -> + NewV = + case maps:is_key(K, Acc) of + true -> + merge_nested_maps(maps:get(K, Acc), V); + false -> + V + end, + Acc#{K => NewV}; +merge_map_item(K, V, Acc) -> + Acc#{K => V}. diff --git a/apps/party_client/src/party_client_context.erl b/apps/party_client/src/party_client_context.erl new file mode 100644 index 00000000..097db4f5 --- /dev/null +++ b/apps/party_client/src/party_client_context.erl @@ -0,0 +1,37 @@ +-module(party_client_context). + +-export([create/1]). +-export([get_woody_context/1]). + +-opaque context() :: #{ + woody_context := woody_context() +}. + +-type options() :: #{ + woody_context => woody_context() +}. + +-export_type([context/0]). +-export_type([options/0]). + +%% Internal types + +-type woody_context() :: woody_context:ctx(). + +%% API + +-spec create(options()) -> context(). +create(Options) -> + ensure_woody_context_exists(Options). + +-spec get_woody_context(context()) -> woody_context(). +get_woody_context(#{woody_context := WoodyContext}) -> + WoodyContext. + +%% Internal functions + +-spec ensure_woody_context_exists(options()) -> options(). +ensure_woody_context_exists(#{woody_context := _WoodyContext} = Options) -> + Options; +ensure_woody_context_exists(Options) -> + Options#{woody_context => woody_context:new()}. diff --git a/apps/party_client/src/party_client_thrift.erl b/apps/party_client/src/party_client_thrift.erl new file mode 100644 index 00000000..38fbe2e1 --- /dev/null +++ b/apps/party_client/src/party_client_thrift.erl @@ -0,0 +1,170 @@ +-module(party_client_thrift). + +-export([compute_provider/5]). +-export([compute_provider_terminal_terms/6]). +-export([compute_globals/4]). +-export([compute_routing_ruleset/5]). +-export([compute_payment_institution/5]). +-export([compute_terms/5]). + +-export([get_account_state/5]). +-export([get_shop_account/5]). +-export([get_wallet_account/5]). + +%% Domain types + +-type party_id() :: dmsl_base_thrift:'ID'(). +-type shop_id() :: dmsl_domain_thrift:'ShopID'(). +-type wallet_id() :: dmsl_domain_thrift:'WalletID'(). +-type account_id() :: dmsl_domain_thrift:'AccountID'(). +-type account_state() :: dmsl_payproc_thrift:'AccountState'(). +-type shop_account() :: dmsl_domain_thrift:'ShopAccount'(). +-type wallet_account() :: dmsl_domain_thrift:'WalletAccount'(). +-type timestamp() :: dmsl_base_thrift:'Timestamp'(). +-type provider_ref() :: dmsl_domain_thrift:'ProviderRef'(). +-type provider() :: dmsl_domain_thrift:'Provider'(). +-type terminal_ref() :: dmsl_domain_thrift:'TerminalRef'(). +-type provision_term_set() :: dmsl_domain_thrift:'ProvisionTermSet'(). +-type globals_ref() :: dmsl_domain_thrift:'GlobalsRef'(). +-type globals() :: dmsl_domain_thrift:'Globals'(). +-type routing_ruleset_ref() :: dmsl_domain_thrift:'RoutingRulesetRef'(). +-type routing_ruleset() :: dmsl_domain_thrift:'RoutingRuleset'(). +-type payment_institution() :: dmsl_domain_thrift:'PaymentInstitution'(). +-type payment_institution_ref() :: dmsl_domain_thrift:'PaymentInstitutionRef'(). +-type term_set() :: dmsl_domain_thrift:'TermSet'(). +-type termset_hierarchy_ref() :: dmsl_domain_thrift:'TermSetHierarchyRef'(). +-type varset() :: dmsl_payproc_thrift:'Varset'(). +-type terms() :: dmsl_domain_thrift:'TermSet'(). +-type domain_revision() :: dmsl_domain_thrift:'DataRevision'(). +-type final_cash_flow() :: dmsl_domain_thrift:'FinalCashFlow'(). + +-export_type([party_id/0]). +-export_type([shop_id/0]). +-export_type([account_id/0]). +-export_type([account_state/0]). +-export_type([shop_account/0]). +-export_type([timestamp/0]). +-export_type([provider_ref/0]). +-export_type([provider/0]). +-export_type([terminal_ref/0]). +-export_type([provision_term_set/0]). +-export_type([globals_ref/0]). +-export_type([globals/0]). +-export_type([routing_ruleset_ref/0]). +-export_type([routing_ruleset/0]). +-export_type([payment_institution_ref/0]). +-export_type([varset/0]). +-export_type([terms/0]). +-export_type([final_cash_flow/0]). + +%% Error types + +-type party_not_found() :: dmsl_payproc_thrift:'PartyNotFound'(). +-type shop_not_found() :: dmsl_payproc_thrift:'ShopNotFound'(). +-type shop_account_not_found() :: dmsl_payproc_thrift:'ShopAccountNotFound'(). +-type wallet_not_found() :: dmsl_payproc_thrift:'ShopNotFound'(). +-type wallet_account_not_found() :: dmsl_payproc_thrift:'WalletAccountNotFound'(). +-type account_not_found() :: dmsl_payproc_thrift:'AccountNotFound'(). +-type payment_institution_not_found() :: dmsl_payproc_thrift:'PaymentInstitutionNotFound'(). +-type termset_hierarchy_not_found() :: dmsl_payproc_thrift:'TermSetHierarchyNotFound'(). +-type provider_not_found() :: dmsl_payproc_thrift:'ProviderNotFound'(). +-type terminal_not_found() :: dmsl_payproc_thrift:'TerminalNotFound'(). +-type provision_term_set_undef() :: dmsl_payproc_thrift:'ProvisionTermSetUndefined'(). +-type globals_not_found() :: dmsl_payproc_thrift:'GlobalsNotFound'(). +-type ruleset_not_found() :: dmsl_payproc_thrift:'RuleSetNotFound'(). + +%% Client types + +-type context() :: party_client_context:context(). +-type client() :: party_client_config:client(). + +-export_type([context/0]). +-export_type([client/0]). + +%% Internal types + +-type error(Error) :: party_not_found() | Error. + +-type result(Success, Error) :: {ok, Success} | {error, error(Error)}. + +%% Party API + +-spec compute_provider(Ref, DomainRevision, Varset, client(), context()) -> result(provider(), Error) when + Ref :: provider_ref(), + DomainRevision :: domain_revision(), + Varset :: varset(), + Error :: provider_not_found(). +compute_provider(Ref, DomainRevision, Varset, Client, Context) -> + call('ComputeProvider', [Ref, DomainRevision, Varset], Client, Context). + +-spec compute_provider_terminal_terms(Ref, TerminalRef, DomainRevision, Varset, client(), context()) -> + result(provision_term_set(), Error) +when + Ref :: provider_ref(), + TerminalRef :: terminal_ref(), + DomainRevision :: domain_revision(), + Varset :: varset(), + Error :: provider_not_found() | terminal_not_found() | provision_term_set_undef(). +compute_provider_terminal_terms(Ref, TerminalRef, DomainRevision, Varset, Client, Context) -> + call('ComputeProviderTerminalTerms', [Ref, TerminalRef, DomainRevision, Varset], Client, Context). + +-spec compute_globals(DomainRevision, Varset, client(), context()) -> result(globals(), Error) when + DomainRevision :: domain_revision(), + Varset :: varset(), + Error :: globals_not_found(). +compute_globals(DomainRevision, Varset, Client, Context) -> + call('ComputeGlobals', [DomainRevision, Varset], Client, Context). + +-spec compute_routing_ruleset(Ref, DomainRevision, Varset, client(), context()) -> result(routing_ruleset(), Error) when + Ref :: routing_ruleset_ref(), + DomainRevision :: domain_revision(), + Varset :: varset(), + Error :: ruleset_not_found(). +compute_routing_ruleset(Ref, DomainRevision, Varset, Client, Context) -> + call('ComputeRoutingRuleset', [Ref, DomainRevision, Varset], Client, Context). + +-spec compute_payment_institution(Ref, DomainRevision, Varset, client(), context()) -> + result(payment_institution(), Error) +when + Ref :: payment_institution_ref(), + DomainRevision :: domain_revision(), + Varset :: varset(), + Error :: payment_institution_not_found(). +compute_payment_institution(Ref, DomainRevision, Varset, Client, Context) -> + call('ComputePaymentInstitution', [Ref, DomainRevision, Varset], Client, Context). + +-spec compute_terms(Ref, DomainRevision, Varset, client(), context()) -> + result(term_set(), Error) +when + Ref :: termset_hierarchy_ref(), + DomainRevision :: domain_revision(), + Varset :: varset(), + Error :: termset_hierarchy_not_found(). +compute_terms(Ref, DomainRevision, Varset, Client, Context) -> + call('ComputeTerms', [Ref, DomainRevision, Varset], Client, Context). + +-spec get_account_state(party_id(), account_id(), domain_revision(), client(), context()) -> + result(account_state(), Error) +when + Error :: account_not_found(). +get_account_state(PartyID, AccountID, DomainRevision, Client, Context) -> + call('GetAccountState', [PartyID, AccountID, DomainRevision], Client, Context). + +-spec get_shop_account(party_id(), shop_id(), domain_revision(), client(), context()) -> + result(shop_account(), Error) +when + Error :: shop_not_found() | shop_account_not_found(). +get_shop_account(PartyID, ShopID, DomainRevision, Client, Context) -> + call('GetShopAccount', [PartyID, ShopID, DomainRevision], Client, Context). + +-spec get_wallet_account(party_id(), wallet_id(), domain_revision(), client(), context()) -> + result(wallet_account(), Error) +when + Error :: wallet_not_found() | wallet_account_not_found(). +get_wallet_account(PartyID, WalletID, DomainRevision, Client, Context) -> + call('GetWalletAccount', [PartyID, WalletID, DomainRevision], Client, Context). + +%% Internal functions + +call(Function, Args, Client, Context) -> + party_client_woody:call(Function, erlang:list_to_tuple(Args), Client, Context). diff --git a/apps/party_client/src/party_client_woody.erl b/apps/party_client/src/party_client_woody.erl new file mode 100644 index 00000000..9986b44a --- /dev/null +++ b/apps/party_client/src/party_client_woody.erl @@ -0,0 +1,144 @@ +-module(party_client_woody). + +-export([child_spec/2]). +-export([start_link/1]). + +-export([call/4]). + +%% Internal types + +-type client() :: party_client_config:client(). +-type context() :: party_client_context:context(). +-type business_error() :: any(). + +%% API + +-spec child_spec(atom(), client()) -> supervisor:child_spec(). +child_spec(ChildID, Client) -> + WoodyOptions = party_client_config:get_woody_options(Client), + woody_caching_client:child_spec(ChildID, WoodyOptions). + +-spec start_link(client()) -> genlib_gen:start_ret(). +start_link(Client) -> + WoodyOptions = party_client_config:get_woody_options(Client), + woody_caching_client:start_link(WoodyOptions). + +-spec call(atom(), tuple(), client(), context()) -> ok | {ok, any()} | {error, business_error()} | no_return(). +call(Function, Args, Client, Context) -> + Service = party_client_config:get_party_service(Client), + CacheControl = get_cache_control(Function, Client), + WoodyOptions = party_client_config:get_woody_options(Client), + WoodyContext0 = party_client_context:get_woody_context(Context), + WoodyContext = ensure_deadline(WoodyContext0, Client), + Retry = get_function_retry(Function, Client), + Request = {Service, Function, Args}, + call(Request, CacheControl, WoodyOptions, WoodyContext, Retry). + +call(Request, CacheControl, WoodyOptions, WoodyContext, Retry) -> + try + case + woody_caching_client:call( + Request, + CacheControl, + WoodyOptions, + WoodyContext + ) + of + {exception, Exception} -> + {error, Exception}; + {ok, ok} -> + ok; + {ok, _Other} = Result -> + Result + end + catch + error:{woody_error, {_Source, Class, _Details}} = Error when + Class =:= resource_unavailable orelse Class =:= result_unknown + -> + NextRetry = apply_retry_strategy(Retry, Error, WoodyContext), + call(Request, CacheControl, WoodyOptions, WoodyContext, NextRetry) + end. + +%% Internal functions + +-spec get_cache_control(atom(), client()) -> woody_caching_client:cache_control(). +get_cache_control(Function, Client) -> + case party_client_config:get_cache_mode(Client) of + safe -> + get_safe_cache_control(Function); + aggressive -> + Timeout = party_client_config:get_aggressive_caching_timeout(Client), + get_aggressive_cache_control(Function, Timeout); + disabled -> + no_cache + end. + +-spec get_safe_cache_control(atom()) -> woody_caching_client:cache_control(). +get_safe_cache_control('Checkout') -> + cache; +get_safe_cache_control(_Other) -> + no_cache. + +-spec get_aggressive_cache_control(atom(), timeout()) -> woody_caching_client:cache_control(). +get_aggressive_cache_control(Function, Timeout) -> + case get_aggressive_function_cache_mode(Function) of + cache -> + cache; + temporary -> + {cache_for, Timeout}; + no_cache -> + no_cache + end. + +get_aggressive_function_cache_mode('Checkout') -> cache; +get_aggressive_function_cache_mode('Get') -> temporary; +get_aggressive_function_cache_mode('GetRevision') -> temporary; +get_aggressive_function_cache_mode('GetContract') -> temporary; +get_aggressive_function_cache_mode('ComputeContractTerms') -> temporary; +get_aggressive_function_cache_mode('GetShop') -> temporary; +get_aggressive_function_cache_mode('GetClaim') -> temporary; +get_aggressive_function_cache_mode('GetClaims') -> temporary; +get_aggressive_function_cache_mode('GetEvents') -> temporary; +get_aggressive_function_cache_mode('GetShopAccount') -> temporary; +get_aggressive_function_cache_mode('ComputePaymentInstitutionTerms') -> temporary; +get_aggressive_function_cache_mode(_Other) -> no_cache. + +% Retry + +ensure_deadline(WoodyContext, Client) -> + case woody_context:get_deadline(WoodyContext) of + undefined -> + Deadline = get_deadline(Client), + woody_context:set_deadline(Deadline, WoodyContext); + _AlreadySet -> + WoodyContext + end. + +get_deadline(Client) -> + woody_deadline:from_timeout(party_client_config:get_deadline_timeout(Client)). + +get_function_retry(Function, Client) -> + FunctionReties = party_client_config:get_retries(Client), + DefaultRetry = maps:get('_', FunctionReties, finish), + maps:get(Function, FunctionReties, DefaultRetry). + +apply_retry_strategy(Retry, Error, Context) -> + apply_retry_step(genlib_retry:next_step(Retry), woody_context:get_deadline(Context), Error). + +apply_retry_step(finish, _, Error) -> + erlang:error(Error); +apply_retry_step({wait, Timeout, Retry}, undefined, _) -> + ok = timer:sleep(Timeout), + Retry; +apply_retry_step({wait, Timeout, Retry}, Deadline0, Error) -> + Deadline1 = woody_deadline:from_unixtime_ms( + woody_deadline:to_unixtime_ms(Deadline0) - Timeout + ), + case woody_deadline:is_reached(Deadline1) of + true -> + % no more time for retries + erlang:error(Error); + false -> + ok = timer:sleep(Timeout), + Retry + end. diff --git a/apps/party_client/test/party_client_base_pm_tests_SUITE.erl b/apps/party_client/test/party_client_base_pm_tests_SUITE.erl new file mode 100644 index 00000000..eac94a85 --- /dev/null +++ b/apps/party_client/test/party_client_base_pm_tests_SUITE.erl @@ -0,0 +1,359 @@ +-module(party_client_base_pm_tests_SUITE). + +-include_lib("stdlib/include/assert.hrl"). +-include("party_domain_fixtures.hrl"). + +-include_lib("damsel/include/dmsl_payproc_thrift.hrl"). +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). + +-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]). + +-export([compute_provider_ok/1]). +-export([compute_provider_not_found/1]). +-export([compute_provider_terminal_terms_ok/1]). +-export([compute_provider_terminal_terms_not_found/1]). +-export([compute_globals_ok/1]). +-export([compute_routing_ruleset_ok/1]). +-export([compute_routing_ruleset_unreducable/1]). +-export([compute_routing_ruleset_not_found/1]). +-export([compute_terms_ok/1]). +-export([compute_terms_hierarchy_not_found/1]). + +%% Internal types + +-type test_entry() :: atom() | {group, atom()}. +-type group() :: {atom(), [Opts :: atom()], [test_entry()]}. +-type config() :: [{atom(), any()}]. + +-define(WRONG_DMT_OBJ_ID, 99999). + +%% CT description + +-spec all() -> [test_entry()]. +all() -> + [ + {group, party_management_api}, + {group, party_management_compute_api} + ]. + +-spec groups() -> [group()]. +groups() -> + [ + {party_management_api, [parallel], [ + %% TODO Add shop, wallet and accounts test + ]}, + {party_management_compute_api, [parallel], [ + compute_provider_ok, + compute_provider_not_found, + compute_provider_terminal_terms_ok, + compute_provider_terminal_terms_not_found, + compute_globals_ok, + compute_routing_ruleset_ok, + compute_routing_ruleset_unreducable, + compute_routing_ruleset_not_found, + compute_terms_ok, + compute_terms_hierarchy_not_found + ]} + ]. + +-spec init_per_suite(config()) -> config(). +init_per_suite(Config) -> + % _ = dbg:tracer(), + % _ = dbg:p(all, c), + % _ = dbg:tpl({'scoper_woody_event_handler', 'handle_event', '_'}, x), + AppConfig = [ + {dmt_client, [ + % milliseconds + {cache_update_interval, 5000}, + {max_cache_size, #{ + elements => 1, + % 2Kb + memory => 2048 + }}, + {service_urls, #{ + 'AuthorManagement' => <<"http://dmt:8022/v1/domain/author">>, + 'Repository' => <<"http://dmt:8022/v1/domain/repository">>, + 'RepositoryClient' => <<"http://dmt:8022/v1/domain/repository_client">> + }} + ]}, + {party_client, []} + ], + Apps = lists:flatten([genlib_app:start_application_with(A, C) || {A, C} <- AppConfig]), + {ok, Revision} = init_domain(), + Client = party_client:create_client(), + {ok, ClientPid} = party_client:start_link(Client), + true = erlang:unlink(ClientPid), + [{apps, Apps}, {client, Client}, {client_pid, ClientPid}, {test_id, genlib:to_binary(Revision)} | Config]. + +-spec end_per_suite(config()) -> ok. +end_per_suite(C) -> + true = erlang:exit(conf(client_pid, C), shutdown), + genlib_app:stop_unload_applications(proplists:get_value(apps, C)). + +-spec init_per_group(atom(), config()) -> config(). +init_per_group(Group, Config) -> + [{test_id, genlib:to_binary(Group)} | Config]. + +-spec end_per_group(atom(), config()) -> ok. +end_per_group(_Group, _Config) -> + ok. + +-spec init_per_testcase(atom(), config()) -> config(). +init_per_testcase(Name, Config) -> + [{test_id, genlib:to_binary(Name)} | Config]. + +-spec end_per_testcase(atom(), config()) -> ok. +end_per_testcase(_Name, _Config) -> + ok. + +%% Tests + +-spec compute_provider_ok(config()) -> any(). +compute_provider_ok(C) -> + {ok, _PartyId, Client, Context} = test_init_info(C), + {ok, DomainRevision} = ensure_latest_version_checked_out(), + Varset = #payproc_Varset{ + currency = ?cur(<<"RUB">>) + }, + CashFlow = make_test_cashflow(), + {ok, #domain_Provider{ + terms = #domain_ProvisionTermSet{ + payments = #domain_PaymentsProvisionTerms{ + cash_flow = {value, [CashFlow]} + }, + recurrent_paytools = #domain_RecurrentPaytoolsProvisionTerms{ + cash_value = {value, ?cash(1000, <<"RUB">>)} + } + } + }} = party_client_thrift:compute_provider(?prv(1), DomainRevision, Varset, Client, Context). + +-spec compute_provider_not_found(config()) -> any(). +compute_provider_not_found(C) -> + {ok, _PartyId, Client, Context} = test_init_info(C), + {ok, DomainRevision} = ensure_latest_version_checked_out(), + {error, #payproc_ProviderNotFound{}} = + party_client_thrift:compute_provider( + ?prv(2), + DomainRevision, + #payproc_Varset{}, + Client, + Context + ). + +-spec compute_provider_terminal_terms_ok(config()) -> any(). +compute_provider_terminal_terms_ok(C) -> + {ok, _PartyId, Client, Context} = test_init_info(C), + {ok, DomainRevision} = ensure_latest_version_checked_out(), + Varset = #payproc_Varset{ + currency = ?cur(<<"RUB">>) + }, + CashFlow = make_test_cashflow(), + PaymentMethods = ?ordset([?pmt_bank_card(visa)]), + {ok, #domain_ProvisionTermSet{ + payments = #domain_PaymentsProvisionTerms{ + cash_flow = {value, [CashFlow]}, + payment_methods = {value, PaymentMethods} + } + }} = party_client_thrift:compute_provider_terminal_terms( + ?prv(1), + ?trm(1), + DomainRevision, + Varset, + Client, + Context + ). + +-spec compute_provider_terminal_terms_not_found(config()) -> any(). +compute_provider_terminal_terms_not_found(C) -> + {ok, _PartyId, Client, Context} = test_init_info(C), + {ok, DomainRevision} = ensure_latest_version_checked_out(), + {error, #payproc_TerminalNotFound{}} = + party_client_thrift:compute_provider_terminal_terms( + ?prv(1), + ?trm(?WRONG_DMT_OBJ_ID), + DomainRevision, + #payproc_Varset{}, + Client, + Context + ), + {error, #payproc_ProviderNotFound{}} = + party_client_thrift:compute_provider_terminal_terms( + ?prv(2), + ?trm(1), + DomainRevision, + #payproc_Varset{}, + Client, + Context + ), + {error, #payproc_ProviderNotFound{}} = + party_client_thrift:compute_provider_terminal_terms( + ?prv(2), + ?trm(?WRONG_DMT_OBJ_ID), + DomainRevision, + #payproc_Varset{}, + Client, + Context + ). + +-spec compute_globals_ok(config()) -> any(). +compute_globals_ok(C) -> + {ok, _PartyId, Client, Context} = test_init_info(C), + {ok, DomainRevision} = ensure_latest_version_checked_out(), + Varset = #payproc_Varset{}, + {ok, #domain_Globals{ + external_account_set = {value, ?eas(1)} + }} = party_client_thrift:compute_globals(DomainRevision, Varset, Client, Context). + +-spec compute_routing_ruleset_ok(config()) -> any(). +compute_routing_ruleset_ok(C) -> + {ok, _PartyId, Client, Context} = test_init_info(C), + {ok, DomainRevision} = ensure_latest_version_checked_out(), + Varset = #payproc_Varset{ + party_ref = #domain_PartyConfigRef{id = <<"67890">>} + }, + {ok, #domain_RoutingRuleset{ + name = <<"Rule#1">>, + decisions = + {candidates, [ + #domain_RoutingCandidate{ + terminal = ?trm(2), + allowed = {constant, true} + }, + #domain_RoutingCandidate{ + terminal = ?trm(3), + allowed = {constant, true} + }, + #domain_RoutingCandidate{ + terminal = ?trm(1), + allowed = {constant, true} + } + ]} + }} = party_client_thrift:compute_routing_ruleset(?ruleset(1), DomainRevision, Varset, Client, Context). + +-spec compute_routing_ruleset_unreducable(config()) -> any(). +compute_routing_ruleset_unreducable(C) -> + {ok, _PartyId, Client, Context} = test_init_info(C), + {ok, DomainRevision} = ensure_latest_version_checked_out(), + Varset = #payproc_Varset{}, + {ok, #domain_RoutingRuleset{ + name = <<"Rule#1">>, + decisions = + {delegates, [ + #domain_RoutingDelegate{ + allowed = + {condition, + {party, #domain_PartyCondition{party_ref = #domain_PartyConfigRef{id = <<"12345">>}}}}, + ruleset = ?ruleset(2) + }, + #domain_RoutingDelegate{ + allowed = + {condition, + {party, #domain_PartyCondition{party_ref = #domain_PartyConfigRef{id = <<"67890">>}}}}, + ruleset = ?ruleset(3) + }, + #domain_RoutingDelegate{ + allowed = {constant, true}, + ruleset = ?ruleset(4) + } + ]} + }} = party_client_thrift:compute_routing_ruleset(?ruleset(1), DomainRevision, Varset, Client, Context). + +-spec compute_routing_ruleset_not_found(config()) -> any(). +compute_routing_ruleset_not_found(C) -> + {ok, _PartyId, Client, Context} = test_init_info(C), + {ok, DomainRevision} = ensure_latest_version_checked_out(), + {error, #payproc_RuleSetNotFound{}} = + (catch party_client_thrift:compute_routing_ruleset( + ?ruleset(5), + DomainRevision, + #payproc_Varset{}, + Client, + Context + )). + +-spec compute_terms_hierarchy_not_found(config()) -> any(). +compute_terms_hierarchy_not_found(C) -> + {ok, _PartyId, Client, Context} = test_init_info(C), + {ok, DomainRevision} = ensure_latest_version_checked_out(), + ?assertMatch( + {error, #payproc_TermSetHierarchyNotFound{}}, + party_client_thrift:compute_terms(?trms(42), DomainRevision, #payproc_Varset{}, Client, Context) + ). + +-spec compute_terms_ok(config()) -> any(). +compute_terms_ok(C) -> + {ok, _PartyId, Client, Context} = test_init_info(C), + {ok, DomainRevision} = ensure_latest_version_checked_out(), + Varset = #payproc_Varset{ + currency = ?cur(<<"RUB">>) + }, + ?assertMatch( + {ok, #domain_TermSet{ + payments = #domain_PaymentsServiceTerms{ + currencies = {value, _}, + categories = {value, _}, + payment_methods = {value, _}, + cash_limit = {value, _} + } + }}, + party_client_thrift:compute_terms(?trms(3), DomainRevision, Varset, Client, Context) + ). + +%% Internal functions + +%% Environment confirators + +-spec init_domain() -> {ok, integer()}. +init_domain() -> + {ok, _} = ensure_latest_version_checked_out(), + ok = party_domain_fixtures:cleanup(), + {ok, _} = ensure_latest_version_checked_out(), + ok = party_domain_fixtures:apply_domain_fixture(), + {ok, _Revision} = ensure_latest_version_checked_out(). + +ensure_latest_version_checked_out() -> + Version = dmt_client:get_latest_version(), + %% NOTE This call updates local cache under with checked out objects of a version + _ = dmt_client:checkout_all(Version), + {ok, Version}. + +%% Config helpers + +-spec get_test_id(config()) -> binary(). +get_test_id(Config) -> + AllId = lists:reverse(proplists:append_values(test_id, Config)), + erlang:iolist_to_binary([[<<".">> | I] || I <- AllId]). + +conf(Key, Config) -> + proplists:get_value(Key, Config). + +%% Domain objects constructors + +create_context() -> + party_client:create_context(). + +test_init_info(C) -> + PartyId = get_test_id(C), + Client = conf(client, C), + Context = create_context(), + {ok, PartyId, Client, Context}. + +-spec make_test_cashflow() -> dmsl_domain_thrift:'CashFlowPosting'(). +make_test_cashflow() -> + ?cfpost( + {system, settlement}, + {provider, settlement}, + {product, + {min_of, + ?ordset([ + ?fixed(10, <<"RUB">>), + ?share(5, 100, operation_amount, round_half_towards_zero) + ])}} + ). diff --git a/apps/party_client/test/party_client_config_tests_SUITE.erl b/apps/party_client/test/party_client_config_tests_SUITE.erl new file mode 100644 index 00000000..a6ed6517 --- /dev/null +++ b/apps/party_client/test/party_client_config_tests_SUITE.erl @@ -0,0 +1,60 @@ +-module(party_client_config_tests_SUITE). + +-export([all/0]). +-export([init_per_suite/1]). +-export([end_per_suite/1]). + +-export([config_merge_test/1]). + +%% Internal types + +-type test_entry() :: atom() | {group, atom()}. +-type config() :: [{atom(), any()}]. + +%% CT description + +-spec all() -> [test_entry()]. +all() -> + [ + config_merge_test + ]. + +-spec init_per_suite(config()) -> config(). +init_per_suite(Config) -> + AppConfig = [ + {party_client, [ + {woody, #{ + options => #{cache => #{local_name => blah}, woody_client => #{protocol_handler_override => my_handler}} + }} + ]} + ], + Apps = lists:flatten([genlib_app:start_application_with(A, C) || {A, C} <- AppConfig]), + [{apps, Apps} | Config]. + +-spec end_per_suite(config()) -> ok. +end_per_suite(C) -> + genlib_app:stop_unload_applications(proplists:get_value(apps, C)). + +%% Tests + +-spec config_merge_test(config()) -> any(). +config_merge_test(_C) -> + Client = party_client:create_client(#{ + woody_options => #{ + cache => #{local_name => party_client_default_cache}, woody_client => #{deadline => undefined} + } + }), + WoodyOptions = party_client_config:get_woody_options(Client), + #{ + cache := #{ + local_name := party_client_default_cache + }, + woody_client := #{ + protocol_handler_override := my_handler, + deadline := undefined, + event_handler := woody_event_handler_default, + transport_opts := #{}, + url := _Urls + }, + workers_name := party_client_default_workers + } = WoodyOptions. diff --git a/apps/party_client/test/party_domain_fixtures.erl b/apps/party_client/test/party_domain_fixtures.erl new file mode 100644 index 00000000..f4bb0629 --- /dev/null +++ b/apps/party_client/test/party_domain_fixtures.erl @@ -0,0 +1,568 @@ +-module(party_domain_fixtures). + +-include("party_domain_fixtures.hrl"). + +-include_lib("damsel/include/dmsl_domain_conf_v2_thrift.hrl"). + +-export([construct_domain_fixture/0]). +-export([apply_domain_fixture/0]). +-export([apply_domain_fixture/1]). +-export([cleanup/0]). + +%% Internal types + +-type name() :: binary(). +-type category() :: dmsl_domain_thrift:'CategoryRef'(). +-type currency() :: dmsl_domain_thrift:'CurrencyRef'(). +-type proxy() :: dmsl_domain_thrift:'ProxyRef'(). +-type inspector() :: dmsl_domain_thrift:'InspectorRef'(). +-type routing_ruleset_ref() :: dmsl_domain_thrift:'RoutingRulesetRef'(). + +-type system_account_set() :: dmsl_domain_thrift:'SystemAccountSetRef'(). +-type external_account_set() :: dmsl_domain_thrift:'ExternalAccountSetRef'(). + +-type business_schedule() :: dmsl_domain_thrift:'BusinessScheduleRef'(). + +%% API + +-spec apply_domain_fixture() -> ok. +apply_domain_fixture() -> + apply_domain_fixture(construct_domain_fixture()). + +-spec apply_domain_fixture([dmsl_domain_thrift:'DomainObject'()]) -> ok. +apply_domain_fixture(Fixture) -> + _NextRevision = dmt_client:insert(Fixture, ensure_stub_author()), + ok. + +-spec cleanup() -> ok. +cleanup() -> + Version = dmt_client:get_latest_version(), + Objects = lists:map( + fun(#domain_conf_v2_VersionedObject{object = Object}) -> Object end, dmt_client:checkout_all(Version) + ), + _NextRevision = dmt_client:remove(Version, Objects, ensure_stub_author()), + ok. + +ensure_stub_author() -> + %% TODO DISCUSS Stubs and fallback authors + ensure_author(~b"unknown", ~b"unknown@local"). + +ensure_author(Name, Email) -> + try + #domain_conf_v2_Author{id = ID} = dmt_client:get_author_by_email(Email), + ID + catch + throw:#domain_conf_v2_AuthorNotFound{} -> + dmt_client:create_author(Name, Email) + end. + +-spec construct_domain_fixture() -> [dmsl_domain_thrift:'DomainObject'()]. +construct_domain_fixture() -> + TestTermSet = #domain_TermSet{ + payments = #domain_PaymentsServiceTerms{ + currencies = {value, ordsets:from_list([?cur(<<"RUB">>)])}, + categories = {value, ordsets:from_list([?cat(1)])} + } + }, + DefaultTermSet = #domain_TermSet{ + payments = #domain_PaymentsServiceTerms{ + currencies = + {value, + ordsets:from_list([ + ?cur(<<"RUB">>), + ?cur(<<"USD">>) + ])}, + categories = + {value, + ordsets:from_list([ + ?cat(2), + ?cat(3) + ])}, + payment_methods = + {value, + ordsets:from_list([ + ?pmt_bank_card(visa) + ])} + } + }, + TermSet = #domain_TermSet{ + payments = #domain_PaymentsServiceTerms{ + cash_limit = + {value, #domain_CashRange{ + lower = {inclusive, #domain_Cash{amount = 1000, currency = ?cur(<<"RUB">>)}}, + upper = {exclusive, #domain_Cash{amount = 4200000, currency = ?cur(<<"RUB">>)}} + }}, + fees = + {value, [ + ?cfpost( + {merchant, settlement}, + {system, settlement}, + ?share(45, 1000, operation_amount) + ) + ]} + }, + wallets = #domain_WalletServiceTerms{ + currencies = {value, ordsets:from_list([?cur(<<"RUB">>)])} + } + }, + Decision1 = + {delegates, [ + #domain_RoutingDelegate{ + allowed = + {condition, {party, #domain_PartyCondition{party_ref = #domain_PartyConfigRef{id = <<"12345">>}}}}, + ruleset = ?ruleset(2) + }, + #domain_RoutingDelegate{ + allowed = + {condition, {party, #domain_PartyCondition{party_ref = #domain_PartyConfigRef{id = <<"67890">>}}}}, + ruleset = ?ruleset(3) + }, + #domain_RoutingDelegate{ + allowed = {constant, true}, + ruleset = ?ruleset(4) + } + ]}, + Decision2 = + {candidates, [ + #domain_RoutingCandidate{ + allowed = {constant, true}, + terminal = ?trm(1) + } + ]}, + Decision3 = + {candidates, [ + #domain_RoutingCandidate{ + allowed = + {condition, {party, #domain_PartyCondition{party_ref = #domain_PartyConfigRef{id = <<"67890">>}}}}, + terminal = ?trm(2) + }, + #domain_RoutingCandidate{ + allowed = {constant, true}, + terminal = ?trm(3) + }, + #domain_RoutingCandidate{ + allowed = {constant, true}, + terminal = ?trm(1) + } + ]}, + Decision4 = + {candidates, [ + #domain_RoutingCandidate{ + allowed = {constant, true}, + terminal = ?trm(3) + } + ]}, + [ + construct_currency(?cur(<<"RUB">>)), + construct_currency(?cur(<<"USD">>)), + + construct_category(?cat(1), <<"Test category">>, test), + construct_category(?cat(2), <<"Generic Store">>, live), + construct_category(?cat(3), <<"Guns & Booze">>, live), + + construct_payment_method(visa, ?pmt_bank_card(visa)), + construct_payment_method(mastercard, ?pmt_bank_card(mastercard)), + construct_payment_method(maestro, ?pmt_bank_card(maestro)), + construct_payment_method(euroset, ?pmt(payment_terminal, #domain_PaymentServiceRef{id = <<"euroset">>})), + + construct_proxy(?prx(1), <<"Dummy proxy">>), + construct_inspector(?insp(1), <<"Dummy Inspector">>, ?prx(1)), + construct_system_account_set(?sas(1)), + construct_system_account_set(?sas(2)), + construct_external_account_set(?eas(1)), + + construct_business_schedule(?bussched(1)), + + construct_routing_ruleset(?ruleset(1), <<"Rule#1">>, Decision1), + construct_routing_ruleset(?ruleset(2), <<"Rule#2">>, Decision2), + construct_routing_ruleset(?ruleset(3), <<"Rule#3">>, Decision3), + construct_routing_ruleset(?ruleset(4), <<"Rule#4">>, Decision4), + + {payment_institution, #domain_PaymentInstitutionObject{ + ref = ?pinst(1), + data = #domain_PaymentInstitution{ + name = <<"Test Inc.">>, + system_account_set = {value, ?sas(1)}, + inspector = {value, ?insp(1)}, + residences = [], + realm = test + } + }}, + + {payment_institution, #domain_PaymentInstitutionObject{ + ref = ?pinst(2), + data = #domain_PaymentInstitution{ + name = <<"Chetky Payments Inc.">>, + system_account_set = {value, ?sas(2)}, + inspector = {value, ?insp(1)}, + residences = [], + realm = live + } + }}, + + {globals, #domain_GlobalsObject{ + ref = #domain_GlobalsRef{}, + data = #domain_Globals{ + external_account_set = {value, ?eas(1)}, + payment_institutions = ?ordset([?pinst(1), ?pinst(2)]) + } + }}, + {term_set_hierarchy, #domain_TermSetHierarchyObject{ + ref = ?trms(1), + data = #domain_TermSetHierarchy{ + parent_terms = undefined, + term_set = TestTermSet + } + }}, + {term_set_hierarchy, #domain_TermSetHierarchyObject{ + ref = ?trms(2), + data = #domain_TermSetHierarchy{ + parent_terms = undefined, + term_set = DefaultTermSet + } + }}, + {term_set_hierarchy, #domain_TermSetHierarchyObject{ + ref = ?trms(3), + data = #domain_TermSetHierarchy{ + parent_terms = ?trms(2), + term_set = TermSet + } + }}, + {term_set_hierarchy, #domain_TermSetHierarchyObject{ + ref = ?trms(4), + data = #domain_TermSetHierarchy{ + parent_terms = ?trms(3), + term_set = + #domain_TermSet{ + payments = #domain_PaymentsServiceTerms{ + currencies = + {value, + ordsets:from_list([ + ?cur(<<"RUB">>) + ])}, + categories = + {value, + ordsets:from_list([ + ?cat(2) + ])}, + payment_methods = + {value, + ordsets:from_list([ + ?pmt_bank_card(visa) + ])} + } + } + } + }}, + {provider, #domain_ProviderObject{ + ref = ?prv(1), + data = #domain_Provider{ + name = <<"Brovider">>, + realm = test, + description = <<"A provider but bro">>, + proxy = #domain_Proxy{ref = ?prx(1), additional = #{}}, + terms = #domain_ProvisionTermSet{ + payments = #domain_PaymentsProvisionTerms{ + currencies = {value, ?ordset([?cur(<<"RUB">>)])}, + categories = {value, ?ordset([?cat(1)])}, + payment_methods = + {value, + ?ordset([ + ?pmt_bank_card(visa), + ?pmt_bank_card(mastercard) + ])}, + cash_limit = + {value, + ?cashrng( + {inclusive, ?cash(1000, <<"RUB">>)}, + {exclusive, ?cash(1000000000, <<"RUB">>)} + )}, + cash_flow = + {decisions, [ + #domain_CashFlowDecision{ + if_ = {condition, {currency_is, ?cur(<<"RUB">>)}}, + then_ = + {value, [ + ?cfpost( + {system, settlement}, + {provider, settlement}, + {product, + {min_of, + ?ordset([ + ?fixed(10, <<"RUB">>), + ?share( + 5, + 100, + operation_amount, + round_half_towards_zero + ) + ])}} + ) + ]} + }, + #domain_CashFlowDecision{ + if_ = {condition, {currency_is, ?cur(<<"USD">>)}}, + then_ = + {value, [ + ?cfpost( + {system, settlement}, + {provider, settlement}, + {product, + {min_of, + ?ordset([ + ?fixed(10, <<"USD">>), + ?share( + 5, + 100, + operation_amount, + round_half_towards_zero + ) + ])}} + ) + ]} + } + ]} + }, + recurrent_paytools = #domain_RecurrentPaytoolsProvisionTerms{ + categories = {value, ?ordset([?cat(1)])}, + payment_methods = + {value, + ?ordset([ + ?pmt_bank_card(visa), + ?pmt_bank_card(mastercard) + ])}, + cash_value = + {decisions, [ + #domain_CashValueDecision{ + if_ = {condition, {currency_is, ?cur(<<"RUB">>)}}, + then_ = {value, ?cash(1000, <<"RUB">>)} + }, + #domain_CashValueDecision{ + if_ = {condition, {currency_is, ?cur(<<"USD">>)}}, + then_ = {value, ?cash(1000, <<"USD">>)} + } + ]} + } + } + } + }}, + + {terminal, #domain_TerminalObject{ + ref = ?trm(1), + data = #domain_Terminal{ + name = <<"Brominal 1">>, + description = <<"Brominal 1">>, + terms = #domain_ProvisionTermSet{ + payments = #domain_PaymentsProvisionTerms{ + payment_methods = + {value, + ?ordset([ + ?pmt_bank_card(visa) + ])} + } + } + } + }}, + {terminal, #domain_TerminalObject{ + ref = ?trm(2), + data = #domain_Terminal{ + name = <<"Brominal 2">>, + description = <<"Brominal 2">>, + terms = #domain_ProvisionTermSet{ + payments = #domain_PaymentsProvisionTerms{ + payment_methods = + {value, + ?ordset([ + ?pmt_bank_card(visa) + ])} + } + } + } + }}, + {terminal, #domain_TerminalObject{ + ref = ?trm(3), + data = #domain_Terminal{ + name = <<"Brominal 3">>, + description = <<"Brominal 3">>, + terms = #domain_ProvisionTermSet{ + payments = #domain_PaymentsProvisionTerms{ + payment_methods = + {value, + ?ordset([ + ?pmt_bank_card(visa) + ])} + } + } + } + }}, + {party_config, #domain_PartyConfigObject{ + ref = #domain_PartyConfigRef{id = <<"12345">>}, + data = #domain_PartyConfig{ + name = <<"12345">>, + block = {unblocked, #domain_Unblocked{reason = ~"whatever reason", since = ~"1970-01-01T00:00:00Z"}}, + suspension = {active, #domain_Active{since = ~"1970-01-01T00:00:00Z"}}, + contact_info = #domain_PartyContactInfo{registration_email = <<"party@example.com">>} + } + }}, + {party_config, #domain_PartyConfigObject{ + ref = #domain_PartyConfigRef{id = <<"67890">>}, + data = #domain_PartyConfig{ + name = <<"67890">>, + block = {unblocked, #domain_Unblocked{reason = ~"whatever reason", since = ~"1970-01-01T00:00:00Z"}}, + suspension = {active, #domain_Active{since = ~"1970-01-01T00:00:00Z"}}, + contact_info = #domain_PartyContactInfo{registration_email = <<"party@example.com">>} + } + }} + ]. + +%% Internal functions + +-spec construct_currency(currency()) -> {currency, dmsl_domain_thrift:'CurrencyObject'()}. +construct_currency(Ref) -> + construct_currency(Ref, 2). + +-spec construct_currency(currency(), Exponent :: pos_integer()) -> {currency, dmsl_domain_thrift:'CurrencyObject'()}. +construct_currency(?cur(SymbolicCode) = Ref, Exponent) -> + {currency, #domain_CurrencyObject{ + ref = Ref, + data = #domain_Currency{ + name = SymbolicCode, + numeric_code = 666, + symbolic_code = SymbolicCode, + exponent = Exponent + } + }}. + +-spec construct_category(category(), name(), test | live) -> {category, dmsl_domain_thrift:'CategoryObject'()}. +construct_category(Ref, Name, Type) -> + {category, #domain_CategoryObject{ + ref = Ref, + data = #domain_Category{ + name = Name, + description = Name, + type = Type + } + }}. + +-spec construct_payment_method(atom(), dmsl_domain_thrift:'PaymentMethodRef'()) -> + {payment_method, dmsl_domain_thrift:'PaymentMethodObject'()}. +construct_payment_method(Name, ?pmt(_, _) = Ref) when is_atom(Name) -> + Def = erlang:atom_to_binary(Name, unicode), + {payment_method, #domain_PaymentMethodObject{ + ref = Ref, + data = #domain_PaymentMethodDefinition{ + name = Def, + description = Def + } + }}. + +-spec construct_proxy(proxy(), name()) -> {proxy, dmsl_domain_thrift:'ProxyObject'()}. +construct_proxy(Ref, Name) -> + construct_proxy(Ref, Name, #{}). + +-spec construct_proxy(proxy(), name(), Opts :: map()) -> {proxy, dmsl_domain_thrift:'ProxyObject'()}. +construct_proxy(Ref, Name, Opts) -> + {proxy, #domain_ProxyObject{ + ref = Ref, + data = #domain_ProxyDefinition{ + name = Name, + description = Name, + url = <<>>, + options = Opts + } + }}. + +-spec construct_inspector(inspector(), name(), proxy()) -> {inspector, dmsl_domain_thrift:'InspectorObject'()}. +construct_inspector(Ref, Name, ProxyRef) -> + construct_inspector(Ref, Name, ProxyRef, #{}). + +-spec construct_inspector(inspector(), name(), proxy(), Additional :: map()) -> + {inspector, dmsl_domain_thrift:'InspectorObject'()}. +construct_inspector(Ref, Name, ProxyRef, Additional) -> + {inspector, #domain_InspectorObject{ + ref = Ref, + data = #domain_Inspector{ + name = Name, + description = Name, + proxy = #domain_Proxy{ + ref = ProxyRef, + additional = Additional + } + } + }}. + +-spec construct_system_account_set(system_account_set()) -> + {system_account_set, dmsl_domain_thrift:'SystemAccountSetObject'()}. +construct_system_account_set(Ref) -> + construct_system_account_set(Ref, <<"Primaries">>, ?cur(<<"RUB">>)). + +-spec construct_system_account_set(system_account_set(), name(), currency()) -> + {system_account_set, dmsl_domain_thrift:'SystemAccountSetObject'()}. +construct_system_account_set(Ref, Name, ?cur(CurrencyCode)) -> + AccountID = 3, + {system_account_set, #domain_SystemAccountSetObject{ + ref = Ref, + data = #domain_SystemAccountSet{ + name = Name, + description = Name, + accounts = #{ + ?cur(CurrencyCode) => #domain_SystemAccount{ + settlement = AccountID + } + } + } + }}. + +-spec construct_external_account_set(external_account_set()) -> + {external_account_set, dmsl_domain_thrift:'ExternalAccountSetObject'()}. +construct_external_account_set(Ref) -> + construct_external_account_set(Ref, <<"Primaries">>, ?cur(<<"RUB">>)). + +-spec construct_external_account_set(external_account_set(), name(), currency()) -> + {external_account_set, dmsl_domain_thrift:'ExternalAccountSetObject'()}. +construct_external_account_set(Ref, Name, ?cur(CurrencyCode)) -> + AccountID1 = 1, + AccountID2 = 2, + {external_account_set, #domain_ExternalAccountSetObject{ + ref = Ref, + data = #domain_ExternalAccountSet{ + name = Name, + description = Name, + accounts = #{ + ?cur(CurrencyCode) => #domain_ExternalAccount{ + income = AccountID1, + outcome = AccountID2 + } + } + } + }}. + +-spec construct_business_schedule(business_schedule()) -> + {business_schedule, dmsl_domain_thrift:'BusinessScheduleObject'()}. +construct_business_schedule(Ref) -> + {business_schedule, #domain_BusinessScheduleObject{ + ref = Ref, + data = #domain_BusinessSchedule{ + name = <<"Every day at 7:40">>, + schedule = #base_Schedule{ + year = ?every, + month = ?every, + day_of_month = ?every, + day_of_week = ?every, + hour = {on, [7]}, + minute = {on, [40]}, + second = {on, [0]} + } + } + }}. + +-spec construct_routing_ruleset(routing_ruleset_ref(), name(), _) -> + {routing_rules, dmsl_domain_thrift:'RoutingRulesObject'()}. +construct_routing_ruleset(Ref, Name, Decisions) -> + {routing_rules, #domain_RoutingRulesObject{ + ref = Ref, + data = #domain_RoutingRuleset{ + name = Name, + decisions = Decisions + } + }}. diff --git a/apps/party_client/test/party_domain_fixtures.hrl b/apps/party_client/test/party_domain_fixtures.hrl new file mode 100644 index 00000000..dc2f0cc1 --- /dev/null +++ b/apps/party_client/test/party_domain_fixtures.hrl @@ -0,0 +1,75 @@ +-ifndef(__party_domain_fixtures__). +-define(__party_domain_fixtures__, true). + +-include_lib("damsel/include/dmsl_base_thrift.hrl"). +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). + +-define(ordset(Es), ordsets:from_list(Es)). + +-define(cur(ID), #domain_CurrencyRef{symbolic_code = ID}). +-define(pmt(C, T), #domain_PaymentMethodRef{id = {C, T}}). +-define(pmt_bank_card(T), + ?pmt(bank_card, #domain_BankCardPaymentMethod{payment_system = #domain_PaymentSystemRef{id = atom_to_binary(T)}}) +). +-define(cat(ID), #domain_CategoryRef{id = ID}). +-define(prx(ID), #domain_ProxyRef{id = ID}). +-define(tmpl(ID), #domain_ContractTemplateRef{id = ID}). +-define(trms(ID), #domain_TermSetHierarchyRef{id = ID}). +-define(sas(ID), #domain_SystemAccountSetRef{id = ID}). +-define(eas(ID), #domain_ExternalAccountSetRef{id = ID}). +-define(insp(ID), #domain_InspectorRef{id = ID}). +-define(pinst(ID), #domain_PaymentInstitutionRef{id = ID}). +-define(bussched(ID), #domain_BusinessScheduleRef{id = ID}). +-define(trm(ID), #domain_TerminalRef{id = ID}). +-define(prv(ID), #domain_ProviderRef{id = ID}). +-define(p2pprov(ID), #domain_P2PProviderRef{id = ID}). +-define(wtdrlprov(ID), #domain_WithdrawalProviderRef{id = ID}). +-define(prvtrm(ID), #domain_ProviderTerminalRef{id = ID}). +-define(ruleset(ID), #domain_RoutingRulesetRef{id = ID}). + +-define(cashrng(Lower, Upper), #domain_CashRange{lower = Lower, upper = Upper}). + +-define(currency(SymCode), #domain_CurrencyRef{symbolic_code = SymCode}). + +-define(cash(Amount, SymCode), #domain_Cash{amount = Amount, currency = ?currency(SymCode)}). + +-define(fixed(Amount, Currency), + {fixed, #domain_CashVolumeFixed{ + cash = #domain_Cash{ + amount = Amount, + currency = ?currency(Currency) + } + }} +). + +-define(prvacc(Stl), #domain_ProviderAccount{settlement = Stl}). + +-define(cfpost(A1, A2, V), #domain_CashFlowPosting{ + source = A1, + destination = A2, + volume = V +}). + +-define(share(P, Q, C), + {share, #domain_CashVolumeShare{ + parts = #base_Rational{p = P, q = Q}, + 'of' = C + }} +). + +-define(share(P, Q, C, RM), + {share, #domain_CashVolumeShare{ + parts = #base_Rational{p = P, q = Q}, + 'of' = C, + 'rounding_method' = RM + }} +). + +-define(tkz_bank_card(PaymentSystem, TokenProvider), #domain_TokenizedBankCard{ + payment_system_deprecated = PaymentSystem, + token_provider_deprecated = TokenProvider +}). + +-define(every, {every, #base_ScheduleEvery{}}). + +-endif. diff --git a/apps/party_management/include/domain.hrl b/apps/party_management/include/domain.hrl new file mode 100644 index 00000000..161a97c1 --- /dev/null +++ b/apps/party_management/include/domain.hrl @@ -0,0 +1,8 @@ +-ifndef(__pm_domain_hrl__). +-define(__pm_domain_hrl__, included). + +-define(currency(SymCode), #domain_CurrencyRef{symbolic_code = SymCode}). + +-define(cash(Amount, SymCode), #domain_Cash{amount = Amount, currency = ?currency(SymCode)}). + +-endif. diff --git a/apps/party_management/src/party_management.app.src b/apps/party_management/src/party_management.app.src new file mode 100644 index 00000000..837e9cc2 --- /dev/null +++ b/apps/party_management/src/party_management.app.src @@ -0,0 +1,33 @@ +{application, party_management, [ + {description, "Party management things"}, + {vsn, "1"}, + {registered, []}, + {mod, {party_management, []}}, + {applications, [ + kernel, + stdlib, + genlib, + pm_proto, + cowboy, + prometheus, + prometheus_cowboy, + woody, + % should be before any scoper event handler usage + scoper, + gproc, + dmt_client, + payproc_errors, + erl_health, + cache, + opentelemetry_api, + opentelemetry_exporter, + opentelemetry + ]}, + {env, []}, + {modules, []}, + {maintainers, [ + "Andrey Mayorov " + ]}, + {licenses, []}, + {links, ["https://github.com/rbkmoney/party_management"]} +]}. diff --git a/apps/party_management/src/party_management.erl b/apps/party_management/src/party_management.erl new file mode 100644 index 00000000..b496c755 --- /dev/null +++ b/apps/party_management/src/party_management.erl @@ -0,0 +1,97 @@ +%%% @doc Public API, supervisor and application startup. +%%% @end + +-module(party_management). + +-behaviour(supervisor). +-behaviour(application). + +%% API +-export([start/0]). +-export([stop/0]). + +%% Supervisor callbacks +-export([init/1]). + +%% Application callbacks +-export([start/2]). +-export([stop/1]). + +%% +%% API +%% +-spec start() -> {ok, _}. +start() -> + application:ensure_all_started(?MODULE). + +-spec stop() -> ok. +stop() -> + application:stop(?MODULE). + +%% Supervisor callbacks + +-spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. +init([]) -> + Options = application:get_env(?MODULE, cache_options, #{}), + {ok, + { + #{strategy => one_for_all, intensity => 6, period => 30}, + [ + get_api_child_spec(Options) + ] + }}. + +get_api_child_spec(Opts) -> + {ok, Ip} = inet:parse_address(genlib_app:env(?MODULE, ip, "::")), + HealthRoutes = construct_health_routes(genlib_app:env(?MODULE, health_check, #{})), + EventHandlerOpts = genlib_app:env(?MODULE, scoper_event_handler_options, #{}), + PrometeusRoute = get_prometheus_route(), + EventHandlers = {pm_woody_event_handler, EventHandlerOpts}, + woody_server:child_spec( + ?MODULE, + #{ + ip => Ip, + port => genlib_app:env(?MODULE, port, 8022), + transport_opts => genlib_app:env(?MODULE, transport_opts, #{}), + protocol_opts => genlib_app:env(?MODULE, protocol_opts, #{}), + event_handler => EventHandlers, + handlers => + [ + construct_service_handler(party_management, pm_party_handler, Opts) + ], + additional_routes => [PrometeusRoute | HealthRoutes], + shutdown_timeout => genlib_app:env(?MODULE, shutdown_timeout, 0) + } + ). + +construct_health_routes(Check) -> + [erl_health_handle:get_route(enable_health_logging(Check))]. + +enable_health_logging(Check) -> + EvHandler = {erl_health_event_handler, []}, + maps:map(fun(_, {_, _, _} = V) -> #{runner => V, event_handler => EvHandler} end, Check). + +construct_service_handler(Name, Module, Opts) -> + {Path, Service} = pm_proto:get_service_spec(Name), + {Path, {Service, {pm_woody_wrapper, maps:merge(#{handler => Module}, Opts)}}}. + +-spec get_prometheus_route() -> {iodata(), module(), _Opts :: any()}. +get_prometheus_route() -> + {"/metrics/[:registry]", prometheus_cowboy2_handler, []}. + +%% Application callbacks + +-spec start(normal, any()) -> {ok, pid()} | {error, any()}. +start(_StartType, _StartArgs) -> + ok = setup_metrics(), + supervisor:start_link(?MODULE, []). + +-spec stop(any()) -> ok. +stop(_State) -> + ok. + +%% + +setup_metrics() -> + ok = woody_ranch_prometheus_collector:setup(), + ok = woody_hackney_prometheus_collector:setup(). diff --git a/apps/party_management/src/pm_accounting.erl b/apps/party_management/src/pm_accounting.erl new file mode 100644 index 00000000..85fa1042 --- /dev/null +++ b/apps/party_management/src/pm_accounting.erl @@ -0,0 +1,93 @@ +-module(pm_accounting). + +-export([get_account/1]). +-export([get_balance/1]). +-export([create_account/1]). + +-include_lib("damsel/include/dmsl_payproc_thrift.hrl"). +-include_lib("damsel/include/dmsl_accounter_thrift.hrl"). + +-type amount() :: dmsl_domain_thrift:'Amount'(). +-type currency_code() :: dmsl_domain_thrift:'CurrencySymbolicCode'(). +-type account_id() :: dmsl_accounter_thrift:'AccountID'(). +-type thrift_account() :: dmsl_accounter_thrift:'Account'(). + +-type account() :: #{ + account_id => account_id(), + currency_code => currency_code() +}. + +-type balance() :: #{ + account_id => account_id(), + own_amount => amount(), + min_available_amount => amount(), + max_available_amount => amount() +}. + +-spec get_account(account_id()) -> account(). +get_account(AccountID) -> + Account = do_get_account(AccountID), + construct_account(Account). + +-spec get_balance(account_id()) -> balance(). +get_balance(AccountID) -> + Account = do_get_account(AccountID), + construct_balance(Account). + +-spec create_account(currency_code()) -> account_id(). +create_account(CurrencyCode) -> + create_account(CurrencyCode, undefined). + +-spec create_account(currency_code(), binary() | undefined) -> account_id(). +create_account(CurrencyCode, Description) -> + {ok, Result} = call_accounter('CreateAccount', {construct_prototype(CurrencyCode, Description)}), + Result. + +-spec do_get_account(account_id()) -> thrift_account(). +do_get_account(AccountID) -> + case call_accounter('GetAccountByID', {AccountID}) of + {ok, Result} -> + Result; + {exception, #accounter_AccountNotFound{}} -> + pm_woody_wrapper:raise(#payproc_AccountNotFound{}) + end. + +construct_prototype(CurrencyCode, Description) -> + #accounter_AccountPrototype{ + currency_sym_code = CurrencyCode, + description = Description, + creation_time = pm_datetime:format_now() + }. + +%% + +construct_account( + #accounter_Account{ + id = AccountID, + currency_sym_code = CurrencyCode + } +) -> + #{ + account_id => AccountID, + currency_code => CurrencyCode + }. + +construct_balance( + #accounter_Account{ + id = AccountID, + own_amount = OwnAmount, + min_available_amount = MinAvailableAmount, + max_available_amount = MaxAvailableAmount + } +) -> + #{ + account_id => AccountID, + own_amount => OwnAmount, + min_available_amount => MinAvailableAmount, + max_available_amount => MaxAvailableAmount + }. + +%% + +call_accounter(Function, Args) -> + pm_woody_wrapper:call(accounter, Function, Args). diff --git a/apps/party_management/src/pm_cash_range.erl b/apps/party_management/src/pm_cash_range.erl new file mode 100644 index 00000000..425513e9 --- /dev/null +++ b/apps/party_management/src/pm_cash_range.erl @@ -0,0 +1,35 @@ +-module(pm_cash_range). + +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). + +-include("domain.hrl"). + +-export([is_inside/2]). + +-type cash_range() :: dmsl_domain_thrift:'CashRange'(). +-type cash() :: dmsl_domain_thrift:'Cash'(). + +-spec is_inside(cash(), cash_range()) -> within | {exceeds, lower | upper} | {error, incompatible}. +is_inside(Cash, #domain_CashRange{lower = Lower, upper = Upper}) -> + case + { + compare_cash(fun erlang:'>'/2, Cash, Lower), + compare_cash(fun erlang:'<'/2, Cash, Upper) + } + of + {true, true} -> + within; + {false, true} -> + {exceeds, lower}; + {true, false} -> + {exceeds, upper}; + _ -> + {error, incompatible} + end. + +compare_cash(_, V, {inclusive, V}) -> + true; +compare_cash(F, ?cash(A, C), {_, ?cash(Am, C)}) -> + F(A, Am); +compare_cash(_, _, _) -> + error. diff --git a/apps/party_management/src/pm_cashflow.erl b/apps/party_management/src/pm_cashflow.erl new file mode 100644 index 00000000..1072ffc8 --- /dev/null +++ b/apps/party_management/src/pm_cashflow.erl @@ -0,0 +1,144 @@ +%%% Cash flow computations +%%% +%%% TODO +%%% - reduction raises suspicions +%%% - should we consider posting with the same source and destination invalid? +%%% - did we get rid of splicing for good? +%%% - we should probably validate final cash flow somewhere here + +-module(pm_cashflow). + +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). +-include_lib("damsel/include/dmsl_base_thrift.hrl"). + +-type account() :: dmsl_domain_thrift:'CashFlowAccount'(). +-type account_id() :: dmsl_domain_thrift:'AccountID'(). +-type account_map() :: #{account() => account_id()}. +-type context() :: dmsl_domain_thrift:'CashFlowContext'(). +-type cash_flow() :: dmsl_domain_thrift:'CashFlow'(). +-type final_cash_flow() :: dmsl_domain_thrift:'FinalCashFlow'(). + +%% + +-export([finalize/3]). + +%% + +-define(posting(Source, Destination, Volume, Details), #domain_CashFlowPosting{ + source = Source, + destination = Destination, + volume = Volume, + details = Details +}). + +-define(final_posting(Source, Destination, Volume, Details), #domain_FinalCashFlowPosting{ + source = Source, + destination = Destination, + volume = Volume, + details = Details +}). + +-spec finalize(cash_flow(), context(), account_map()) -> final_cash_flow() | no_return(). +finalize(CF, Context, AccountMap) -> + compute_postings(CF, Context, AccountMap). + +compute_postings(CF, Context, AccountMap) -> + [ + ?final_posting( + construct_final_account(Source, AccountMap), + construct_final_account(Destination, AccountMap), + compute_volume(Volume, Context), + Details + ) + || ?posting(Source, Destination, Volume, Details) <- CF + ]. + +construct_final_account(AccountType, AccountMap) -> + #domain_FinalCashFlowAccount{ + account_type = AccountType, + account_id = resolve_account(AccountType, AccountMap) + }. + +resolve_account(AccountType, AccountMap) -> + case AccountMap of + #{AccountType := V} -> + V; + #{} -> + error({misconfiguration, {'Cash flow account can not be mapped', {AccountType, AccountMap}}}) + end. + +%% + +-define(fixed(Cash), + {fixed, #domain_CashVolumeFixed{cash = Cash}} +). + +-define(share(P, Q, Of, RoundingMethod), + {share, #domain_CashVolumeShare{'parts' = ?rational(P, Q), 'of' = Of, 'rounding_method' = RoundingMethod}} +). + +-define(product(Fun, CVs), + {product, {Fun, CVs}} +). + +-define(rational(P, Q), #base_Rational{p = P, q = Q}). + +compute_volume(?fixed(Cash), _Context) -> + Cash; +compute_volume(?share(P, Q, Of, RoundingMethod), Context) -> + compute_parts_of(P, Q, resolve_constant(Of, Context), RoundingMethod); +compute_volume(?product(Fun, CVs) = CV0, Context) -> + case ordsets:size(CVs) of + N when N > 0 -> + compute_product(Fun, ordsets:to_list(CVs), CV0, Context); + 0 -> + error({misconfiguration, {'Cash volume product over empty set', CV0}}) + end. + +compute_parts_of(P, Q, #domain_Cash{amount = Amount} = Cash, RoundingMethod) -> + Cash#domain_Cash{ + amount = genlib_rational:round( + genlib_rational:mul( + genlib_rational:new(Amount), + genlib_rational:new(P, Q) + ), + get_rounding_method(RoundingMethod) + ) + }. + +compute_product(Fun, [CV | CVRest], CV0, Context) -> + lists:foldl( + fun(CVN, CVMin) -> compute_product(Fun, CVN, CVMin, CV0, Context) end, + compute_volume(CV, Context), + CVRest + ). + +compute_product(Fun, CV, #domain_Cash{amount = AmountMin, currency = Currency} = CVMin, CV0, Context) -> + case compute_volume(CV, Context) of + #domain_Cash{amount = Amount, currency = Currency} -> + CVMin#domain_Cash{amount = compute_product_fun(Fun, AmountMin, Amount)}; + _ -> + error({misconfiguration, {'Cash volume product over volumes of different currencies', CV0}}) + end. + +compute_product_fun(min_of, V1, V2) -> + erlang:min(V1, V2); +compute_product_fun(max_of, V1, V2) -> + erlang:max(V1, V2); +compute_product_fun(sum_of, V1, V2) -> + V1 + V2. + +resolve_constant(Constant, Context) -> + case Context of + #{Constant := V} -> + V; + #{} -> + error({misconfiguration, {'Cash flow constant not found', {Constant, Context}}}) + end. + +get_rounding_method(undefined) -> + round_half_away_from_zero; +get_rounding_method(round_half_towards_zero) -> + round_half_towards_zero; +get_rounding_method(round_half_away_from_zero) -> + round_half_away_from_zero. diff --git a/apps/party_management/src/pm_condition.erl b/apps/party_management/src/pm_condition.erl new file mode 100644 index 00000000..5417f983 --- /dev/null +++ b/apps/party_management/src/pm_condition.erl @@ -0,0 +1,289 @@ +-module(pm_condition). + +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). +-include_lib("party_management/include/domain.hrl"). + +%% + +-export([test/3]). +-export([some_defined/1]). +-export([ternary_and/1]). +-export([ternary_or/1]). +-export([ternary_while/1]). + +%% +-export_type([ternary_term/0]). +-export_type([ternary_value/0]). + +-type condition() :: dmsl_domain_thrift:'Condition'(). +-type varset() :: pm_selector:varset(). +-type ternary_term() :: ternary_lazy_term() | ternary_simple_term(). +-type ternary_lazy_term() :: fun(() -> ternary_simple_term()). +%% Any (any()) other value is evaluated as true +-type ternary_simple_term() :: ternary_value() | any(). +-type ternary_value() :: true | undefined | false. + +-spec test(condition(), varset(), pm_domain:revision()) -> true | false | undefined. +test({category_is, V1}, #{category := V2}, _) -> + V1 =:= V2; +test({currency_is, V1}, #{currency := V2}, _) -> + V1 =:= V2; +test({cost_in, V}, #{cost := C}, _) -> + pm_cash_range:is_inside(C, V) =:= within; +test({cost_is_multiple_of, V}, #{cost := C}, _) -> + test_cash_is_multiple_of(C, V); +test({payment_tool, C}, #{payment_tool := V}, Rev) -> + pm_payment_tool:test_condition(C, V, Rev); +test({shop_location_is, V}, #{shop := S}, _) -> + V =:= S#domain_ShopConfig.location; +test({party, V}, #{party_ref := PartyRef} = VS, _) -> + test_party(V, PartyRef, VS); +test({identification_level_is, V1}, #{identification_level := V2}, _) -> + V1 =:= V2; +test({bin_data, #domain_BinDataCondition{} = C}, #{bin_data := #domain_BinData{} = V}, Rev) -> + test_bindata_tool(C, V, Rev); +test({trust_level_is, V1}, #{trust_level := V2}, _) -> + V1 =:= V2; +test(_, #{}, _) -> + undefined. + +test_party(#domain_PartyCondition{party_ref = PartyRef, definition = Def}, PartyRef, VS) -> + test_party_definition(Def, VS); +test_party(_, _, _) -> + false. + +test_party_definition(undefined, _) -> + true; +test_party_definition({shop_is, ID1}, #{shop_id := ID2}) -> + ID1 =:= ID2; +test_party_definition({wallet_is, ID1}, #{wallet_id := ID2}) -> + ID1 =:= ID2; +test_party_definition(_, _) -> + undefined. + +test_bindata_tool( + #domain_BinDataCondition{ + payment_system = PaymentSystemCondition, + bank_name = BankNameCondition + }, + #domain_BinData{ + payment_system = PaymentSystem, + bank_name = BankName + }, + _Rev +) -> + ternary_and([ + ternary_or([ + PaymentSystemCondition == undefined, + fun() -> test_string_condition(PaymentSystemCondition, PaymentSystem) end + ]), + ternary_or([ + BankNameCondition == undefined, + fun() -> test_string_condition(BankNameCondition, BankName) end + ]) + ]). + +test_string_condition({matches, Substring}, String) -> + string:find(String, Substring) /= nomatch; +test_string_condition({equals, String1}, String2) -> + String1 =:= String2. + +-spec some_defined(list()) -> boolean(). +some_defined(List) -> + genlib_list:compact(List) /= []. + +%% Ternary AND +%% Truth-table +%% T U F +%% T T U F +%% U U U F +%% F F F F +%% Empty list is undefined +%% Lazily-evaluated if applicable +-spec ternary_and([ternary_term()]) -> ternary_value(). +ternary_and([]) -> + undefined; +ternary_and(List) -> + genlib_list:foldl_while( + fun(Elem, Acc) -> + case ternary_and(compute_term(Elem), Acc) of + false -> {halt, false}; + Result -> {cont, Result} + end + end, + true, + List + ). + +%% Ternary OR +%% Truth-table +%% T U F +%% T T T T +%% U T U U +%% F T U F +%% Empty list is undefined +%% Lazily-evaluated if applicable +-spec ternary_or([ternary_term()]) -> ternary_value(). +ternary_or([]) -> + undefined; +ternary_or(List) -> + genlib_list:foldl_while( + fun(Elem, Acc) -> + case ternary_or(compute_term(Elem), Acc) of + true -> {halt, true}; + Result -> {cont, Result} + end + end, + false, + List + ). + +%% Similar to Ternary AND, but stops on any non-true value +%% Useful for calculating applications with possibly-defined dependencies, like: +%% ternary_while([Arg1, Arg2, fun () -> fn(Arg1, Arg2) end]) +%% Truth-table +%% T U F +%% T T U F +%% U U U U +%% F F F F +%% (T if all arguments are T, first non-T arg otherwise) +%% Empty list is undefined +%% Lazily-evaluated if applicable +-spec ternary_while([ternary_term()]) -> ternary_value(). +ternary_while(Terms) -> + genlib_list:foldl_while( + fun(Term, _) -> + case compute_term(Term) of + true -> {cont, true}; + Result -> {halt, Result} + end + end, + undefined, + Terms + ). + +ternary_and(true, true) -> + true; +ternary_and(MaybeLeftFalse, MaybeRightFalse) when MaybeLeftFalse == false; MaybeRightFalse == false -> + false; +ternary_and(MaybeLeftUndef, MaybeRightUndef) when MaybeLeftUndef == undefined; MaybeRightUndef == undefined -> + undefined. + +ternary_or(false, false) -> + false; +ternary_or(MaybeLeftTrue, MaybeRightTrue) when MaybeLeftTrue == true; MaybeRightTrue == true -> + true; +ternary_or(MaybeLeftUndef, MaybeRightUndef) when MaybeLeftUndef == undefined; MaybeRightUndef == undefined -> + undefined. + +compute_term(Fun) when is_function(Fun, 0) -> to_ternary_bool(Fun()); +compute_term(Term) -> to_ternary_bool(Term). + +to_ternary_bool(Bool) when is_boolean(Bool) -> Bool; +to_ternary_bool(undefined) -> undefined; +to_ternary_bool(_) -> true. + +test_cash_is_multiple_of( + #domain_Cash{amount = A1, currency = C}, + #domain_Cash{amount = A2, currency = C} +) -> + A1 rem A2 =:= 0; +test_cash_is_multiple_of(_, _) -> + false. + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +-spec test() -> _. + +-spec some_defined_empty_test() -> _. +some_defined_empty_test() -> + ?assertEqual(false, some_defined([])). + +-spec some_defined_undefined_test() -> _. +some_defined_undefined_test() -> + ?assertEqual(false, some_defined([undefined, undefined, undefined])). + +-spec some_defined_defined_test() -> _. +some_defined_defined_test() -> + ?assertEqual(true, some_defined([undefined, undefined, true])). + +-spec ternary_and_empty_args_test() -> _. +ternary_and_empty_args_test() -> + ?assertEqual(undefined, ternary_and([])). + +-spec ternary_and_truth_table_test() -> _. +ternary_and_truth_table_test() -> + Table = [ + {true, true, true}, + {true, undefined, undefined}, + {true, false, false}, + {undefined, true, undefined}, + {undefined, undefined, undefined}, + {undefined, false, false}, + {false, true, false}, + {false, undefined, false}, + {false, false, false} + ], + lists:foreach( + fun({L, R, Result}) -> + ?assertEqual(Result, ternary_and([L, R])) + end, + Table + ). + +-spec ternary_or_empty_args_test() -> _. +ternary_or_empty_args_test() -> + ?assertEqual(undefined, ternary_or([])). + +-spec ternary_or_truth_table_test() -> _. +ternary_or_truth_table_test() -> + Table = [ + {true, true, true}, + {true, undefined, true}, + {true, false, true}, + {undefined, true, true}, + {undefined, undefined, undefined}, + {undefined, false, undefined}, + {false, true, true}, + {false, undefined, undefined}, + {false, false, false} + ], + lists:foreach( + fun({L, R, Result}) -> + ?assertEqual(Result, ternary_or([L, R])) + end, + Table + ). + +-spec ternary_while_truth_table_test() -> _. +ternary_while_truth_table_test() -> + Table = [ + {true, true, true}, + {true, undefined, undefined}, + {true, false, false}, + {undefined, true, undefined}, + {undefined, undefined, undefined}, + {undefined, false, undefined}, + {false, true, false}, + {false, undefined, false}, + {false, false, false} + ], + lists:foreach( + fun({L, R, Result}) -> + ?assertEqual(Result, ternary_while([L, R])) + end, + Table + ). + +-spec cash_is_multiple_of_condition_test() -> _. +cash_is_multiple_of_condition_test() -> + Currency1 = <<"RUB">>, + Currency2 = <<"USD">>, + _ = [ + ?assertEqual(test_cash_is_multiple_of(?cash(10, Currency1), ?cash(5, Currency1)), true), + ?assertEqual(test_cash_is_multiple_of(?cash(10, Currency1), ?cash(7, Currency1)), false), + ?assertEqual(test_cash_is_multiple_of(?cash(10, Currency1), ?cash(5, Currency2)), false) + ]. + +-endif. diff --git a/apps/party_management/src/pm_context.erl b/apps/party_management/src/pm_context.erl new file mode 100644 index 00000000..225e036c --- /dev/null +++ b/apps/party_management/src/pm_context.erl @@ -0,0 +1,69 @@ +-module(pm_context). + +-export([create/0]). +-export([create/1]). +-export([save/1]). +-export([load/0]). +-export([cleanup/0]). + +-export([get_woody_context/1]). + +-opaque context() :: #{ + woody_context := woody_context() +}. + +-type options() :: #{ + woody_context => woody_context() +}. + +-export_type([context/0]). +-export_type([options/0]). + +%% Internal types + +-type woody_context() :: woody_context:ctx(). + +%% TODO change when moved to separate app +-define(REGISTRY_KEY, {p, l, stored_hg_context}). + +%% API + +-spec create() -> context(). +create() -> + create(#{}). + +-spec create(options()) -> context(). +create(Options0) -> + ensure_woody_context_exists(Options0). + +-spec save(context()) -> ok. +save(Context) -> + true = + try + gproc:reg(?REGISTRY_KEY, Context) + catch + error:badarg -> + gproc:set_value(?REGISTRY_KEY, Context) + end, + ok. + +-spec load() -> context() | no_return(). +load() -> + gproc:get_value(?REGISTRY_KEY). + +-spec cleanup() -> ok. +cleanup() -> + true = gproc:unreg(?REGISTRY_KEY), + ok. + +-spec get_woody_context(context()) -> woody_context(). +get_woody_context(#{woody_context := WoodyContext}) -> + WoodyContext. + +%% Internal functions + +-spec ensure_woody_context_exists(options()) -> options(). +ensure_woody_context_exists(#{woody_context := _WoodyContext} = Options) -> + Options; +ensure_woody_context_exists(Options) -> + Options#{woody_context => woody_context:new()}. diff --git a/apps/party_management/src/pm_currency.erl b/apps/party_management/src/pm_currency.erl new file mode 100644 index 00000000..e21a2101 --- /dev/null +++ b/apps/party_management/src/pm_currency.erl @@ -0,0 +1,24 @@ +%%% Currency related functions +%%% + +-module(pm_currency). + +-include_lib("damsel/include/dmsl_base_thrift.hrl"). +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). + +-export([validate_currency/2]). + +-type currency() :: dmsl_domain_thrift:'CurrencyRef'(). +-type shop() :: dmsl_domain_thrift:'ShopConfig'(). + +-spec validate_currency(currency(), shop()) -> ok. +validate_currency(Currency, #domain_ShopConfig{} = Shop) -> + validate_currency_(Currency, get_shop_currency(Shop)). + +validate_currency_(Currency, Currency) -> + ok; +validate_currency_(_, _) -> + throw(#base_InvalidRequest{errors = [<<"Invalid currency">>]}). + +get_shop_currency(#domain_ShopConfig{account = #domain_ShopAccount{currency = Currency}}) -> + Currency. diff --git a/apps/party_management/src/pm_datetime.erl b/apps/party_management/src/pm_datetime.erl new file mode 100644 index 00000000..b72e934b --- /dev/null +++ b/apps/party_management/src/pm_datetime.erl @@ -0,0 +1,114 @@ +-module(pm_datetime). + +%% + +-export([format_now/0]). +-export([compare/2]). +-export([between/2]). +-export([between/3]). +-export([add_interval/2]). + +-include_lib("damsel/include/dmsl_base_thrift.hrl"). + +-type unix_timestamp() :: integer(). +-type timestamp() :: dmsl_base_thrift:'Timestamp'(). +-type timestamp_interval() :: dmsl_base_thrift:'TimestampInterval'(). +-type timestamp_interval_bound() :: dmsl_base_thrift:'TimestampIntervalBound'(). + +%% not exported from calendar module +-type rfc3339_time_unit() :: + microsecond + | millisecond + | nanosecond + | second. + +-export_type([timestamp/0]). + +%% + +-spec format_ts(unix_timestamp()) -> timestamp(). +format_ts(Ts) when is_integer(Ts) -> + format_ts(Ts, second). + +-spec format_now() -> timestamp(). +format_now() -> + USec = erlang:system_time(microsecond), + format_ts(USec, microsecond). + +-spec compare(timestamp(), timestamp()) -> later | earlier | simultaneously. +compare(T1, T2) when is_binary(T1) andalso is_binary(T2) -> + compare_int(to_integer(T1), to_integer(T2)). + +% Compare inclusivly! undefined == ∞ +-spec between(timestamp(), timestamp() | undefined, timestamp() | undefined) -> boolean(). +between(Timestamp, Start, End) -> + LB = to_interval_bound(Start, inclusive), + UB = to_interval_bound(End, inclusive), + between(Timestamp, #base_TimestampInterval{lower_bound = LB, upper_bound = UB}). + +-spec between(timestamp(), timestamp_interval()) -> boolean(). +between(Timestamp, #base_TimestampInterval{lower_bound = LB, upper_bound = UB}) -> + check_bound(Timestamp, LB, later) andalso + check_bound(Timestamp, UB, earlier). + +-spec add_interval(timestamp(), {Years, Months, Days}) -> timestamp() when + Years :: integer() | undefined, + Months :: integer() | undefined, + Days :: integer() | undefined. +add_interval(Timestamp, {YY, MM, DD}) -> + TSSeconds = erlang:convert_time_unit(to_integer(Timestamp), microsecond, second), + {Date, Time} = genlib_time:unixtime_to_daytime(TSSeconds), + NewDate = genlib_time:shift_date(Date, {nvl(YY), nvl(MM), nvl(DD)}), + format_ts(genlib_time:daytime_to_unixtime({NewDate, Time})). + +-spec parse(binary(), rfc3339_time_unit()) -> integer(). +parse(Bin, Precision) when is_binary(Bin) -> + Str = erlang:binary_to_list(Bin), + calendar:rfc3339_to_system_time(Str, [{unit, Precision}]). + +%% Internal functions + +-spec format_ts(integer(), rfc3339_time_unit()) -> timestamp(). +format_ts(Ts, Unit) -> + Str = calendar:system_time_to_rfc3339(Ts, [{unit, Unit}, {offset, "Z"}]), + erlang:list_to_binary(Str). + +-spec to_integer(timestamp()) -> integer(). +to_integer(Timestamp) -> + parse(Timestamp, microsecond). + +to_interval_bound(undefined, _) -> + undefined; +to_interval_bound(Timestamp, BoundType) -> + #base_TimestampIntervalBound{bound_type = BoundType, bound_time = Timestamp}. + +compare_int(T1, T2) -> + case T1 > T2 of + true -> + later; + false when T1 < T2 -> + earlier; + false when T1 =:= T2 -> + simultaneously + end. + +-spec check_bound(timestamp(), timestamp_interval_bound(), later | earlier) -> boolean(). +check_bound(_, undefined, _) -> + true; +check_bound(Timestamp, #base_TimestampIntervalBound{bound_type = Type, bound_time = BoundTime}, Operator) -> + case compare(Timestamp, BoundTime) of + Operator -> + true; + simultaneously when Type == inclusive -> + true; + _ -> + false + end. + +nvl(Val) -> + nvl(Val, 0). + +nvl(undefined, Default) -> + Default; +nvl(Val, _) -> + Val. diff --git a/apps/party_management/src/pm_domain.erl b/apps/party_management/src/pm_domain.erl new file mode 100644 index 00000000..1b14494d --- /dev/null +++ b/apps/party_management/src/pm_domain.erl @@ -0,0 +1,162 @@ +%%% Domain config interfaces +%%% +%%% TODO +%%% - Use proper reflection instead of blind pattern matching when (un)wrapping +%%% domain objects + +-module(pm_domain). + +-include_lib("damsel/include/dmsl_domain_conf_v2_thrift.hrl"). + +%% + +-export([head/0]). +-export([get/2]). +-export([find/2]). +-export([exists/2]). +-export([find_party_with_shops_and_wallets/2]). + +-export([insert/1]). +-export([update/1]). +-export([upsert/1]). +-export([cleanup/1]). + +%% + +-type revision() :: pos_integer(). +-type ref() :: dmt_client:object_ref(). +-type object() :: dmt_client:domain_object(). +-type data() :: _. + +-export_type([revision/0]). +-export_type([ref/0]). +-export_type([object/0]). +-export_type([data/0]). + +-spec head() -> revision(). +head() -> + dmt_client:get_latest_version(). + +-spec get(revision(), ref()) -> data() | no_return(). +get(Revision, Ref) -> + try + extract_data(dmt_client:checkout_object(Revision, Ref)) + catch + error:version_not_found -> + error({object_not_found, {Revision, Ref}}); + throw:#domain_conf_v2_ObjectNotFound{} -> + error({object_not_found, {Revision, Ref}}) + end. + +-spec find(revision(), ref()) -> data() | notfound. +find(Revision, Ref) -> + try + extract_data(dmt_client:checkout_object(Revision, Ref)) + catch + error:version_not_found -> + notfound; + throw:#domain_conf_v2_ObjectNotFound{} -> + notfound + end. + +-spec exists(revision(), ref()) -> boolean(). +exists(Revision, Ref) -> + try + _ = dmt_client:checkout_object(Revision, Ref), + true + catch + throw:#domain_conf_v2_ObjectNotFound{} -> + false + end. + +extract_data(#domain_conf_v2_VersionedObject{object = {_Tag, {_Name, _Ref, Data}}}) -> + Data. + +commit(Revision, Operations, AuthorID) -> + dmt_client:commit(Revision, Operations, AuthorID). + +-spec find_party_with_shops_and_wallets(revision(), dmsl_domain_thrift:'PartyConfigRef'()) -> + { + ok, + dmsl_domain_thrift:'PartyConfigObject'(), + [dmsl_domain_thrift:'ShopConfigObject'()], + [dmsl_domain_thrift:'WalletConfigObject'()] + } + | {error, notfound}. +find_party_with_shops_and_wallets(Revision, PartyRef) -> + try + #domain_conf_v2_VersionedObjectWithReferences{ + object = #domain_conf_v2_VersionedObject{object = {party_config, Object}}, + referenced_by = ReferencedBy + } = + dmt_client:checkout_object_with_references(Revision, {party_config, PartyRef}), + {Shops, Wallets} = lists:foldl( + fun + (#domain_conf_v2_VersionedObject{object = {shop_config, O}}, {S, W}) -> {[O | S], W}; + (#domain_conf_v2_VersionedObject{object = {wallet_config, O}}, {S, W}) -> {S, [O | W]}; + (_, Acc) -> Acc + end, + {[], []}, + ReferencedBy + ), + {ok, Object, Shops, Wallets} + catch + error:version_not_found -> + {error, notfound}; + throw:#domain_conf_v2_ObjectNotFound{} -> + {error, notfound} + end. + +-spec insert(object() | [object()]) -> {revision(), [ref()]} | no_return(). +insert(Objects) -> + insert(Objects, generate_author()). + +-spec insert(object() | [object()], binary()) -> {revision(), [ref()]} | no_return(). +insert(Object, AuthorID) when not is_list(Object) -> + insert([Object], AuthorID); +insert(Objects, AuthorID) -> + Commit = [ + {insert, #domain_conf_v2_InsertOp{ + object = {Type, Object}, + force_ref = {Type, ForceRef} + }} + || {Type, {_ObjectName, ForceRef, Object}} <- Objects + ], + #domain_conf_v2_CommitResponse{version = Version, new_objects = NewObjects} = + commit(head(), Commit, AuthorID), + NewObjectsIDs = [ + {Tag, Ref} + || {Tag, {_ON, Ref, _Obj}} <- ordsets:to_list(NewObjects) + ], + {Version, NewObjectsIDs}. + +-spec update(object() | [object()]) -> revision() | no_return(). +update(NewObject) when not is_list(NewObject) -> + update(NewObject, generate_author()). + +-spec update(object() | [object()], binary()) -> revision() | no_return(). +update(Objects, AuthorID) -> + dmt_client:update(Objects, AuthorID). + +-spec upsert([object()]) -> revision() | no_return(). +upsert(Objects) -> + upsert(Objects, generate_author()). + +-spec upsert([object()], binary()) -> revision() | no_return(). +upsert(Objects, AuthorID) -> + dmt_client:upsert(Objects, AuthorID). + +-spec cleanup([ref()]) -> revision() | no_return(). +cleanup(Refs) -> + Commit = [ + {remove, #domain_conf_v2_RemoveOp{ + ref = Ref + }} + || Ref <- Refs + ], + #domain_conf_v2_CommitResponse{version = Version} = + commit(head(), Commit, generate_author()), + Version. + +generate_author() -> + dmt_client:create_author(genlib:unique(), genlib:unique()). diff --git a/apps/party_management/src/pm_globals.erl b/apps/party_management/src/pm_globals.erl new file mode 100644 index 00000000..e5c727fc --- /dev/null +++ b/apps/party_management/src/pm_globals.erl @@ -0,0 +1,16 @@ +-module(pm_globals). + +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). + +%% API +-export([reduce_globals/3]). + +-type globals() :: dmsl_domain_thrift:'Globals'(). +-type varset() :: pm_selector:varset(). +-type domain_revision() :: pm_domain:revision(). + +-spec reduce_globals(globals(), varset(), domain_revision()) -> globals(). +reduce_globals(Globals, VS, DomainRevision) -> + Globals#domain_Globals{ + external_account_set = pm_selector:reduce(Globals#domain_Globals.external_account_set, VS, DomainRevision) + }. diff --git a/apps/party_management/src/pm_maybe.erl b/apps/party_management/src/pm_maybe.erl new file mode 100644 index 00000000..cc1460fd --- /dev/null +++ b/apps/party_management/src/pm_maybe.erl @@ -0,0 +1,34 @@ +-module(pm_maybe). + +-export([apply/2]). +-export([apply/3]). + +-export([get_defined/1]). +-export([get_defined/2]). + +-type 'maybe'(T) :: + undefined | T. + +-export_type(['maybe'/1]). + +-spec apply(fun((T) -> U), 'maybe'(T)) -> 'maybe'(U). +apply(Fun, Arg) -> + pm_maybe:apply(Fun, Arg, undefined). + +-spec apply(fun((T) -> U), 'maybe'(T), Default) -> U | Default. +apply(Fun, Arg, _Default) when Arg =/= undefined -> + Fun(Arg); +apply(_Fun, undefined, Default) -> + Default. + +-spec get_defined(['maybe'(T)]) -> T | no_return(). +get_defined([]) -> + erlang:error(badarg); +get_defined([Value | _Tail]) when Value =/= undefined -> + Value; +get_defined([undefined | Tail]) -> + get_defined(Tail). + +-spec get_defined('maybe'(T), 'maybe'(T)) -> T | no_return(). +get_defined(V1, V2) -> + get_defined([V1, V2]). diff --git a/apps/party_management/src/pm_party.erl b/apps/party_management/src/pm_party.erl new file mode 100644 index 00000000..4ef2b315 --- /dev/null +++ b/apps/party_management/src/pm_party.erl @@ -0,0 +1,195 @@ +-module(pm_party). + +-include_lib("damsel/include/dmsl_payproc_thrift.hrl"). +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). + +%% Party support functions + +-export([get_term_set/2]). +-export([reduce_terms/3]). + +-export([get_shop_account/3]). +-export([get_wallet_account/3]). +-export([get_account_state/3]). + +%% + +-type party_ref() :: dmsl_domain_thrift:'PartyConfigRef'(). +-type termset_ref() :: dmsl_domain_thrift:'TermSetHierarchyRef'(). +-type shop_ref() :: dmsl_domain_thrift:'ShopConfigRef'(). +-type shop_account() :: dmsl_domain_thrift:'ShopAccount'(). +-type wallet_ref() :: dmsl_domain_thrift:'WalletConfigRef'(). +-type wallet_account() :: dmsl_domain_thrift:'WalletAccount'(). + +-type revision() :: pm_domain:revision(). + +%% Interface + +-spec get_shop_account(shop_ref(), party_ref(), revision()) -> shop_account() | no_return(). +get_shop_account(ShopRef, PartyRef, DomainRevision) -> + case pm_domain:find_party_with_shops_and_wallets(DomainRevision, PartyRef) of + {ok, _Party, Shops, _Wallets} -> + case lists:search(fun(#domain_ShopConfigObject{ref = Ref}) -> Ref =:= ShopRef end, Shops) of + {value, #domain_ShopConfigObject{data = #domain_ShopConfig{account = Account}}} -> + Account; + false -> + throw(#payproc_ShopNotFound{}) + end; + {error, notfound} -> + throw(#payproc_PartyNotFound{}) + end. + +-spec get_wallet_account(wallet_ref(), party_ref(), revision()) -> wallet_account() | no_return(). +get_wallet_account(WalletRef, PartyRef, DomainRevision) -> + case pm_domain:find_party_with_shops_and_wallets(DomainRevision, PartyRef) of + {ok, _Party, _Shops, Wallets} -> + case lists:search(fun(#domain_WalletConfigObject{ref = Ref}) -> Ref =:= WalletRef end, Wallets) of + {value, #domain_WalletConfigObject{data = #domain_WalletConfig{account = Account}}} -> + Account; + false -> + throw(#payproc_WalletNotFound{}) + end; + {error, notfound} -> + throw(#payproc_PartyNotFound{}) + end. + +-spec get_account_state(dmsl_accounter_thrift:'AccountID'(), party_ref(), revision()) -> + dmsl_payproc_thrift:'AccountState'() | no_return(). +get_account_state(AccountID, PartyRef, DomainRevision) -> + case pm_domain:find_party_with_shops_and_wallets(DomainRevision, PartyRef) of + {ok, _Party, Shops, Wallets} -> + ok = ensure_account(AccountID, Shops ++ Wallets), + Account = pm_accounting:get_account(AccountID), + #{currency_code := CurrencyCode} = Account, + Currency = pm_domain:get(pm_domain:head(), {currency, #domain_CurrencyRef{symbolic_code = CurrencyCode}}), + Balance = pm_accounting:get_balance(AccountID), + #{own_amount := OwnAmount, min_available_amount := MinAvailableAmount} = Balance, + #payproc_AccountState{ + account_id = AccountID, + own_amount = OwnAmount, + available_amount = MinAvailableAmount, + currency = Currency + }; + {error, notfound} -> + throw(#payproc_PartyNotFound{}) + end. + +%% Internals + +-spec reduce_terms(dmsl_domain_thrift:'TermSet'(), pm_selector:varset(), revision()) -> dmsl_domain_thrift:'TermSet'(). +reduce_terms(TermSet, VS, Revision) -> + reduce_terms(TermSet, {struct, struct, {dmsl_domain_thrift, 'TermSet'}}, VS, Revision). + +reduce_terms(undefined, _Type, _VS, _Revision) -> + undefined; +reduce_terms(Terms, Type, VS, Revision) -> + case is_terms(Type, Terms) of + true -> + reduce_terms_fields(Terms, Type, VS, Revision); + false -> + case is_selector(Type) of + true -> + pm_selector:reduce(Terms, VS, Revision); + false -> + case is_predicate(Type) of + true -> pm_selector:reduce_predicate(Terms, VS, Revision); + false -> error({unknown_reducee, Terms}) + end + end + end. + +%% Explicit Terms check here: since for predicates and selectors it's done in corresponding modules +is_terms({struct, struct, {dmsl_domain_thrift, Struct}}, Terms) when + Struct =:= 'TermSet'; + Struct =:= 'PaymentsServiceTerms'; + Struct =:= 'RecurrentPaytoolsServiceTerms'; + Struct =:= 'PaymentHoldsServiceTerms'; + Struct =:= 'PaymentRefundsServiceTerms'; + Struct =:= 'PartialRefundsServiceTerms'; + Struct =:= 'PaymentChargebackServiceTerms'; + Struct =:= 'PartialCaptureServiceTerms'; + Struct =:= 'ReportsServiceTerms'; + Struct =:= 'ServiceAcceptanceActsTerms'; + Struct =:= 'WalletServiceTerms'; + Struct =:= 'WithdrawalServiceTerms'; + Struct =:= 'W2WServiceTerms'; + Struct =:= 'PaymentAllocationServiceTerms' +-> + is_record(Terms, dmsl_domain_thrift:record_name(Struct)); +is_terms(_, _) -> + false. + +is_predicate({struct, union, {dmsl_domain_thrift, 'Predicate'}}) -> true; +is_predicate(_FieldType) -> false. + +is_selector({struct, union, {dmsl_domain_thrift, UnionName}}) -> + pm_utils:binary_ends_with(atom_to_binary(UnionName), <<"Selector">>); +is_selector(_) -> + false. + +reduce_terms_fields(Terms, Type, VS, Revision) -> + StructInfo = get_terms_struct_info(Type), + reduce_terms_fields(Terms, 2, StructInfo, VS, Revision). + +reduce_terms_fields(Terms, Idx, [{_, optional, Type, _Name, _} | Rest], VS, Revision) -> + Term = reduce_terms(element(Idx, Terms), Type, VS, Revision), + reduce_terms_fields(setelement(Idx, Terms, Term), Idx + 1, Rest, VS, Revision); +reduce_terms_fields(Terms, _Idx, [], _, _) -> + Terms. + +get_terms_struct_info(Type) -> + {struct, struct, {Mod, StructName}} = Type, + {struct, struct, StructInfo} = Mod:struct_info(StructName), + StructInfo. + +-spec get_term_set(termset_ref(), revision()) -> dmsl_domain_thrift:'TermSet'() | no_return(). +get_term_set(TermsRef, Revision) -> + case pm_domain:find(Revision, {term_set_hierarchy, TermsRef}) of + #domain_TermSetHierarchy{parent_terms = ParentRef, term_set = TermSet} -> + case ParentRef of + undefined -> + TermSet; + #domain_TermSetHierarchyRef{} -> + ParentTermSet = get_term_set(ParentRef, Revision), + merge_terms([ParentTermSet, TermSet]) + end; + notfound -> + throw(#payproc_TermSetHierarchyNotFound{}) + end. + +merge_terms(TermSets) when is_list(TermSets) -> + Type = {struct, struct, {dmsl_domain_thrift, 'TermSet'}}, + lists:foldl(fun(Left, Right) -> merge_terms(Left, Right, Type) end, undefined, TermSets). + +merge_terms(Left, Right, Type) when element(1, Left) == element(1, Right), tuple_size(Left) == tuple_size(Right) -> + case is_terms(Type, Left) of + false -> + %% Replace the value altogether + Left; + true -> + merge_terms_fields(Left, Right, Type) + end; +merge_terms(undefined, Right, _Type) -> + Right; +merge_terms(Left, _Right, _Type) -> + Left. + +merge_terms_fields(Left, Right, Type) -> + StructInfo = get_terms_struct_info(Type), + Target = setelement(1, erlang:make_tuple(tuple_size(Left), undefined), element(1, Left)), + merge_terms_fields(Target, Left, Right, 2, StructInfo). + +merge_terms_fields(Target, Left, Right, Idx, [{_, optional, Type, _Name, _} | Rest]) -> + Term = merge_terms(element(Idx, Left), element(Idx, Right), Type), + merge_terms_fields(setelement(Idx, Target, Term), Left, Right, Idx + 1, Rest); +merge_terms_fields(Target, _Left, _Right, _Idx, []) -> + Target. + +-define(SHOP_ACCOUNT(Account), {_Tag, _Ref, #domain_ShopConfig{account = Account}}). +-define(WALLET_ACCOUNT(Account), {_Tag, _Ref, #domain_WalletConfig{account = Account}}). + +ensure_account(_AccountID, []) -> throw(#payproc_AccountNotFound{}); +ensure_account(AccountID, [?SHOP_ACCOUNT(#domain_ShopAccount{settlement = AccountID}) | _]) -> ok; +ensure_account(AccountID, [?SHOP_ACCOUNT(#domain_ShopAccount{guarantee = AccountID}) | _]) -> ok; +ensure_account(AccountID, [?WALLET_ACCOUNT(#domain_WalletAccount{settlement = AccountID}) | _]) -> ok; +ensure_account(AccountID, [_ | Objects]) -> ensure_account(AccountID, Objects). diff --git a/apps/party_management/src/pm_party_handler.erl b/apps/party_management/src/pm_party_handler.erl new file mode 100644 index 00000000..466b6207 --- /dev/null +++ b/apps/party_management/src/pm_party_handler.erl @@ -0,0 +1,159 @@ +-module(pm_party_handler). + +-include_lib("damsel/include/dmsl_payproc_thrift.hrl"). +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). +-include_lib("damsel/include/dmsl_domain_conf_v2_thrift.hrl"). + +%% Woody handler called by pm_woody_wrapper + +-behaviour(pm_woody_wrapper). + +-export([handle_function/3]). + +%% + +-spec handle_function(woody:func(), woody:args(), pm_woody_wrapper:handler_opts()) -> term() | no_return(). +handle_function(Func, Args, Opts) -> + scoper:scope(partymgmt, fun() -> + handle_function_(Func, Args, Opts) + end). + +-spec handle_function_(woody:func(), woody:args(), pm_woody_wrapper:handler_opts()) -> term() | no_return(). +%% Accounts +handle_function_('GetShopAccountSimple', {PartyRef, ShopRef}, Opts) -> + DomainRevision = dmt_client:get_latest_version(), + handle_function('GetShopAccount', {PartyRef, ShopRef, DomainRevision}, Opts); +handle_function_('GetWalletAccountSimple', {PartyRef, WalletRef}, Opts) -> + DomainRevision = dmt_client:get_latest_version(), + handle_function_('GetWalletAccount', {PartyRef, WalletRef, DomainRevision}, Opts); +handle_function_('GetAccountStateSimple', {PartyRef, AccountID}, Opts) -> + DomainRevision = dmt_client:get_latest_version(), + handle_function_('GetAccountState', {PartyRef, AccountID, DomainRevision}, Opts); +handle_function_('GetShopAccount', {PartyRef, ShopRef, DomainRevision}, _Opts) -> + _ = set_party_mgmt_meta(PartyRef), + pm_party:get_shop_account(ShopRef, PartyRef, DomainRevision); +handle_function_('GetWalletAccount', {PartyRef, WalletRef, DomainRevision}, _Opts) -> + _ = set_party_mgmt_meta(PartyRef), + pm_party:get_wallet_account(WalletRef, PartyRef, DomainRevision); +handle_function_('GetAccountState', {PartyRef, AccountID, DomainRevision}, _Opts) -> + _ = set_party_mgmt_meta(PartyRef), + pm_party:get_account_state(AccountID, PartyRef, DomainRevision); +%% Providers +handle_function_('ComputeProvider', Args, _Opts) -> + {ProviderRef, DomainRevision, Varset} = Args, + Provider = get_provider(ProviderRef, DomainRevision), + VS = pm_varset:decode_varset(Varset), + ComputedProvider = pm_provider:reduce_provider(Provider, VS, DomainRevision), + _ = assert_provider_reduced(ComputedProvider), + ComputedProvider; +handle_function_('ComputeProviderTerminalTerms', Args, _Opts) -> + {ProviderRef, TerminalRef, DomainRevision, Varset} = Args, + Provider = get_provider(ProviderRef, DomainRevision), + Terminal = get_terminal(TerminalRef, DomainRevision), + VS = pm_varset:decode_varset(Varset), + Terms = pm_provider:reduce_provider_terminal_terms(Provider, Terminal, VS, DomainRevision), + _ = assert_provider_terms_reduced(Terms), + Terms; +handle_function_('ComputeProviderTerminal', {TerminalRef, DomainRevision, VarsetIn}, _Opts) -> + Terminal = get_terminal(TerminalRef, DomainRevision), + ProviderRef = Terminal#domain_Terminal.provider_ref, + Provider = get_provider(ProviderRef, DomainRevision), + Proxy = pm_provider:compute_proxy(Provider, Terminal, DomainRevision), + ComputedTerms = pm_maybe:apply( + fun(Varset) -> + VS = pm_varset:decode_varset(Varset), + pm_provider:reduce_provider_terminal_terms(Provider, Terminal, VS, DomainRevision) + end, + VarsetIn + ), + #payproc_ProviderTerminal{ + ref = TerminalRef, + name = Terminal#domain_Terminal.name, + description = Terminal#domain_Terminal.description, + proxy = Proxy, + provider = #payproc_ProviderDetails{ + ref = ProviderRef, + name = Provider#domain_Provider.name, + description = Provider#domain_Provider.description + }, + terms = ComputedTerms + }; +%% Globals +handle_function_('ComputeGlobals', Args, _Opts) -> + {DomainRevision, Varset} = Args, + Globals = get_globals(DomainRevision), + VS = pm_varset:decode_varset(Varset), + pm_globals:reduce_globals(Globals, VS, DomainRevision); +%% RuleSets +handle_function_('ComputeRoutingRuleset', Args, _Opts) -> + {RuleSetRef, DomainRevision, Varset} = Args, + RuleSet = get_payment_routing_ruleset(RuleSetRef, DomainRevision), + VS = pm_varset:decode_varset(Varset), + pm_ruleset:reduce_payment_routing_ruleset(RuleSet, VS, DomainRevision); +%% Payment Institutions +handle_function_('ComputePaymentInstitution', Args, _Opts) -> + {PaymentInstitutionRef, DomainRevision, Varset} = Args, + PaymentInstitution = get_payment_institution(PaymentInstitutionRef, DomainRevision), + VS = pm_varset:decode_varset(Varset), + pm_payment_institution:reduce_payment_institution(PaymentInstitution, VS, DomainRevision); +handle_function_('ComputeTerms', Args, _Opts) -> + {Ref, Revision, Varset} = Args, + VS = pm_varset:decode_varset(Varset), + Terms = pm_party:get_term_set(Ref, Revision), + pm_party:reduce_terms(Terms, VS, Revision). + +%% + +assert_provider_reduced(#domain_Provider{terms = Terms}) -> + assert_provider_terms_reduced(Terms). + +assert_provider_terms_reduced(#domain_ProvisionTermSet{}) -> + ok; +assert_provider_terms_reduced(undefined) -> + throw(#payproc_ProvisionTermSetUndefined{}). + +set_party_mgmt_meta(#domain_PartyConfigRef{id = PartyID}) -> + scoper:add_meta(#{party_id => PartyID}); +set_party_mgmt_meta(_PartyRef) -> + ok. + +get_payment_institution(PaymentInstitutionRef, Revision) -> + case pm_domain:find(Revision, {payment_institution, PaymentInstitutionRef}) of + #domain_PaymentInstitution{} = P -> + P; + notfound -> + throw(#payproc_PaymentInstitutionNotFound{}) + end. + +get_provider(ProviderRef, DomainRevision) -> + try + pm_domain:get(DomainRevision, {provider, ProviderRef}) + catch + error:{object_not_found, {DomainRevision, {provider, ProviderRef}}} -> + throw(#payproc_ProviderNotFound{}) + end. + +get_terminal(TerminalRef, DomainRevision) -> + try + pm_domain:get(DomainRevision, {terminal, TerminalRef}) + catch + error:{object_not_found, {DomainRevision, {terminal, TerminalRef}}} -> + throw(#payproc_TerminalNotFound{}) + end. + +get_globals(DomainRevision) -> + Globals = {globals, #domain_GlobalsRef{}}, + try + pm_domain:get(DomainRevision, Globals) + catch + error:{object_not_found, {DomainRevision, Globals}} -> + throw(#payproc_GlobalsNotFound{}) + end. + +get_payment_routing_ruleset(RuleSetRef, DomainRevision) -> + try + pm_domain:get(DomainRevision, {routing_rules, RuleSetRef}) + catch + error:{object_not_found, {DomainRevision, {routing_rules, RuleSetRef}}} -> + throw(#payproc_RuleSetNotFound{}) + end. diff --git a/apps/party_management/src/pm_payment_institution.erl b/apps/party_management/src/pm_payment_institution.erl new file mode 100644 index 00000000..7bed1d27 --- /dev/null +++ b/apps/party_management/src/pm_payment_institution.erl @@ -0,0 +1,68 @@ +-module(pm_payment_institution). + +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). + +%% + +-export([reduce_payment_institution/3]). +-export([get_system_account/4]). +-export([get_realm/1]). +-export([is_live/1]). + +%% + +-type currency() :: dmsl_domain_thrift:'CurrencyRef'(). +-type varset() :: pm_selector:varset(). +-type revision() :: pm_domain:revision(). +-type payment_inst() :: dmsl_domain_thrift:'PaymentInstitution'(). +-type realm() :: dmsl_domain_thrift:'PaymentInstitutionRealm'(). + +%% + +-spec reduce_payment_institution(payment_inst(), varset(), revision()) -> payment_inst(). +reduce_payment_institution(PaymentInstitution, VS, Revision) -> + PaymentInstitution#domain_PaymentInstitution{ + system_account_set = reduce_if_defined( + PaymentInstitution#domain_PaymentInstitution.system_account_set, + VS, + Revision + ), + inspector = reduce_if_defined( + PaymentInstitution#domain_PaymentInstitution.inspector, + VS, + Revision + ), + wallet_system_account_set = reduce_if_defined( + PaymentInstitution#domain_PaymentInstitution.wallet_system_account_set, + VS, + Revision + ), + payment_system = reduce_if_defined( + PaymentInstitution#domain_PaymentInstitution.payment_system, + VS, + Revision + ) + }. + +-spec get_system_account(currency(), varset(), revision(), payment_inst()) -> + dmsl_domain_thrift:'SystemAccount'() | no_return(). +get_system_account(Currency, VS, Revision, #domain_PaymentInstitution{system_account_set = S}) -> + SystemAccountSetRef = pm_selector:reduce_to_value(S, VS, Revision), + SystemAccountSet = pm_domain:get(Revision, {system_account_set, SystemAccountSetRef}), + case maps:find(Currency, SystemAccountSet#domain_SystemAccountSet.accounts) of + {ok, Account} -> + Account; + error -> + error({misconfiguration, {'No system account for a given currency', Currency}}) + end. + +-spec get_realm(payment_inst()) -> realm(). +get_realm(#domain_PaymentInstitution{realm = Realm}) -> + Realm. + +-spec is_live(payment_inst()) -> boolean(). +is_live(#domain_PaymentInstitution{realm = Realm}) -> + Realm =:= live. + +reduce_if_defined(Selector, VS, Rev) -> + pm_maybe:apply(fun(X) -> pm_selector:reduce(X, VS, Rev) end, Selector). diff --git a/apps/party_management/src/pm_payment_tool.erl b/apps/party_management/src/pm_payment_tool.erl new file mode 100644 index 00000000..5e3d0dff --- /dev/null +++ b/apps/party_management/src/pm_payment_tool.erl @@ -0,0 +1,392 @@ +%%% Payment tools + +-module(pm_payment_tool). + +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). +-include_lib("damsel/include/dmsl_base_thrift.hrl"). + +%% + +-export([create_from_method/1]). +-export([test_condition/3]). + +%% + +-type t() :: dmsl_domain_thrift:'PaymentTool'(). +-type method() :: dmsl_domain_thrift:'PaymentMethodRef'(). +-type condition() :: dmsl_domain_thrift:'PaymentToolCondition'(). + +-import(pm_condition, [some_defined/1, ternary_and/1, ternary_while/1]). + +-spec create_from_method(method()) -> t(). +%% TODO empty strings - ugly hack for dialyzar +create_from_method(#domain_PaymentMethodRef{ + id = + {bank_card, #domain_BankCardPaymentMethod{ + payment_system = PaymentSystem, + is_cvv_empty = IsCVVEmpty, + payment_token = PaymentToken, + tokenization_method = TokenizationMethod + }} +}) -> + {bank_card, #domain_BankCard{ + token = <<"">>, + payment_system = PaymentSystem, + bin = <<"">>, + last_digits = <<"">>, + payment_token = PaymentToken, + tokenization_method = TokenizationMethod, + is_cvv_empty = IsCVVEmpty + }}; +create_from_method(#domain_PaymentMethodRef{id = {payment_terminal, Ref}}) -> + {payment_terminal, #domain_PaymentTerminal{payment_service = Ref}}; +create_from_method(#domain_PaymentMethodRef{id = {digital_wallet, Ref}}) -> + {digital_wallet, #domain_DigitalWallet{ + payment_service = Ref, + id = <<"">> + }}; +create_from_method(#domain_PaymentMethodRef{id = {mobile, Ref}}) -> + {mobile_commerce, #domain_MobileCommerce{ + operator = Ref, + phone = #domain_MobilePhone{ + cc = <<"">>, + ctn = <<"">> + } + }}; +create_from_method(#domain_PaymentMethodRef{id = {crypto_currency, Ref}}) -> + {crypto_currency, Ref}; +create_from_method(#domain_PaymentMethodRef{id = {generic, Generic}}) -> + {generic, #domain_GenericPaymentTool{ + payment_service = Generic#domain_GenericPaymentMethod.payment_service + }}. + +%% + +-spec test_condition(condition(), t(), pm_domain:revision()) -> boolean() | undefined. +test_condition({bank_card, C}, {bank_card, V = #domain_BankCard{}}, Rev) -> + test_bank_card_condition(C, V, Rev); +test_condition({payment_terminal, C}, {payment_terminal, V = #domain_PaymentTerminal{}}, _Rev) -> + test_payment_terminal_condition(C, V); +test_condition({digital_wallet, C}, {digital_wallet, V = #domain_DigitalWallet{}}, _Rev) -> + test_digital_wallet_condition(C, V); +test_condition({crypto_currency, C}, {crypto_currency, V}, _Rev) -> + test_crypto_currency_condition(C, {ref, V}); +test_condition({mobile_commerce, C}, {mobile_commerce, V}, _Rev) -> + test_mobile_commerce_condition(C, V); +test_condition({generic, C}, {generic, V}, _Rev) -> + test_generic_condition(C, V); +test_condition(_PaymentTool, _Condition, _Rev) -> + false. + +test_bank_card_condition(#domain_BankCardCondition{definition = Def}, V, Rev) when Def /= undefined -> + test_bank_card_condition_def(Def, V, Rev); +test_bank_card_condition(#domain_BankCardCondition{}, _, _Rev) -> + true. + +test_bank_card_condition_def({payment_system, PaymentSystem}, V, _Rev) -> + test_payment_system_condition(PaymentSystem, V); +test_bank_card_condition_def({issuer_country_is, IssuerCountry}, V, _Rev) -> + test_issuer_country_condition(IssuerCountry, V); +test_bank_card_condition_def({issuer_bank_is, BankRef}, V, Rev) -> + test_issuer_bank_condition(BankRef, V, Rev); +test_bank_card_condition_def({category_is, CategoryRef}, V, Rev) -> + test_bank_card_category_condition(CategoryRef, V, Rev); +test_bank_card_condition_def( + {empty_cvv_is, Val}, + #domain_BankCard{is_cvv_empty = Val}, + _Rev +) -> + true; +%% Для обратной совместимости с картами, у которых нет is_cvv_empty +test_bank_card_condition_def( + {empty_cvv_is, false}, + #domain_BankCard{is_cvv_empty = undefined}, + _Rev +) -> + true; +test_bank_card_condition_def({empty_cvv_is, _Val}, #domain_BankCard{}, _Rev) -> + false. + +test_payment_system_condition( + #domain_PaymentSystemCondition{ + payment_system_is = PsIs, + token_service_is = TpIs, + tokenization_method_is = TmIs + }, + #domain_BankCard{ + payment_system = Ps, + payment_token = Tp, + tokenization_method = Tm + } +) -> + ternary_and([ + some_defined([PsIs, TpIs, TmIs]), + PsIs == undefined orelse PsIs == Ps, + TpIs == undefined orelse TpIs == Tp, + TmIs == undefined orelse ternary_while([Tm, TmIs == Tm]) + ]). + +test_issuer_country_condition(Country, #domain_BankCard{issuer_country = TargetCountry}) -> + ternary_while([TargetCountry, Country == TargetCountry]). + +test_issuer_bank_condition(BankRef, #domain_BankCard{bank_name = BankName, bin = BIN}, Rev) -> + #domain_Bank{binbase_id_patterns = Patterns, bins = BINs} = pm_domain:get(Rev, {bank, BankRef}), + case {Patterns, BankName} of + {P, B} when is_list(P) and is_binary(B) -> + test_bank_card_patterns(Patterns, BankName); + % TODO т.к. BinBase не обладает полным объемом данных, при их отсутствии мы возвращаемся к проверкам по бинам. + % B будущем стоит избавиться от этого. + {_, _} -> + test_bank_card_bins(BIN, BINs) + end. + +test_bank_card_category_condition(CategoryRef, #domain_BankCard{category = Category}, Rev) -> + #domain_BankCardCategory{ + category_patterns = Patterns + } = pm_domain:get(Rev, {bank_card_category, CategoryRef}), + test_bank_card_patterns(Patterns, Category). + +test_bank_card_bins(BIN, BINs) -> + ordsets:is_element(BIN, BINs). + +test_bank_card_patterns(Patterns, BankName) -> + Matches = ordsets:filter(fun(E) -> genlib_wildcard:match(BankName, E) end, Patterns), + ordsets:size(Matches) > 0. + +test_payment_terminal_condition(#domain_PaymentTerminalCondition{definition = Def}, V) -> + Def =:= undefined orelse test_payment_terminal_condition_def(Def, V). + +test_payment_terminal_condition_def( + {payment_service_is, Ps1}, + #domain_PaymentTerminal{payment_service = Ps2} +) -> + Ps1 =:= Ps2; +test_payment_terminal_condition_def(_Cond, _Data) -> + false. + +test_digital_wallet_condition(#domain_DigitalWalletCondition{definition = Def}, V) -> + Def =:= undefined orelse test_digital_wallet_condition_def(Def, V). + +test_digital_wallet_condition_def( + {payment_service_is, Ps1}, + #domain_DigitalWallet{payment_service = Ps2} +) -> + Ps1 =:= Ps2; +test_digital_wallet_condition_def(_Cond, _Data) -> + false. + +test_crypto_currency_condition(#domain_CryptoCurrencyCondition{definition = Def}, V) -> + Def =:= undefined orelse test_crypto_currency_condition_def(Def, V). + +test_crypto_currency_condition_def({crypto_currency_is, C1}, {ref, C2}) -> + C1 =:= C2; +test_crypto_currency_condition_def(_Cond, _Data) -> + false. + +test_mobile_commerce_condition(#domain_MobileCommerceCondition{definition = Def}, V) -> + Def =:= undefined orelse test_mobile_commerce_condition_def(Def, V). + +test_mobile_commerce_condition_def( + {operator_is, C1}, + #domain_MobileCommerce{operator = C2} +) -> + C1 =:= C2; +test_mobile_commerce_condition_def(_Cond, _Data) -> + false. + +test_generic_condition({resource_field_matches, _}, #domain_GenericPaymentTool{data = undefined}) -> + false; +test_generic_condition( + {resource_field_matches, #domain_GenericResourceCondition{field_path = Path, value = Value}}, + #domain_GenericPaymentTool{data = #base_Content{type = Type, data = Data}} +) -> + case decode_content(Type, Data) of + {ok, Result} -> + case get_field_by_path(Path, Result) of + {ok, Value} -> true; + _ -> false + end; + _ -> + false + end; +test_generic_condition({payment_service_is, Ref1}, #domain_GenericPaymentTool{payment_service = Ref2}) -> + Ref1 =:= Ref2; +test_generic_condition(_Cond, _Data) -> + false. + +decode_content(<<"application/schema-instance+json; schema=", _/binary>>, Data) -> + {ok, jsx:decode(Data)}; +decode_content(<<"application/json">>, Data) -> + {ok, jsx:decode(Data)}; +decode_content(Type, _Data) -> + {error, {unsupported, Type}}. + +get_field_by_path([], Data) -> + {ok, Data}; +get_field_by_path([Key | Path], Data) -> + case maps:get(Key, Data, undefined) of + undefined -> {error, notfound}; + Value -> get_field_by_path(Path, Value) + end. + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +-spec test() -> _. + +%% In order to test nonsense condition +-dialyzer({nowarn_function, test_condition_test/0}). +-spec test_condition_test() -> _. +test_condition_test() -> + RevisionUnused = 1, + PaymentSystemRef = #domain_PaymentSystemRef{id = <<"id">>}, + BankCardTokenServiceRef = #domain_BankCardTokenServiceRef{id = <<"id">>}, + + %% BankCard + ?assertEqual( + true, + test_condition( + {bank_card, #domain_BankCardCondition{}}, + {bank_card, #domain_BankCard{}}, + RevisionUnused + ) + ), + ?assertEqual( + true, + test_condition( + {bank_card, #domain_BankCardCondition{ + definition = + {payment_system, #domain_PaymentSystemCondition{ + payment_system_is = PaymentSystemRef + }} + }}, + {bank_card, #domain_BankCard{payment_system = PaymentSystemRef}}, + RevisionUnused + ) + ), + ?assertEqual( + true, + test_condition( + {bank_card, #domain_BankCardCondition{ + definition = + {payment_system, #domain_PaymentSystemCondition{ + token_service_is = BankCardTokenServiceRef + }} + }}, + {bank_card, #domain_BankCard{payment_token = BankCardTokenServiceRef}}, + RevisionUnused + ) + ), + ?assertEqual( + true, + test_condition( + {bank_card, #domain_BankCardCondition{ + definition = + {payment_system, #domain_PaymentSystemCondition{ + tokenization_method_is = dpan + }} + }}, + {bank_card, #domain_BankCard{tokenization_method = dpan}}, + RevisionUnused + ) + ), + ?assertEqual( + true, + test_condition( + {bank_card, #domain_BankCardCondition{definition = {issuer_country_is, 'RUS'}}}, + {bank_card, #domain_BankCard{issuer_country = 'RUS'}}, + RevisionUnused + ) + ), + ?assertEqual( + true, + test_condition( + {bank_card, #domain_BankCardCondition{definition = {empty_cvv_is, true}}}, + {bank_card, #domain_BankCard{is_cvv_empty = true}}, + RevisionUnused + ) + ), + ?assertEqual( + false, + test_condition( + {bank_card, #domain_BankCardCondition{definition = {empty_cvv_is, true}}}, + {bank_card, #domain_BankCard{}}, + RevisionUnused + ) + ), + + PaymentServiceRef = #domain_PaymentServiceRef{id = <<"id">>}, + %% PaymentTerminal + ?assertEqual( + true, + test_condition( + {payment_terminal, #domain_PaymentTerminalCondition{definition = {payment_service_is, PaymentServiceRef}}}, + {payment_terminal, #domain_PaymentTerminal{payment_service = PaymentServiceRef}}, + RevisionUnused + ) + ), + ?assertEqual( + false, + test_condition( + {payment_terminal, #domain_PaymentTerminalCondition{definition = nonsense}}, + {payment_terminal, #domain_PaymentTerminal{}}, + RevisionUnused + ) + ), + + %% DigitalWallet + ?assertEqual( + true, + test_condition( + {digital_wallet, #domain_DigitalWalletCondition{definition = {payment_service_is, PaymentServiceRef}}}, + {digital_wallet, #domain_DigitalWallet{payment_service = PaymentServiceRef}}, + RevisionUnused + ) + ), + ?assertEqual( + false, + test_condition( + {digital_wallet, #domain_DigitalWalletCondition{definition = nonsense}}, + {digital_wallet, #domain_DigitalWallet{}}, + RevisionUnused + ) + ), + + %% MobileCommerce + MobileOperatorRef = #domain_MobileOperatorRef{id = <<"id">>}, + ?assertEqual( + true, + test_condition( + {mobile_commerce, #domain_MobileCommerceCondition{definition = {operator_is, MobileOperatorRef}}}, + {mobile_commerce, #domain_MobileCommerce{operator = MobileOperatorRef}}, + RevisionUnused + ) + ), + ?assertEqual( + false, + test_condition( + {mobile_commerce, #domain_MobileCommerceCondition{definition = nonsense}}, + {mobile_commerce, #domain_MobileCommerce{}}, + RevisionUnused + ) + ), + + %% Generic + ?assertEqual( + true, + test_condition( + {generic, {payment_service_is, PaymentServiceRef}}, + {generic, #domain_GenericPaymentTool{payment_service = PaymentServiceRef}}, + RevisionUnused + ) + ), + ?assertEqual( + false, + test_condition( + {generic, nonsense}, + {generic, #domain_GenericPaymentTool{}}, + RevisionUnused + ) + ). + +-endif. diff --git a/apps/party_management/src/pm_provider.erl b/apps/party_management/src/pm_provider.erl new file mode 100644 index 00000000..a11730fc --- /dev/null +++ b/apps/party_management/src/pm_provider.erl @@ -0,0 +1,335 @@ +-module(pm_provider). + +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). +-include_lib("damsel/include/dmsl_payproc_thrift.hrl"). +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). + +%% API +-export([reduce_provider/3]). +-export([reduce_provider_terminal_terms/4]). + +-export([compute_proxy/3]). + +-type provider() :: dmsl_domain_thrift:'Provider'(). +-type terminal() :: dmsl_domain_thrift:'Terminal'(). +-type provision_terms() :: dmsl_domain_thrift:'ProvisionTermSet'(). +-type varset() :: pm_selector:varset(). +-type domain_revision() :: pm_domain:revision(). + +-spec reduce_provider(provider(), varset(), domain_revision()) -> provider(). +reduce_provider(Provider, VS, Rev) -> + Provider#domain_Provider{ + terms = reduce_provision_term_set(Provider#domain_Provider.terms, VS, Rev) + }. + +-spec reduce_provider_terminal_terms(provider(), terminal(), varset(), domain_revision()) -> + provision_terms() | undefined. +reduce_provider_terminal_terms(Provider, Terminal, VS, Rev) -> + ProviderTerms = Provider#domain_Provider.terms, + TerminalTerms = Terminal#domain_Terminal.terms, + MergedTerms = merge_provision_term_sets(ProviderTerms, TerminalTerms), + reduce_provision_term_set(MergedTerms, VS, Rev). + +reduce_withdrawal_terms(undefined = Terms, _VS, _Rev) -> + Terms; +reduce_withdrawal_terms(#domain_WithdrawalProvisionTerms{} = Terms, VS, Rev) -> + Terms#domain_WithdrawalProvisionTerms{ + allow = reduce_predicate_if_defined(Terms#domain_WithdrawalProvisionTerms.allow, VS, Rev), + global_allow = reduce_predicate_if_defined(Terms#domain_WithdrawalProvisionTerms.global_allow, VS, Rev), + currencies = reduce_if_defined(Terms#domain_WithdrawalProvisionTerms.currencies, VS, Rev), + cash_limit = reduce_if_defined(Terms#domain_WithdrawalProvisionTerms.cash_limit, VS, Rev), + cash_flow = reduce_if_defined(Terms#domain_WithdrawalProvisionTerms.cash_flow, VS, Rev), + turnover_limit = reduce_if_defined(Terms#domain_WithdrawalProvisionTerms.turnover_limit, VS, Rev) + }. + +reduce_provision_term_set(undefined = ProvisionTermSet, _VS, _DomainRevision) -> + ProvisionTermSet; +reduce_provision_term_set(ProvisionTermSet, VS, DomainRevision) -> + #domain_ProvisionTermSet{ + payments = pm_maybe:apply( + fun(X) -> reduce_payment_terms(X, VS, DomainRevision) end, + ProvisionTermSet#domain_ProvisionTermSet.payments + ), + recurrent_paytools = pm_maybe:apply( + fun(X) -> reduce_recurrent_paytool_terms(X, VS, DomainRevision) end, + ProvisionTermSet#domain_ProvisionTermSet.recurrent_paytools + ), + wallet = pm_maybe:apply( + fun(X) -> reduce_wallet_provision(X, VS, DomainRevision) end, + ProvisionTermSet#domain_ProvisionTermSet.wallet + ), + extension = ProvisionTermSet#domain_ProvisionTermSet.extension + }. + +reduce_payment_terms(undefined = PaymentTerms, _VS, _DomainRevision) -> + PaymentTerms; +reduce_payment_terms(PaymentTerms, VS, DomainRevision) -> + PaymentTerms#domain_PaymentsProvisionTerms{ + allow = reduce_predicate_if_defined(PaymentTerms#domain_PaymentsProvisionTerms.allow, VS, DomainRevision), + global_allow = + reduce_predicate_if_defined(PaymentTerms#domain_PaymentsProvisionTerms.global_allow, VS, DomainRevision), + currencies = reduce_if_defined(PaymentTerms#domain_PaymentsProvisionTerms.currencies, VS, DomainRevision), + categories = reduce_if_defined(PaymentTerms#domain_PaymentsProvisionTerms.categories, VS, DomainRevision), + payment_methods = reduce_if_defined( + PaymentTerms#domain_PaymentsProvisionTerms.payment_methods, + VS, + DomainRevision + ), + cash_limit = reduce_if_defined(PaymentTerms#domain_PaymentsProvisionTerms.cash_limit, VS, DomainRevision), + cash_flow = reduce_if_defined(PaymentTerms#domain_PaymentsProvisionTerms.cash_flow, VS, DomainRevision), + holds = pm_maybe:apply( + fun(X) -> reduce_payment_hold_terms(X, VS, DomainRevision) end, + PaymentTerms#domain_PaymentsProvisionTerms.holds + ), + refunds = pm_maybe:apply( + fun(X) -> reduce_payment_refund_terms(X, VS, DomainRevision) end, + PaymentTerms#domain_PaymentsProvisionTerms.refunds + ), + chargebacks = pm_maybe:apply( + fun(X) -> reduce_payment_chargeback_terms(X, VS, DomainRevision) end, + PaymentTerms#domain_PaymentsProvisionTerms.chargebacks + ), + risk_coverage = reduce_if_defined(PaymentTerms#domain_PaymentsProvisionTerms.risk_coverage, VS, DomainRevision), + turnover_limits = + reduce_if_defined(PaymentTerms#domain_PaymentsProvisionTerms.turnover_limits, VS, DomainRevision), + allow_exchange = + reduce_predicate_if_defined(PaymentTerms#domain_PaymentsProvisionTerms.allow_exchange, VS, DomainRevision) + }. + +reduce_payment_hold_terms(PaymentHoldTerms, VS, DomainRevision) -> + PaymentHoldTerms#domain_PaymentHoldsProvisionTerms{ + lifetime = reduce_if_defined(PaymentHoldTerms#domain_PaymentHoldsProvisionTerms.lifetime, VS, DomainRevision), + partial_captures = pm_maybe:apply( + fun(X) -> reduce_partial_captures_terms(X, VS, DomainRevision) end, + PaymentHoldTerms#domain_PaymentHoldsProvisionTerms.partial_captures + ) + }. + +reduce_partial_captures_terms(#domain_PartialCaptureProvisionTerms{} = Terms, _VS, _DomainRevision) -> + Terms. + +reduce_payment_refund_terms(PaymentRefundTerms, VS, DomainRevision) -> + PaymentRefundTerms#domain_PaymentRefundsProvisionTerms{ + cash_flow = reduce_if_defined( + PaymentRefundTerms#domain_PaymentRefundsProvisionTerms.cash_flow, + VS, + DomainRevision + ), + partial_refunds = pm_maybe:apply( + fun(X) -> reduce_partial_refunds_terms(X, VS, DomainRevision) end, + PaymentRefundTerms#domain_PaymentRefundsProvisionTerms.partial_refunds + ) + }. + +reduce_partial_refunds_terms(PartialRefundTerms, VS, DomainRevision) -> + PartialRefundTerms#domain_PartialRefundsProvisionTerms{ + cash_limit = reduce_if_defined( + PartialRefundTerms#domain_PartialRefundsProvisionTerms.cash_limit, + VS, + DomainRevision + ) + }. + +reduce_payment_chargeback_terms(PaymentChargebackTerms, VS, DomainRevision) -> + PaymentChargebackTerms#domain_PaymentChargebackProvisionTerms{ + cash_flow = reduce_if_defined( + PaymentChargebackTerms#domain_PaymentChargebackProvisionTerms.cash_flow, + VS, + DomainRevision + ) + }. + +reduce_recurrent_paytool_terms(RecurrentPaytoolTerms, VS, DomainRevision) -> + RecurrentPaytoolTerms#domain_RecurrentPaytoolsProvisionTerms{ + cash_value = reduce_if_defined( + RecurrentPaytoolTerms#domain_RecurrentPaytoolsProvisionTerms.cash_value, + VS, + DomainRevision + ), + categories = reduce_if_defined( + RecurrentPaytoolTerms#domain_RecurrentPaytoolsProvisionTerms.categories, + VS, + DomainRevision + ), + payment_methods = reduce_if_defined( + RecurrentPaytoolTerms#domain_RecurrentPaytoolsProvisionTerms.payment_methods, + VS, + DomainRevision + ) + }. + +reduce_wallet_provision(WalletProvisionTerms, VS, DomainRevision) -> + #domain_WalletProvisionTerms{ + turnover_limit = reduce_if_defined( + WalletProvisionTerms#domain_WalletProvisionTerms.turnover_limit, + VS, + DomainRevision + ), + withdrawals = pm_maybe:apply( + fun(X) -> reduce_withdrawal_terms(X, VS, DomainRevision) end, + WalletProvisionTerms#domain_WalletProvisionTerms.withdrawals + ) + }. + +merge_provision_term_sets( + #domain_ProvisionTermSet{ + payments = PPayments, + recurrent_paytools = PRecurrents, + wallet = PWallet, + extension = PExtension + }, + #domain_ProvisionTermSet{ + payments = TPayments, + % TODO: Allow to define recurrent terms in terminal + recurrent_paytools = _TRecurrents, + wallet = TWallet, + extension = TExtension + } +) -> + #domain_ProvisionTermSet{ + payments = merge_payment_terms(PPayments, TPayments), + recurrent_paytools = PRecurrents, + wallet = merge_wallet_terms(PWallet, TWallet), + extension = merge_extension_terms(PExtension, TExtension) + }; +merge_provision_term_sets(ProviderTerms, TerminalTerms) -> + pm_utils:select_defined(TerminalTerms, ProviderTerms). + +merge_payment_terms( + #domain_PaymentsProvisionTerms{ + allow = PAllow, + global_allow = PGAllow, + currencies = PCurrencies, + categories = PCategories, + payment_methods = PPaymentMethods, + cash_limit = PCashLimit, + cash_flow = PCashflow, + holds = PHolds, + refunds = PRefunds, + chargebacks = PChargebacks, + risk_coverage = PRiskCoverage, + turnover_limits = PTurnoverLimits, + allow_exchange = PAllowExchange + }, + #domain_PaymentsProvisionTerms{ + allow = TAllow, + global_allow = TGAllow, + currencies = TCurrencies, + categories = TCategories, + payment_methods = TPaymentMethods, + cash_limit = TCashLimit, + cash_flow = TCashflow, + holds = THolds, + refunds = TRefunds, + chargebacks = TChargebacks, + risk_coverage = TRiskCoverage, + turnover_limits = TTurnoverLimits, + allow_exchange = TAllowExchange + } +) -> + #domain_PaymentsProvisionTerms{ + allow = pm_utils:select_defined(TAllow, PAllow), + global_allow = pm_utils:select_defined(TGAllow, PGAllow), + currencies = pm_utils:select_defined(TCurrencies, PCurrencies), + categories = pm_utils:select_defined(TCategories, PCategories), + payment_methods = pm_utils:select_defined(TPaymentMethods, PPaymentMethods), + cash_limit = pm_utils:select_defined(TCashLimit, PCashLimit), + cash_flow = pm_utils:select_defined(TCashflow, PCashflow), + holds = pm_utils:select_defined(THolds, PHolds), + refunds = pm_utils:select_defined(TRefunds, PRefunds), + chargebacks = pm_utils:select_defined(TChargebacks, PChargebacks), + risk_coverage = pm_utils:select_defined(TRiskCoverage, PRiskCoverage), + turnover_limits = pm_utils:select_defined(TTurnoverLimits, PTurnoverLimits), + allow_exchange = pm_utils:select_defined(TAllowExchange, PAllowExchange) + }; +merge_payment_terms(ProviderTerms, TerminalTerms) -> + pm_utils:select_defined(TerminalTerms, ProviderTerms). + +merge_wallet_terms( + #domain_WalletProvisionTerms{ + turnover_limit = PLimit, + withdrawals = PWithdrawal + }, + #domain_WalletProvisionTerms{ + turnover_limit = TLimit, + withdrawals = TWithdrawal + } +) -> + #domain_WalletProvisionTerms{ + turnover_limit = pm_utils:select_defined(TLimit, PLimit), + withdrawals = merge_withdrawal_terms(PWithdrawal, TWithdrawal) + }; +merge_wallet_terms(ProviderTerms, TerminalTerms) -> + pm_utils:select_defined(TerminalTerms, ProviderTerms). + +merge_extension_terms( + #domain_ExtendedProvisionTerms{ + skip_recurrent = PSkipRecurrent + }, + #domain_ExtendedProvisionTerms{ + skip_recurrent = TSkipRecurrent + } +) -> + #domain_ExtendedProvisionTerms{ + skip_recurrent = pm_utils:select_defined(TSkipRecurrent, PSkipRecurrent) + }; +merge_extension_terms(ProviderTerms, TerminalTerms) -> + pm_utils:select_defined(TerminalTerms, ProviderTerms). + +merge_withdrawal_terms( + #domain_WithdrawalProvisionTerms{ + allow = PAllow, + global_allow = PGAllow, + currencies = PCurrencies, + cash_limit = PLimit, + cash_flow = PCashflow, + turnover_limit = PTurnoverLimit + }, + #domain_WithdrawalProvisionTerms{ + allow = TAllow, + global_allow = TGAllow, + currencies = TCurrencies, + cash_limit = TLimit, + cash_flow = TCashflow, + turnover_limit = TTurnoverLimit + } +) -> + #domain_WithdrawalProvisionTerms{ + allow = pm_utils:select_defined(TAllow, PAllow), + global_allow = pm_utils:select_defined(TGAllow, PGAllow), + currencies = pm_utils:select_defined(TCurrencies, PCurrencies), + cash_limit = pm_utils:select_defined(TLimit, PLimit), + cash_flow = pm_utils:select_defined(TCashflow, PCashflow), + turnover_limit = pm_utils:select_defined(TTurnoverLimit, PTurnoverLimit) + }; +merge_withdrawal_terms(ProviderTerms, TerminalTerms) -> + pm_utils:select_defined(TerminalTerms, ProviderTerms). + +reduce_if_defined(Selector, VS, Rev) -> + pm_maybe:apply(fun(X) -> pm_selector:reduce(X, VS, Rev) end, Selector). + +reduce_predicate_if_defined(Predicate, VS, Rev) -> + pm_maybe:apply(fun(X) -> pm_selector:reduce_predicate(X, VS, Rev) end, Predicate). + +-spec compute_proxy(provider(), terminal(), domain_revision()) -> + dmsl_domain_thrift:'ProxyDefinition'(). +compute_proxy(Provider, Terminal, DomainRevision) -> + Proxy = Provider#domain_Provider.proxy, + ProxyDef = pm_domain:get(DomainRevision, {proxy, Proxy#domain_Proxy.ref}), + EffectiveOptions = lists:foldl( + fun + (undefined, M) -> + M; + (M1, M) -> + maps:merge(M1, M) + end, + #{}, + [ + Terminal#domain_Terminal.options, + Proxy#domain_Proxy.additional, + ProxyDef#domain_ProxyDefinition.options + ] + ), + ProxyDef#domain_ProxyDefinition{ + options = EffectiveOptions + }. diff --git a/apps/party_management/src/pm_ruleset.erl b/apps/party_management/src/pm_ruleset.erl new file mode 100644 index 00000000..70f1c2ee --- /dev/null +++ b/apps/party_management/src/pm_ruleset.erl @@ -0,0 +1,92 @@ +-module(pm_ruleset). + +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). +-include_lib("damsel/include/dmsl_payproc_thrift.hrl"). + +%% API +-export([reduce_payment_routing_ruleset/3]). + +-define(const(Bool), {constant, Bool}). + +-type payment_routing_ruleset() :: dmsl_domain_thrift:'RoutingRuleset'(). +-type varset() :: pm_selector:varset(). +-type domain_revision() :: pm_domain:revision(). + +-spec reduce_payment_routing_ruleset(payment_routing_ruleset(), varset(), domain_revision()) -> + payment_routing_ruleset(). +reduce_payment_routing_ruleset(RuleSet, VS, DomainRevision) -> + logger:log( + info, + "Routing start reduce ruleset with varset: ~p", + [VS], + logger:get_process_metadata() + ), + RuleSet#domain_RoutingRuleset{ + decisions = reduce_payment_routing_decisions(RuleSet#domain_RoutingRuleset.decisions, VS, DomainRevision) + }. + +reduce_payment_routing_decisions({delegates, Delegates}, VS, Rev) -> + reduce_payment_routing_delegates(Delegates, VS, Rev); +reduce_payment_routing_decisions({candidates, Candidates}, VS, Rev) -> + reduce_payment_routing_candidates(Candidates, VS, Rev). + +reduce_payment_routing_delegates([], _VS, _Rev) -> + {delegates, []}; +reduce_payment_routing_delegates([D | Delegates], VS, Rev) -> + Predicate = D#domain_RoutingDelegate.allowed, + RuleSetRef = D#domain_RoutingDelegate.ruleset, + case pm_selector:reduce_predicate(Predicate, VS, Rev) of + ?const(false) -> + logger:log( + info, + "Routing delegate rejected. Delegate: ~p~nPredicate: ~p", + [D, Predicate], + logger:get_process_metadata() + ), + reduce_payment_routing_delegates(Delegates, VS, Rev); + ?const(true) -> + #domain_RoutingRuleset{ + decisions = Decisions + } = get_payment_routing_ruleset(RuleSetRef, Rev), + reduce_payment_routing_decisions(Decisions, VS, Rev); + _ -> + logger:warning( + "Routing rule misconfiguration, can't reduce decision. Predicate: ~p~n Varset:~n~p", + [Predicate, VS] + ), + {delegates, [D | Delegates]} + end. + +reduce_payment_routing_candidates(Candidates, VS, Rev) -> + {candidates, + lists:foldr( + fun(C, AccIn) -> + Predicate = C#domain_RoutingCandidate.allowed, + case pm_selector:reduce_predicate(Predicate, VS, Rev) of + ?const(false) -> + logger:log( + info, + "Routing candidate rejected. Candidate: ~p~nPredicate: ~p", + [C, Predicate], + logger:get_process_metadata() + ), + AccIn; + ?const(true) = ReducedPredicate -> + ReducedCandidate = C#domain_RoutingCandidate{ + allowed = ReducedPredicate + }, + [ReducedCandidate | AccIn]; + _ -> + logger:warning( + "Routing rule misconfiguration, can't reduce decision. Predicate: ~p~nVarset:~n~p", + [Predicate, VS] + ), + [C | AccIn] + end + end, + [], + Candidates + )}. + +get_payment_routing_ruleset(RuleSetRef, DomainRevision) -> + pm_domain:get(DomainRevision, {routing_rules, RuleSetRef}). diff --git a/apps/party_management/src/pm_selector.erl b/apps/party_management/src/pm_selector.erl new file mode 100644 index 00000000..689a5926 --- /dev/null +++ b/apps/party_management/src/pm_selector.erl @@ -0,0 +1,188 @@ +%%% Domain selectors manipulation +%%% +%%% TODO +%%% - Manipulating predicates w/o respect to their struct infos is dangerous +%%% - Decide on semantics +%%% - First satisfiable predicate wins? +%%% If not, it would be harder to join / overlay selectors +%%% - Domain revision is out of place. An `Opts`, anyone? + +-module(pm_selector). + +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). + +%% + +-type t() :: + dmsl_domain_thrift:'CurrencySelector'() + | dmsl_domain_thrift:'CategorySelector'() + | dmsl_domain_thrift:'CashLimitSelector'() + | dmsl_domain_thrift:'CashFlowSelector'() + | dmsl_domain_thrift:'PaymentMethodSelector'() + | dmsl_domain_thrift:'SystemAccountSetSelector'() + | dmsl_domain_thrift:'ExternalAccountSetSelector'() + | dmsl_domain_thrift:'HoldLifetimeSelector'() + | dmsl_domain_thrift:'CashValueSelector'() + | dmsl_domain_thrift:'TurnoverLimitSelector'() + | dmsl_domain_thrift:'TimeSpanSelector'() + | dmsl_domain_thrift:'FeeSelector'() + | dmsl_domain_thrift:'InspectorSelector'(). + +-type value() :: + %% FIXME + _. + +-type varset() :: #{ + category => dmsl_domain_thrift:'CategoryRef'(), + currency => dmsl_domain_thrift:'CurrencyRef'(), + cost => dmsl_domain_thrift:'Cash'(), + payment_tool => dmsl_domain_thrift:'PaymentTool'(), + party_ref => dmsl_domain_thrift:'PartyConfigRef'(), + shop_id => dmsl_domain_thrift:'ShopID'(), + risk_score => dmsl_domain_thrift:'RiskScore'(), + flow => instant | {hold, dmsl_domain_thrift:'HoldLifetime'()}, + wallet_id => dmsl_domain_thrift:'WalletID'() +}. + +-type predicate() :: dmsl_domain_thrift:'Predicate'(). + +-export_type([varset/0]). + +-export([reduce/3]). +-export([reduce_to_value/3]). +-export([reduce_predicate/3]). + +-define(const(Bool), {constant, Bool}). + +%% + +-spec reduce_to_value(t(), varset(), pm_domain:revision()) -> value() | no_return(). +reduce_to_value(Selector, VS, Revision) -> + case reduce(Selector, VS, Revision) of + {value, Value} -> + Value; + _ -> + error({misconfiguration, {'Can\'t reduce selector to value', Selector, VS, Revision}}) + end. + +-spec reduce(t(), varset(), pm_domain:revision()) -> t(). +reduce({value, _} = V, _, _) -> + V; +reduce({decisions, Decisions}, VS, Rev) -> + case reduce_decisions(Decisions, VS, Rev) of + %% Return value only if topmost decision's predicate resolved to true: + %% otherwise, + %% the decision was either dropped (predicate reduced to false) + %% or there's not enough info to reduce a predicate (the result is undefined) + [{_Type, ?const(true), Selector} | _] -> + Selector; + Ps1 -> + {decisions, Ps1} + end. + +%% domain structs in form #domain_SomeDecision { Predicate if_; SomeSelector then_ }; +reduce_decisions([{Type, Predicate, Selector} | Rest], VS, Rev) -> + case reduce_predicate(Predicate, VS, Rev) of + ?const(false) -> + reduce_decisions(Rest, VS, Rev); + NewPredicate -> + case reduce(Selector, VS, Rev) of + {decisions, []} -> + reduce_decisions(Rest, VS, Rev); + NewSelector -> + [{Type, NewPredicate, NewSelector} | reduce_decisions(Rest, VS, Rev)] + end + end; +reduce_decisions([], _, _) -> + []. + +-spec reduce_predicate(predicate(), varset(), pm_domain:revision()) -> + predicate(). +reduce_predicate(?const(B), _, _) -> + ?const(B); +reduce_predicate({condition, C0}, VS, Rev) -> + case reduce_condition(C0, VS, Rev) of + ?const(B) -> + ?const(B); + C1 -> + {condition, C1} + end; +reduce_predicate({is_not, P0}, VS, Rev) -> + case reduce_predicate(P0, VS, Rev) of + ?const(B) -> + ?const(not B); + P1 -> + {is_not, P1} + end; +reduce_predicate({all_of, Ps}, VS, Rev) -> + reduce_combination(all_of, false, Ps, VS, Rev, []); +reduce_predicate({any_of, Ps}, VS, Rev) -> + reduce_combination(any_of, true, Ps, VS, Rev, []); +reduce_predicate({criterion, CriterionRef = #domain_CriterionRef{}}, VS, Rev) -> + Criterion = pm_domain:get(Rev, {criterion, CriterionRef}), + Predicate = Criterion#domain_Criterion.predicate, + case reduce_predicate(Predicate, VS, Rev) of + ?const(B) -> + ?const(B); + Predicate -> + {criterion, CriterionRef}; + NewPredicate -> + NewPredicate + end. + +reduce_combination(Type, Fix, [P | Ps], VS, Rev, PAcc) -> + case reduce_predicate(P, VS, Rev) of + ?const(Fix) -> + ?const(Fix); + ?const(_) -> + reduce_combination(Type, Fix, Ps, VS, Rev, PAcc); + P1 -> + reduce_combination(Type, Fix, Ps, VS, Rev, [P1 | PAcc]) + end; +reduce_combination(_, Fix, [], _, _, []) -> + ?const(not Fix); +reduce_combination(_, _, [], _, _, [P]) -> + P; +reduce_combination(Type, _, [], _, _, PAcc) -> + {Type, ordsets:from_list(lists:reverse(PAcc))}. + +reduce_condition(C, VS, Rev) -> + case pm_condition:test(C, VS, Rev) of + B when is_boolean(B) -> + ?const(B); + undefined -> + % Irreducible, return as is for further possible reduce + C + end. + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("damsel/include/dmsl_payproc_thrift.hrl"). + +-spec test() -> _. + +-spec bin_data_allow_test() -> _. +bin_data_allow_test() -> + VS = pm_varset:decode_varset(#payproc_Varset{ + bin_data = #domain_BinData{ + payment_system = <<"payment_system">>, + bank_name = <<"bank_name">> + } + }), + CondFun = fun(PaymentSystem, BN) -> + {any_of, [ + {condition, + {bin_data, #domain_BinDataCondition{ + payment_system = PaymentSystem, + bank_name = BN + }}} + ]} + end, + + ?assertEqual(?const(true), reduce_predicate(CondFun({matches, <<"pay">>}, {matches, <<"ban">>}), VS, 1)), + ?assertEqual(?const(true), reduce_predicate(CondFun({matches, <<"_system">>}, {matches, <<"_name">>}), VS, 1)), + ?assertEqual(?const(false), reduce_predicate(CondFun({equals, <<"system">>}, undefined), VS, 1)), + ?assertEqual(?const(false), reduce_predicate(CondFun(undefined, {equals, <<"bank">>}), VS, 1)), + ?assertEqual(?const(true), reduce_predicate(CondFun(undefined, undefined), VS, 1)). + +-endif. diff --git a/apps/party_management/src/pm_utils.erl b/apps/party_management/src/pm_utils.erl new file mode 100644 index 00000000..f1c19ce7 --- /dev/null +++ b/apps/party_management/src/pm_utils.erl @@ -0,0 +1,41 @@ +-module(pm_utils). + +-export([unique_id/0]). +-export([unwrap_result/1]). +-export([select_defined/2]). +-export([binary_ends_with/2]). + +%% + +-spec unique_id() -> dmsl_base_thrift:'ID'(). +unique_id() -> + <> = snowflake:new(), + genlib_format:format_int_base(ID, 62). + +-spec select_defined(T | undefined, T | undefined) -> T | undefined. +select_defined(V1, V2) -> + select_defined([V1, V2]). + +-spec select_defined([T | undefined]) -> T | undefined. +select_defined([V | _]) when V /= undefined -> + V; +select_defined([undefined | Vs]) -> + select_defined(Vs); +select_defined([]) -> + undefined. + +-spec binary_ends_with(binary(), binary()) -> boolean(). +binary_ends_with(Binary, Suffix) when byte_size(Binary) < byte_size(Suffix) -> + false; +binary_ends_with(Binary, Suffix) when is_binary(Binary), is_binary(Suffix) -> + Suffix =:= binary:part(Binary, byte_size(Binary) - byte_size(Suffix), byte_size(Suffix)). + +%% + +-spec unwrap_result + ({ok, T}) -> T; + ({error, _}) -> no_return(). +unwrap_result({ok, V}) -> + V; +unwrap_result({error, E}) -> + error(E). diff --git a/apps/party_management/src/pm_varset.erl b/apps/party_management/src/pm_varset.erl new file mode 100644 index 00000000..7c4f5213 --- /dev/null +++ b/apps/party_management/src/pm_varset.erl @@ -0,0 +1,106 @@ +-module(pm_varset). + +-include_lib("damsel/include/dmsl_payproc_thrift.hrl"). +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). + +-export([encode_varset/1]). +-export([decode_varset/2]). +-export([decode_varset/1]). + +-export_type([varset/0]). + +-type varset() :: #{ + category => dmsl_domain_thrift:'CategoryRef'(), + currency => dmsl_domain_thrift:'CurrencyRef'(), + cost => dmsl_domain_thrift:'Cash'(), + payment_method => dmsl_domain_thrift:'PaymentMethodRef'(), + wallet_id => dmsl_domain_thrift:'WalletID'(), + shop_id => dmsl_domain_thrift:'ShopID'(), + payment_tool => dmsl_domain_thrift:'PaymentTool'(), + party_ref => dmsl_domain_thrift:'PartyConfigRef'(), + bin_data => dmsl_domain_thrift:'BinData'(), + trust_level => dmsl_domain_thrift:'ClientTrustLevel'() +}. + +-type encoded_varset() :: dmsl_payproc_thrift:'Varset'(). + +-spec encode_varset(varset()) -> encoded_varset(). +encode_varset(Varset) -> + #payproc_Varset{ + category = genlib_map:get(category, Varset), + currency = genlib_map:get(currency, Varset), + amount = genlib_map:get(cost, Varset), + payment_method = genlib_map:get(payment_method, Varset), + wallet_id = genlib_map:get(wallet_id, Varset), + shop_id = genlib_map:get(shop_id, Varset), + payment_tool = genlib_map:get(payment_tool, Varset), + party_ref = genlib_map:get(party_ref, Varset), + bin_data = genlib_map:get(bin_data, Varset), + trust_level = genlib_map:get(trust_level, Varset) + }. + +-spec decode_varset(encoded_varset()) -> varset(). +decode_varset(Varset) -> + decode_varset(Varset, #{}). +-spec decode_varset(encoded_varset(), varset()) -> varset(). +decode_varset(#payproc_Varset{} = Varset, VS) -> + genlib_map:compact(VS#{ + category => Varset#payproc_Varset.category, + currency => Varset#payproc_Varset.currency, + cost => Varset#payproc_Varset.amount, + payment_method => Varset#payproc_Varset.payment_method, + wallet_id => Varset#payproc_Varset.wallet_id, + shop_id => Varset#payproc_Varset.shop_id, + payment_tool => prepare_payment_tool_var( + Varset#payproc_Varset.payment_method, + Varset#payproc_Varset.payment_tool + ), + party_ref => Varset#payproc_Varset.party_ref, + bin_data => Varset#payproc_Varset.bin_data, + trust_level => Varset#payproc_Varset.trust_level + }). + +prepare_payment_tool_var(_PaymentMethodRef, PaymentTool) when PaymentTool /= undefined -> + PaymentTool; +prepare_payment_tool_var(#domain_PaymentMethodRef{} = PaymentMethodRef, _PaymentTool) -> + pm_payment_tool:create_from_method(PaymentMethodRef); +prepare_payment_tool_var(undefined, undefined) -> + undefined. + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +-spec test() -> _. + +-spec encode_decode_test() -> _. +encode_decode_test() -> + Varset = #{ + category => #domain_CategoryRef{id = 1}, + currency => #domain_CurrencyRef{symbolic_code = <<"RUB">>}, + cost => #domain_Cash{ + amount = 20, + currency = #domain_CurrencyRef{symbolic_code = <<"RUB">>} + }, + payment_method => #domain_PaymentMethodRef{ + id = + {bank_card, #domain_BankCardPaymentMethod{ + payment_system = #domain_PaymentSystemRef{id = <<"visa">>} + }} + }, + wallet_id => <<"wallet_id">>, + shop_id => <<"shop_id">>, + payment_tool => + {digital_wallet, #domain_DigitalWallet{ + payment_service = #domain_PaymentServiceRef{id = <<"qiwi">>}, + id = <<"digital_wallet_id">> + }}, + party_ref => #domain_PartyConfigRef{id = <<"party_id">>}, + bin_data => #domain_BinData{ + payment_system = <<"payment_system">>, + bank_name = <<"bank_name">> + }, + trust_level => well_known + }, + ?assertEqual(Varset, decode_varset(encode_varset(Varset))). + +-endif. diff --git a/apps/party_management/src/pm_woody_client.erl b/apps/party_management/src/pm_woody_client.erl new file mode 100644 index 00000000..531c05cf --- /dev/null +++ b/apps/party_management/src/pm_woody_client.erl @@ -0,0 +1,34 @@ +-module(pm_woody_client). + +%% API +-export([new/1]). + +-type url() :: woody:url(). +-type event_handler() :: woody:ev_handler(). +-type transport_opts() :: woody_client_thrift_http_transport:transport_options(). + +-type client() :: #{ + url := url(), + event_handler := event_handler(), + transport_opts => transport_opts() +}. + +-type opts() :: #{ + url := url(), + event_handler => event_handler(), + transport_opts => transport_opts() +}. + +-spec new(woody:url() | opts()) -> client(). +new(#{url := _} = Opts) -> + EventHandlerOpts = genlib_app:env(party_management, scoper_event_handler_options, #{}), + maps:merge( + #{ + event_handler => {pm_woody_event_handler, EventHandlerOpts} + }, + maps:with([url, event_handler, transport_opts], Opts) + ); +new(Url) when is_binary(Url); is_list(Url) -> + new(#{ + url => genlib:to_binary(Url) + }). diff --git a/apps/party_management/src/pm_woody_event_handler.erl b/apps/party_management/src/pm_woody_event_handler.erl new file mode 100644 index 00000000..ecd63afc --- /dev/null +++ b/apps/party_management/src/pm_woody_event_handler.erl @@ -0,0 +1,101 @@ +-module(pm_woody_event_handler). + +-behaviour(woody_event_handler). + +-include_lib("damsel/include/dmsl_base_thrift.hrl"). +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). +-include_lib("damsel/include/dmsl_payproc_thrift.hrl"). + +%% woody_event_handler behaviour callbacks +-export([handle_event/4]). + +-spec handle_event(Event, RpcID, Meta, Opts) -> ok when + Event :: woody_event_handler:event(), + RpcID :: woody:rpc_id() | undefined, + Meta :: woody_event_handler:event_meta(), + Opts :: woody:options(). +handle_event(Event, RpcID, RawMeta, Opts) -> + FilteredMeta = filter_meta(RawMeta), + scoper_woody_event_handler:handle_event(Event, RpcID, FilteredMeta, Opts). + +%% Internals + +filter_meta(RawMeta0) -> + maps:map(fun do_filter_meta/2, RawMeta0). + +do_filter_meta(args, Args) -> + filter(Args); +do_filter_meta(_Key, Value) -> + Value. + +%% cut secrets +filter(#payproc_ProviderTerminal{proxy = Proxy} = ProviderTerminal) -> + #domain_ProxyDefinition{options = Options} = Proxy, + ProviderTerminal#payproc_ProviderTerminal{ + proxy = Proxy#domain_ProxyDefinition{options = maps:without([<<"api-key">>, <<"secret-key">>], Options)} + }; +%% common +filter(L) when is_list(L) -> + [filter(E) || E <- L]; +filter(T) when is_tuple(T) -> + list_to_tuple(filter(tuple_to_list(T))); +%% default +filter(V) -> + V. + +-ifdef(TEST). + +-include_lib("eunit/include/eunit.hrl"). + +-define(ARG_W_SECRET, + { + #payproc_ProviderTerminal{ + ref = #domain_TerminalRef{id = 128}, + name = <<"TestTerm">>, + provider = #payproc_ProviderDetails{ + ref = #domain_ProviderRef{id = 1}, + name = <<"Provider1">> + }, + proxy = #domain_ProxyDefinition{ + name = <<"Proxy">>, + description = <<"Desc">>, + url = <<"http://127.0.0.1">>, + options = #{<<"api-key">> => <<"secret">>, <<"secret-key">> => <<"secret">>} + } + } + } +). + +-define(ARG_WO_SECRET, + { + #payproc_ProviderTerminal{ + ref = #domain_TerminalRef{id = 128}, + name = <<"TestTerm">>, + provider = #payproc_ProviderDetails{ + ref = #domain_ProviderRef{id = 1}, + name = <<"Provider1">> + }, + proxy = #domain_ProxyDefinition{ + name = <<"Proxy">>, + description = <<"Desc">>, + url = <<"http://127.0.0.1">>, + options = #{} + } + } + } +). + +-spec test() -> _. + +-spec format_event_w_secret_test_() -> _. +format_event_w_secret_test_() -> + [ + ?_assertEqual( + #{args => {some_data, ?ARG_WO_SECRET}, code => 200, function => 'ComputePaymentInstitution'}, + filter_meta( + #{args => {some_data, ?ARG_W_SECRET}, code => 200, function => 'ComputePaymentInstitution'} + ) + ) + ]. + +-endif. diff --git a/apps/party_management/src/pm_woody_wrapper.erl b/apps/party_management/src/pm_woody_wrapper.erl new file mode 100644 index 00000000..e0fea4f4 --- /dev/null +++ b/apps/party_management/src/pm_woody_wrapper.erl @@ -0,0 +1,123 @@ +-module(pm_woody_wrapper). + +%% Woody handler + +-behaviour(woody_server_thrift_handler). + +-export([handle_function/4]). + +-export_type([handler_opts/0]). +-export_type([client_opts/0]). + +-type handler_opts() :: #{ + handler := module(), + default_handling_timeout => timeout() +}. + +-type client_opts() :: #{ + url := woody:url(), + transport_opts => [{_, _}] +}. + +% 30 seconds +-define(DEFAULT_HANDLING_TIMEOUT, 30000). + +%% Callbacks + +-callback handle_function(woody:func(), woody:args(), handler_opts()) -> term() | no_return(). + +%% API + +-export([call/3]). +-export([call/4]). +-export([call/5]). +-export([raise/1]). + +-export([get_service_options/1]). + +-spec handle_function(woody:func(), woody:args(), woody_context:ctx(), handler_opts()) -> {ok, term()} | no_return(). +handle_function(Func, Args, WoodyContext0, #{handler := Handler} = Opts) -> + WoodyContext = ensure_woody_deadline_set(WoodyContext0, Opts), + ok = pm_context:save(create_context(WoodyContext)), + try + Result = Handler:handle_function( + Func, + Args, + Opts + ), + {ok, Result} + catch + throw:Reason -> + raise(Reason) + after + pm_context:cleanup() + end. + +-spec call(atom(), woody:func(), woody:args()) -> term(). +call(ServiceName, Function, Args) -> + Opts = get_service_options(ServiceName), + Deadline = undefined, + call(ServiceName, Function, Args, Opts, Deadline). + +-spec call(atom(), woody:func(), woody:args(), client_opts()) -> term(). +call(ServiceName, Function, Args, Opts) -> + Deadline = undefined, + call(ServiceName, Function, Args, Opts, Deadline). + +-spec call(atom(), woody:func(), woody:args(), client_opts(), woody_deadline:deadline()) -> term(). +call(ServiceName, Function, Args, Opts, Deadline) -> + Service = get_service_modname(ServiceName), + Context = pm_context:get_woody_context(pm_context:load()), + Request = {Service, Function, Args}, + woody_client:call( + Request, + Opts#{ + event_handler => { + scoper_woody_event_handler, + genlib_app:env(party_management, scoper_event_handler_options, #{}) + } + }, + attach_deadline(Deadline, Context) + ). + +-spec get_service_options(atom()) -> client_opts(). +get_service_options(ServiceName) -> + construct_opts(maps:get(ServiceName, genlib_app:env(party_management, services))). + +-spec attach_deadline(woody_deadline:deadline(), woody_context:ctx()) -> woody_context:ctx(). +attach_deadline(undefined, Context) -> + Context; +attach_deadline(Deadline, Context) -> + woody_context:set_deadline(Deadline, Context). + +-spec raise(term()) -> no_return(). +raise(Exception) -> + woody_error:raise(business, Exception). + +%% Internal functions + +construct_opts(#{url := Url} = Opts) -> + Opts#{url := genlib:to_binary(Url)}; +construct_opts(Url) -> + #{url => genlib:to_binary(Url)}. + +-spec get_service_modname(atom()) -> {module(), atom()}. +get_service_modname(ServiceName) -> + pm_proto:get_service(ServiceName). + +create_context(WoodyContext) -> + ContextOptions = #{ + woody_context => WoodyContext + }, + pm_context:create(ContextOptions). + +-spec ensure_woody_deadline_set(woody_context:ctx(), handler_opts()) -> woody_context:ctx(). +ensure_woody_deadline_set(WoodyContext, Opts) -> + case woody_context:get_deadline(WoodyContext) of + undefined -> + DefaultTimeout = maps:get(default_handling_timeout, Opts, ?DEFAULT_HANDLING_TIMEOUT), + Deadline = woody_deadline:from_timeout(DefaultTimeout), + woody_context:set_deadline(Deadline, WoodyContext); + _Other -> + WoodyContext + end. diff --git a/apps/party_management/test/pm_ct_domain.erl b/apps/party_management/test/pm_ct_domain.erl new file mode 100644 index 00000000..7c7bb42b --- /dev/null +++ b/apps/party_management/test/pm_ct_domain.erl @@ -0,0 +1,63 @@ +-module(pm_ct_domain). + +-include_lib("damsel/include/dmsl_domain_conf_v2_thrift.hrl"). + +-export([upsert/2]). +-export([commit/2]). + +-export([with/2]). + +%% + +-type revision() :: pm_domain:revision(). +-type object() :: pm_domain:object(). + +-spec upsert(revision(), object() | [object()]) -> {revision(), [pm_domain:ref()]} | no_return(). +upsert(Revision, NewObject) when not is_list(NewObject) -> + upsert(Revision, [NewObject]); +upsert(Revision, NewObjects) -> + Commit = lists:foldl( + fun({Tag, {_ObjectName, Ref, NewData}} = NewObject, Ops) -> + case pm_domain:find(Revision, {Tag, Ref}) of + NewData -> + Ops; + notfound -> + [ + {insert, #domain_conf_v2_InsertOp{ + object = {Tag, NewData}, + force_ref = {Tag, Ref} + }} + | Ops + ]; + _OldData -> + [ + {update, #domain_conf_v2_UpdateOp{object = NewObject}} + | Ops + ] + end + end, + [], + NewObjects + ), + commit(Revision, Commit). + +-spec commit(revision(), [dmt_client:operation()]) -> {revision(), [pm_domain:ref()]} | no_return(). +commit(Revision, Operations) -> + #domain_conf_v2_CommitResponse{version = Version, new_objects = NewObjects} = + dmt_client:commit(Revision, Operations, generate_author()), + NewObjectsIDs = [ + {Tag, Ref} + || {Tag, {_ON, Ref, _Obj}} <- ordsets:to_list(NewObjects) + ], + {Version, NewObjectsIDs}. + +-spec with(object() | [object()], fun((revision()) -> _)) -> + {revision(), [pm_domain:ref()]} | no_return(). +with(NewObjects, Fun) -> + WasRevision = pm_domain:head(), + {Version, NewObjectsIDs} = upsert(WasRevision, NewObjects), + _ = Fun(Version), + {Version, NewObjectsIDs}. + +generate_author() -> + dmt_client:create_author(genlib:unique(), genlib:unique()). diff --git a/apps/party_management/test/pm_ct_domain.hrl b/apps/party_management/test/pm_ct_domain.hrl new file mode 100644 index 00000000..972536a1 --- /dev/null +++ b/apps/party_management/test/pm_ct_domain.hrl @@ -0,0 +1,104 @@ +-ifndef(__pm_ct_domain__). +-define(__pm_ct_domain__, 42). + +-include("domain.hrl"). + +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). + +-define(ordset(Es), ordsets:from_list(Es)). + +-define(party(ID), #domain_PartyConfigRef{id = ID}). +-define(shop(ID), #domain_ShopConfigRef{id = ID}). +-define(wallet(ID), #domain_WalletConfigRef{id = ID}). +-define(lim(ID), #domain_LimitConfigRef{id = ID}). +-define(cur(ID), #domain_CurrencyRef{symbolic_code = ID}). +-define(pmt(C, T), #domain_PaymentMethodRef{id = {C, T}}). +-define(pmt_sys(ID), #domain_PaymentSystemRef{id = ID}). +-define(pmt_srv(ID), #domain_PaymentServiceRef{id = ID}). +-define(cat(ID), #domain_CategoryRef{id = ID}). +-define(prx(ID), #domain_ProxyRef{id = ID}). +-define(prv(ID), #domain_ProviderRef{id = ID}). +-define(prvtrm(ID), #domain_ProviderTerminalRef{id = ID}). +-define(trm(ID), #domain_TerminalRef{id = ID}). +-define(tmpl(ID), #domain_ContractTemplateRef{id = ID}). +-define(trms(ID), #domain_TermSetHierarchyRef{id = ID}). +-define(sas(ID), #domain_SystemAccountSetRef{id = ID}). +-define(eas(ID), #domain_ExternalAccountSetRef{id = ID}). +-define(insp(ID), #domain_InspectorRef{id = ID}). +-define(pinst(ID), #domain_PaymentInstitutionRef{id = ID}). +-define(bank(ID), #domain_BankRef{id = ID}). +-define(bussched(ID), #domain_BusinessScheduleRef{id = ID}). +-define(ruleset(ID), #domain_RoutingRulesetRef{id = ID}). +-define(mob(ID), #domain_MobileOperatorRef{id = ID}). +-define(crypta(ID), #domain_CryptoCurrencyRef{id = ID}). +-define(token_srv(ID), #domain_BankCardTokenServiceRef{id = ID}). +-define(bank_card(ID), #domain_BankCardPaymentMethod{payment_system = ?pmt_sys(ID)}). +-define(bank_card_no_cvv(ID), #domain_BankCardPaymentMethod{payment_system = ?pmt_sys(ID), is_cvv_empty = true}). +-define(token_bank_card(ID, Prv), ?token_bank_card(ID, Prv, dpan)). +-define(token_bank_card(ID, Prv, Method), #domain_BankCardPaymentMethod{ + payment_system = ?pmt_sys(ID), + payment_token = ?token_srv(Prv), + tokenization_method = Method +}). +-define(crit(ID), #domain_CriterionRef{id = ID}). +-define(crp(ID), #domain_CashRegisterProviderRef{id = ID}). +-define(gnrc(PS), #domain_GenericPaymentMethod{payment_service = PS}). +-define(gnrc_cond(Path, Value), #domain_GenericResourceCondition{field_path = Path, value = Value}). +-define(gnrc_tool(PS, Data), #domain_GenericPaymentTool{payment_service = PS, data = Data}). + +-define(cashrng(Lower, Upper), #domain_CashRange{lower = Lower, upper = Upper}). + +-define(prvacc(Stl), #domain_ProviderAccount{settlement = Stl}). +-define(partycond(ID, Def), + {condition, {party, #domain_PartyCondition{party_ref = ?party(ID), definition = Def}}} +). + +-define(fixed(Amount, Currency), + {fixed, #domain_CashVolumeFixed{ + cash = #domain_Cash{ + amount = Amount, + currency = ?currency(Currency) + } + }} +). + +-define(share(P, Q, C), + {share, #domain_CashVolumeShare{ + parts = #base_Rational{p = P, q = Q}, + 'of' = C + }} +). + +-define(share(P, Q, C, RM), + {share, #domain_CashVolumeShare{ + parts = #base_Rational{p = P, q = Q}, + 'of' = C, + rounding_method = RM + }} +). + +-define(cfpost(A1, A2, V), #domain_CashFlowPosting{ + source = A1, + destination = A2, + volume = V +}). + +-define(timeout_reason(), <<"Timeout">>). + +-define(bank_card_payment_tool(BankName, IsCVVEmpty), + {bank_card, #domain_BankCard{ + token = <<>>, + bin = <<>>, + last_digits = <<>>, + bank_name = BankName, + payment_system = #domain_PaymentSystemRef{id = <<"visa">>}, + issuer_country = rus, + is_cvv_empty = IsCVVEmpty + }} +). + +-define(bank_card_payment_tool(BankName), + ?bank_card_payment_tool(BankName, undefined) +). + +-endif. diff --git a/apps/party_management/test/pm_ct_fixture.erl b/apps/party_management/test/pm_ct_fixture.erl new file mode 100644 index 00000000..17b03db5 --- /dev/null +++ b/apps/party_management/test/pm_ct_fixture.erl @@ -0,0 +1,425 @@ +-module(pm_ct_fixture). + +-include("pm_ct_domain.hrl"). + +-include_lib("damsel/include/dmsl_base_thrift.hrl"). +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). + +%% + +-export([construct_party/1]). +-export([construct_shop_account/1]). +-export([construct_shop/7]). +-export([construct_wallet_account/1]). +-export([construct_wallet/5]). +-export([construct_currency/1]). +-export([construct_currency/2]). +-export([construct_category/2]). +-export([construct_category/3]). +-export([construct_payment_method/1]). +-export([construct_proxy/2]). +-export([construct_proxy/4]). +-export([construct_inspector/3]). +-export([construct_inspector/4]). +-export([construct_inspector/5]). +-export([construct_provider_account_set/1]). +-export([construct_system_account_set/1]). +-export([construct_system_account_set/3]). +-export([construct_external_account_set/1]). +-export([construct_external_account_set/3]). +-export([construct_business_schedule/1]). +-export([construct_criterion/3]). +-export([construct_term_set_hierarchy/3]). +-export([construct_payment_routing_ruleset/3]). +-export([construct_payment_system/2]). +-export([construct_mobile_operator/2]). +-export([construct_payment_service/2]). +-export([construct_crypto_currency/2]). +-export([construct_tokenized_service/2]). + +%% + +-type name() :: binary(). +-type category() :: dmsl_domain_thrift:'CategoryRef'(). +-type currency() :: dmsl_domain_thrift:'CurrencyRef'(). +-type proxy() :: dmsl_domain_thrift:'ProxyRef'(). +-type inspector() :: dmsl_domain_thrift:'InspectorRef'(). +-type risk_score() :: dmsl_domain_thrift:'RiskScore'(). +-type payment_routing_ruleset() :: dmsl_domain_thrift:'RoutingRulesetRef'(). + +-type payment_system() :: dmsl_domain_thrift:'PaymentSystemRef'(). +-type mobile_operator() :: dmsl_domain_thrift:'MobileOperatorRef'(). +-type payment_service() :: dmsl_domain_thrift:'PaymentServiceRef'(). +-type crypto_currency() :: dmsl_domain_thrift:'CryptoCurrencyRef'(). +-type tokenized_service() :: dmsl_domain_thrift:'BankCardTokenServiceRef'(). + +-type system_account_set() :: dmsl_domain_thrift:'SystemAccountSetRef'(). +-type external_account_set() :: dmsl_domain_thrift:'ExternalAccountSetRef'(). + +-type business_schedule() :: dmsl_domain_thrift:'BusinessScheduleRef'(). + +-type criterion() :: dmsl_domain_thrift:'CriterionRef'(). +-type predicate() :: dmsl_domain_thrift:'Predicate'(). + +-type term_set() :: dmsl_domain_thrift:'TermSet'(). +-type term_set_hierarchy() :: dmsl_domain_thrift:'TermSetHierarchyRef'(). + +%% + +-define(EVERY, {every, #'base_ScheduleEvery'{}}). + +%% + +-spec construct_party(dmsl_domain_thrift:'PartyConfigRef'()) -> + {party_config, dmsl_domain_thrift:'PartyConfigObject'()}. +construct_party(PartyRef) -> + {party_config, #domain_PartyConfigObject{ + ref = PartyRef, + data = #domain_PartyConfig{ + name = PartyRef#domain_PartyConfigRef.id, + block = make_unblocked(), + suspension = make_active(), + contact_info = #domain_PartyContactInfo{registration_email = <<"party@example.com">>} + } + }}. + +-spec construct_shop_account(dmsl_domain_thrift:'CurrencySymbolicCode'()) -> dmsl_domain_thrift:'ShopAccount'(). +construct_shop_account(CurrencyCode) -> + ok = pm_context:save(pm_context:create()), + Settlement = pm_accounting:create_account(CurrencyCode), + Guarantee = pm_accounting:create_account(CurrencyCode), + _ = pm_context:cleanup(), + #domain_ShopAccount{ + currency = ?cur(CurrencyCode), + settlement = Settlement, + guarantee = Guarantee + }. + +-spec construct_shop( + dmsl_domain_thrift:'ShopConfigRef'(), + dmsl_domain_thrift:'PaymentInstitutionRef'(), + dmsl_domain_thrift:'TermSetHierarchyRef'(), + dmsl_domain_thrift:'ShopAccount'(), + dmsl_domain_thrift:'PartyConfigRef'(), + binary(), + dmsl_domain_thrift:'CategoryRef'() +) -> {shop_config, dmsl_domain_thrift:'ShopConfigObject'()}. +construct_shop(ShopRef, PaymentInstitutionRef, TermSetHierarchyRef, ShopAccount, PartyRef, ShopLocation, CategoryRef) -> + {shop_config, #domain_ShopConfigObject{ + ref = ShopRef, + data = #domain_ShopConfig{ + name = ShopRef#domain_ShopConfigRef.id, + block = make_unblocked(), + suspension = make_active(), + payment_institution = PaymentInstitutionRef, + terms = TermSetHierarchyRef, + account = ShopAccount, + party_ref = PartyRef, + location = {url, ShopLocation}, + category = CategoryRef + } + }}. + +-spec construct_wallet_account(dmsl_domain_thrift:'CurrencySymbolicCode'()) -> dmsl_domain_thrift:'WalletAccount'(). +construct_wallet_account(CurrencyCode) -> + ok = pm_context:save(pm_context:create()), + Settlement = pm_accounting:create_account(CurrencyCode), + _ = pm_context:cleanup(), + #domain_WalletAccount{ + currency = ?cur(CurrencyCode), + settlement = Settlement + }. + +-spec construct_wallet( + dmsl_domain_thrift:'WalletConfigRef'(), + dmsl_domain_thrift:'PaymentInstitutionRef'(), + dmsl_domain_thrift:'TermSetHierarchyRef'(), + dmsl_domain_thrift:'WalletAccount'(), + dmsl_domain_thrift:'PartyConfigRef'() +) -> {wallet_config, dmsl_domain_thrift:'WalletConfigObject'()}. +construct_wallet(WalletRef, PaymentInstitutionRef, TermSetHierarchyRef, WalletAccount, PartyRef) -> + {wallet_config, #domain_WalletConfigObject{ + ref = WalletRef, + data = #domain_WalletConfig{ + name = WalletRef#domain_WalletConfigRef.id, + block = make_unblocked(), + suspension = make_active(), + payment_institution = PaymentInstitutionRef, + terms = TermSetHierarchyRef, + account = WalletAccount, + party_ref = PartyRef + } + }}. + +-spec construct_currency(currency()) -> {currency, dmsl_domain_thrift:'CurrencyObject'()}. +construct_currency(Ref) -> + construct_currency(Ref, 2). + +-spec construct_currency(currency(), Exponent :: pos_integer()) -> {currency, dmsl_domain_thrift:'CurrencyObject'()}. +construct_currency(?cur(SymbolicCode) = Ref, Exponent) -> + {currency, #domain_CurrencyObject{ + ref = Ref, + data = #domain_Currency{ + name = SymbolicCode, + numeric_code = 666, + symbolic_code = SymbolicCode, + exponent = Exponent + } + }}. + +-spec construct_category(category(), name()) -> {category, dmsl_domain_thrift:'CategoryObject'()}. +construct_category(Ref, Name) -> + construct_category(Ref, Name, test). + +-spec construct_category(category(), name(), test | live) -> {category, dmsl_domain_thrift:'CategoryObject'()}. +construct_category(Ref, Name, Type) -> + {category, #domain_CategoryObject{ + ref = Ref, + data = #domain_Category{ + name = Name, + description = Name, + type = Type + } + }}. + +-spec construct_payment_method(dmsl_domain_thrift:'PaymentMethodRef'()) -> + {payment_method, dmsl_domain_thrift:'PaymentMethodObject'()}. +construct_payment_method(?pmt(generic, ?gnrc(?pmt_srv(Name))) = Ref) -> + construct_payment_method(Name, Ref); +construct_payment_method(?pmt(mobile, ?mob(Name)) = Ref) -> + construct_payment_method(Name, Ref); +construct_payment_method(?pmt(_, ?pmt_srv(Name)) = Ref) -> + construct_payment_method(Name, Ref); +construct_payment_method(?pmt(crypto_currency, ?crypta(Name)) = Ref) -> + construct_payment_method(Name, Ref); +construct_payment_method(?pmt(bank_card, ?token_bank_card(Name, _)) = Ref) -> + construct_payment_method(Name, Ref); +construct_payment_method(?pmt(bank_card, ?bank_card(Name)) = Ref) -> + construct_payment_method(Name, Ref); +construct_payment_method(?pmt(_Type, #domain_BankCardPaymentMethod{} = Card) = Ref) -> + construct_payment_method(Card#domain_BankCardPaymentMethod.payment_system, Ref). + +construct_payment_method(Name, Ref) when is_atom(Name) -> + construct_payment_method(atom_to_binary(Name, unicode), Ref); +construct_payment_method(Name, Ref) when is_binary(Name) -> + {payment_method, #domain_PaymentMethodObject{ + ref = Ref, + data = #domain_PaymentMethodDefinition{ + name = Name, + description = Name + } + }}. + +-spec construct_payment_system(payment_system(), name()) -> + {payment_system, dmsl_domain_thrift:'PaymentSystemObject'()}. +construct_payment_system(Ref, Name) -> + {payment_system, #domain_PaymentSystemObject{ + ref = Ref, + data = #domain_PaymentSystem{ + name = Name + } + }}. + +-spec construct_mobile_operator(mobile_operator(), name()) -> + {mobile_operator, dmsl_domain_thrift:'MobileOperatorObject'()}. +construct_mobile_operator(Ref, Name) -> + {mobile_operator, #domain_MobileOperatorObject{ + ref = Ref, + data = #domain_MobileOperator{ + name = Name + } + }}. + +-spec construct_payment_service(payment_service(), name()) -> + {payment_service, dmsl_domain_thrift:'PaymentServiceObject'()}. +construct_payment_service(Ref, Name) -> + {payment_service, #domain_PaymentServiceObject{ + ref = Ref, + data = #domain_PaymentService{ + name = Name + } + }}. + +-spec construct_crypto_currency(crypto_currency(), name()) -> + {crypto_currency, dmsl_domain_thrift:'CryptoCurrencyObject'()}. +construct_crypto_currency(Ref, Name) -> + {crypto_currency, #domain_CryptoCurrencyObject{ + ref = Ref, + data = #domain_CryptoCurrency{ + name = Name + } + }}. + +-spec construct_tokenized_service(tokenized_service(), name()) -> + {payment_token, dmsl_domain_thrift:'BankCardTokenServiceObject'()}. +construct_tokenized_service(Ref, Name) -> + {payment_token, #domain_BankCardTokenServiceObject{ + ref = Ref, + data = #domain_BankCardTokenService{ + name = Name + } + }}. + +-spec construct_proxy(proxy(), name()) -> {proxy, dmsl_domain_thrift:'ProxyObject'()}. +construct_proxy(Ref, Name) -> + construct_proxy(Ref, Name, <<>>, #{}). + +-spec construct_proxy(proxy(), name(), binary(), Opts :: map()) -> {proxy, dmsl_domain_thrift:'ProxyObject'()}. +construct_proxy(Ref, Name, Url, Opts) -> + {proxy, #domain_ProxyObject{ + ref = Ref, + data = #domain_ProxyDefinition{ + name = Name, + description = Name, + url = Url, + options = Opts + } + }}. + +-spec construct_inspector(inspector(), name(), proxy()) -> {inspector, dmsl_domain_thrift:'InspectorObject'()}. +construct_inspector(Ref, Name, ProxyRef) -> + construct_inspector(Ref, Name, ProxyRef, #{}). + +-spec construct_inspector(inspector(), name(), proxy(), Additional :: map()) -> + {inspector, dmsl_domain_thrift:'InspectorObject'()}. +construct_inspector(Ref, Name, ProxyRef, Additional) -> + construct_inspector(Ref, Name, ProxyRef, Additional, undefined). + +-spec construct_inspector(inspector(), name(), proxy(), Additional :: map(), undefined | risk_score()) -> + {inspector, dmsl_domain_thrift:'InspectorObject'()}. +construct_inspector(Ref, Name, ProxyRef, Additional, FallBackScore) -> + {inspector, #domain_InspectorObject{ + ref = Ref, + data = #domain_Inspector{ + name = Name, + description = Name, + proxy = #domain_Proxy{ + ref = ProxyRef, + additional = Additional + }, + fallback_risk_score = FallBackScore + } + }}. + +-spec construct_provider_account_set([currency()]) -> dmsl_domain_thrift:'ProviderAccountSet'(). +construct_provider_account_set(Currencies) -> + ok = pm_context:save(pm_context:create()), + AccountSet = lists:foldl( + fun(Cur = ?cur(Code), Acc) -> + Acc#{Cur => ?prvacc(pm_accounting:create_account(Code))} + end, + #{}, + Currencies + ), + _ = pm_context:cleanup(), + AccountSet. + +-spec construct_system_account_set(system_account_set()) -> + {system_account_set, dmsl_domain_thrift:'SystemAccountSetObject'()}. +construct_system_account_set(Ref) -> + construct_system_account_set(Ref, <<"Primaries">>, ?cur(<<"RUB">>)). + +-spec construct_system_account_set(system_account_set(), name(), currency()) -> + {system_account_set, dmsl_domain_thrift:'SystemAccountSetObject'()}. +construct_system_account_set(Ref, Name, ?cur(CurrencyCode)) -> + ok = pm_context:save(pm_context:create()), + SettlementAccountID = pm_accounting:create_account(CurrencyCode), + SubagentAccountID = pm_accounting:create_account(CurrencyCode), + pm_context:cleanup(), + {system_account_set, #domain_SystemAccountSetObject{ + ref = Ref, + data = #domain_SystemAccountSet{ + name = Name, + description = Name, + accounts = #{ + ?cur(CurrencyCode) => #domain_SystemAccount{ + settlement = SettlementAccountID, + subagent = SubagentAccountID + } + } + } + }}. + +-spec construct_external_account_set(external_account_set()) -> + {external_account_set, dmsl_domain_thrift:'ExternalAccountSetObject'()}. +construct_external_account_set(Ref) -> + construct_external_account_set(Ref, <<"Primaries">>, ?cur(<<"RUB">>)). + +-spec construct_external_account_set(external_account_set(), name(), currency()) -> + {external_account_set, dmsl_domain_thrift:'ExternalAccountSetObject'()}. +construct_external_account_set(Ref, Name, ?cur(CurrencyCode)) -> + ok = pm_context:save(pm_context:create()), + AccountID1 = pm_accounting:create_account(CurrencyCode), + AccountID2 = pm_accounting:create_account(CurrencyCode), + pm_context:cleanup(), + {external_account_set, #domain_ExternalAccountSetObject{ + ref = Ref, + data = #domain_ExternalAccountSet{ + name = Name, + description = Name, + accounts = #{ + ?cur(<<"RUB">>) => #domain_ExternalAccount{ + income = AccountID1, + outcome = AccountID2 + } + } + } + }}. + +-spec construct_business_schedule(business_schedule()) -> + {business_schedule, dmsl_domain_thrift:'BusinessScheduleObject'()}. +construct_business_schedule(Ref) -> + {business_schedule, #domain_BusinessScheduleObject{ + ref = Ref, + data = #domain_BusinessSchedule{ + name = <<"Every day at 7:40">>, + schedule = #'base_Schedule'{ + year = ?EVERY, + month = ?EVERY, + day_of_month = ?EVERY, + day_of_week = ?EVERY, + hour = {on, [7]}, + minute = {on, [40]}, + second = {on, [0]} + } + } + }}. + +-spec construct_criterion(criterion(), name(), predicate()) -> {criterion, dmsl_domain_thrift:'CriterionObject'()}. +construct_criterion(Ref, Name, Pred) -> + {criterion, #domain_CriterionObject{ + ref = Ref, + data = #domain_Criterion{ + name = Name, + predicate = Pred + } + }}. + +-spec construct_term_set_hierarchy(term_set_hierarchy(), undefined | term_set_hierarchy(), term_set()) -> + {term_set_hierarchy, dmsl_domain_thrift:'TermSetHierarchyObject'()}. +construct_term_set_hierarchy(Ref, ParentRef, TermSet) -> + {term_set_hierarchy, #domain_TermSetHierarchyObject{ + ref = Ref, + data = #domain_TermSetHierarchy{ + parent_terms = ParentRef, + term_set = TermSet + } + }}. + +-spec construct_payment_routing_ruleset(payment_routing_ruleset(), name(), _) -> dmsl_domain_thrift:'DomainObject'(). +construct_payment_routing_ruleset(Ref, Name, Decisions) -> + {routing_rules, #domain_RoutingRulesObject{ + ref = Ref, + data = #domain_RoutingRuleset{ + name = Name, + decisions = Decisions + } + }}. + +%% + +make_unblocked() -> + {unblocked, #domain_Unblocked{reason = ~"whatever reason", since = ~"1970-01-01T00:00:00Z"}}. + +make_active() -> + {active, #domain_Active{since = ~"1970-01-01T00:00:00Z"}}. diff --git a/apps/party_management/test/pm_ct_helper.erl b/apps/party_management/test/pm_ct_helper.erl new file mode 100644 index 00000000..737f5c6c --- /dev/null +++ b/apps/party_management/test/pm_ct_helper.erl @@ -0,0 +1,137 @@ +-module(pm_ct_helper). + +-export([start_app/1]). +-export([start_app/2]). +-export([start_apps/1]). + +-export([cfg/2]). + +-export([create_client/0]). +-export([create_client/1]). + +-export_type([config/0]). +-export_type([test_case_name/0]). +-export_type([group_name/0]). + +%% + +-type app_name() :: atom(). + +-spec start_app(app_name()) -> {[app_name()], map()}. +start_app(scoper = AppName) -> + { + start_app(AppName, [ + {storage, scoper_storage_logger} + ]), + #{} + }; +start_app(woody = AppName) -> + { + start_app(AppName, [ + {acceptors_pool_size, 4} + ]), + #{} + }; +start_app(dmt_client = AppName) -> + { + start_app(AppName, [ + % milliseconds + {cache_update_interval, 5000}, + {max_cache_size, #{ + elements => 20, + % 50Mb + memory => 52428800 + }}, + {woody_event_handlers, [ + {scoper_woody_event_handler, #{ + event_handler_opts => #{ + formatter_opts => #{ + max_length => 1000 + } + } + }} + ]}, + {service_urls, #{ + 'Repository' => <<"http://dmt:8022/v1/domain/repository">>, + 'RepositoryClient' => <<"http://dmt:8022/v1/domain/repository_client">>, + 'AuthorManagement' => <<"http://dmt:8022/v1/domain/author">> + }} + ]), + #{} + }; +start_app(party_management = AppName) -> + { + start_app(AppName, [ + {scoper_event_handler_options, #{ + event_handler_opts => #{ + formatter_opts => #{ + max_length => 1000 + } + } + }}, + {services, #{ + accounter => <<"http://shumway:8022/accounter">>, + party_management => #{ + url => <<"http://party-management:8022/v1/processing/partymgmt">>, + transport_opts => #{ + pool => party_management, + max_connections => 300 + } + } + }} + ]), + #{} + }; +start_app(AppName) -> + {genlib_app:start_application(AppName), #{}}. + +-spec start_app(app_name(), list()) -> [app_name()]. +start_app(cowboy = AppName, Env) -> + #{ + listener_ref := Ref, + acceptors_count := Count, + transport_opts := TransOpt, + proto_opts := ProtoOpt + } = Env, + {ok, _} = cowboy:start_clear(Ref, [{num_acceptors, Count} | TransOpt], ProtoOpt), + [AppName]; +start_app(AppName, Env) -> + genlib_app:start_application_with(AppName, Env). + +-spec start_apps([app_name() | {app_name(), list()}]) -> {[app_name()], map()}. +start_apps(Apps) -> + lists:foldl( + fun + ({AppName, Env}, {AppsAcc, RetAcc}) -> + {lists:reverse(start_app(AppName, Env)) ++ AppsAcc, RetAcc}; + (AppName, {AppsAcc, RetAcc}) -> + {Apps0, Ret0} = start_app(AppName), + {lists:reverse(Apps0) ++ AppsAcc, maps:merge(Ret0, RetAcc)} + end, + {[], #{}}, + Apps + ). + +-type config() :: [{atom(), term()}]. +-type test_case_name() :: atom(). +-type group_name() :: atom(). + +-spec cfg(atom(), config()) -> term(). +cfg(Key, Config) -> + case lists:keyfind(Key, 1, Config) of + {Key, V} -> V; + _ -> undefined + end. + +%% + +-spec create_client() -> pm_client_api:t(). +create_client() -> + create_client_w_context(woody_context:new()). + +-spec create_client(woody:trace_id()) -> pm_client_api:t(). +create_client(TraceID) -> + create_client_w_context(woody_context:new(TraceID)). + +create_client_w_context(WoodyCtx) -> + pm_client_api:new(WoodyCtx). diff --git a/apps/party_management/test/pm_party_tests_SUITE.erl b/apps/party_management/test/pm_party_tests_SUITE.erl new file mode 100644 index 00000000..d1779209 --- /dev/null +++ b/apps/party_management/test/pm_party_tests_SUITE.erl @@ -0,0 +1,1485 @@ +-module(pm_party_tests_SUITE). + +-include_lib("party_management/test/pm_ct_domain.hrl"). +-include_lib("party_management/include/domain.hrl"). +-include_lib("common_test/include/ct.hrl"). +-include_lib("stdlib/include/assert.hrl"). +-include_lib("damsel/include/dmsl_payproc_thrift.hrl"). +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). +-include_lib("damsel/include/dmsl_base_thrift.hrl"). +-include_lib("damsel/include/dmsl_limiter_config_thrift.hrl"). +-include_lib("damsel/include/dmsl_domain_conf_v2_thrift.hrl"). + +-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]). + +-export([get_shop_account_simple/1]). +-export([get_wallet_account_simple/1]). +-export([get_account_state_simple/1]). +-export([get_shop_account/1]). +-export([get_shop_account_non_existant_version/1]). +-export([get_wallet_account/1]). +-export([get_wallet_account_non_existant_version/1]). +-export([get_account_state/1]). +-export([get_account_state_non_existant_version/1]). +-export([get_account_state_account_notfound/1]). + +-export([compute_terms_ok/1]). +-export([compute_terms_hierarchy_not_found/1]). +-export([compute_payment_institution/1]). + +-export([compute_provider_ok/1]). +-export([compute_provider_not_found/1]). +-export([compute_provider_terminal_terms_ok/1]). +-export([compute_provider_terminal_terms_global_allow_ok/1]). +-export([compute_provider_terminal_terms_not_found/1]). +-export([compute_provider_terminal_terms_undefined_terms/1]). +-export([compute_provider_terminal_ok/1]). +-export([compute_provider_terminal_empty_varset_ok/1]). +-export([compute_provider_terminal_not_found/1]). +-export([compute_globals_ok/1]). +-export([compute_payment_routing_ruleset_ok/1]). +-export([compute_payment_routing_ruleset_irreducible/1]). +-export([compute_payment_routing_ruleset_not_found/1]). +-export([compute_payment_routing_ruleset_with_trust_level_ok/1]). + +-export([compute_pred_w_partial_all_of/1]). +-export([compute_pred_w_irreducible_criterion/1]). +-export([compute_pred_w_partially_irreducible_criterion/1]). + +%% tests descriptions + +-type config() :: pm_ct_helper:config(). +-type test_case_name() :: pm_ct_helper:test_case_name(). +-type group_name() :: pm_ct_helper:group_name(). + +-define(assert_different_term_sets(T1, T2), + case T1 =:= T2 of + true -> error({equal_term_sets, T1, T2}); + false -> ok + end +). + +cfg(Key, C) -> + pm_ct_helper:cfg(Key, C). + +-spec all() -> [{group, group_name()}]. +all() -> + [ + {group, accounts}, + {group, compute}, + {group, terms} + ]. + +-spec groups() -> [{group_name(), list(), [test_case_name()]}]. +groups() -> + [ + {accounts, [parallel], [ + get_shop_account_simple, + get_wallet_account_simple, + get_account_state_simple, + get_shop_account, + get_shop_account_non_existant_version, + get_wallet_account, + get_wallet_account_non_existant_version, + get_account_state, + get_account_state_non_existant_version, + get_account_state_account_notfound + ]}, + {compute, [parallel], [ + compute_terms_ok, + compute_terms_hierarchy_not_found, + compute_payment_institution, + compute_provider_ok, + compute_provider_not_found, + compute_provider_terminal_terms_ok, + compute_provider_terminal_terms_global_allow_ok, + compute_provider_terminal_terms_not_found, + compute_provider_terminal_terms_undefined_terms, + compute_provider_terminal_ok, + compute_provider_terminal_empty_varset_ok, + compute_provider_terminal_not_found, + compute_globals_ok, + compute_payment_routing_ruleset_ok, + compute_payment_routing_ruleset_with_trust_level_ok, + compute_payment_routing_ruleset_irreducible, + compute_payment_routing_ruleset_not_found + ]}, + {terms, [sequence], [ + compute_pred_w_partial_all_of, + compute_pred_w_irreducible_criterion, + compute_pred_w_partially_irreducible_criterion + ]} + ]. + +%% starting/stopping + +-spec init_per_suite(config()) -> config(). +init_per_suite(C) -> + {Apps, _Ret} = pm_ct_helper:start_apps([woody, scoper, dmt_client, party_management]), + PartyRef = ?party(list_to_binary(lists:concat(["party.", erlang:system_time()]))), + LimitsRev = pm_domain:upsert(constuct_limits_domain_fixture()), + _Rev = pm_domain:upsert(construct_domain_fixture(PartyRef, LimitsRev)), + [{apps, Apps}, {party_ref, PartyRef} | C]. + +-spec end_per_suite(config()) -> _. +end_per_suite(C) -> + [application:stop(App) || App <- cfg(apps, C)]. + +%% tests + +-spec init_per_group(group_name(), config()) -> config(). +init_per_group(_Group, C) -> + ApiClient = pm_ct_helper:create_client(), + Client = pm_client_party:start(cfg(party_ref, C), ApiClient), + [{client, Client} | C]. + +-spec end_per_group(group_name(), config()) -> _. +end_per_group(_Group, C) -> + pm_client_party:stop(cfg(client, C)). + +-spec init_per_testcase(test_case_name(), config()) -> config(). +init_per_testcase(_Name, C) -> + C. + +-spec end_per_testcase(test_case_name(), config()) -> _. +end_per_testcase(_Name, _C) -> + ok. + +%% + +-define(SHOP_ID, <<"SHOP1">>). +-define(WALLET_ID, <<"WALLET1">>). + +-define(WRONG_DMT_OBJ_ID, 99999). + +-spec get_shop_account_simple(config()) -> _ | no_return(). +-spec get_wallet_account_simple(config()) -> _ | no_return(). +-spec get_account_state_simple(config()) -> _ | no_return(). +-spec get_shop_account(config()) -> _ | no_return(). +-spec get_shop_account_non_existant_version(config()) -> _ | no_return(). +-spec get_wallet_account(config()) -> _ | no_return(). +-spec get_wallet_account_non_existant_version(config()) -> _ | no_return(). +-spec get_account_state(config()) -> _ | no_return(). +-spec get_account_state_non_existant_version(config()) -> _ | no_return(). +-spec get_account_state_account_notfound(config()) -> _ | no_return(). + +-spec compute_terms_ok(config()) -> _ | no_return(). +-spec compute_terms_hierarchy_not_found(config()) -> _ | no_return(). +-spec compute_payment_institution(config()) -> _ | no_return(). + +-spec compute_provider_ok(config()) -> _ | no_return(). +-spec compute_provider_not_found(config()) -> _ | no_return(). +-spec compute_provider_terminal_terms_ok(config()) -> _ | no_return(). +-spec compute_provider_terminal_terms_global_allow_ok(config()) -> _ | no_return(). +-spec compute_provider_terminal_terms_not_found(config()) -> _ | no_return(). +-spec compute_provider_terminal_terms_undefined_terms(config()) -> _ | no_return(). +-spec compute_provider_terminal_ok(config()) -> _ | no_return(). +-spec compute_provider_terminal_empty_varset_ok(config()) -> _ | no_return(). +-spec compute_provider_terminal_not_found(config()) -> _ | no_return(). +-spec compute_globals_ok(config()) -> _ | no_return(). +-spec compute_payment_routing_ruleset_ok(config()) -> _ | no_return(). +-spec compute_payment_routing_ruleset_irreducible(config()) -> _ | no_return(). +-spec compute_payment_routing_ruleset_not_found(config()) -> _ | no_return(). + +-spec compute_pred_w_partial_all_of(config()) -> _ | no_return(). +-spec compute_pred_w_irreducible_criterion(config()) -> _ | no_return(). +-spec compute_pred_w_partially_irreducible_criterion(config()) -> _ | no_return(). + +%% Accounts + +-define(NON_EXISTANT_DOMAIN_REVISION, 42_000_000). +-define(NON_EXISTANT_ACCOUNT_ID, 42_000). + +get_shop_account_simple(C) -> + Client = cfg(client, C), + ?assertMatch( + #domain_ShopAccount{}, + pm_client_party:get_shop_account_simple(?shop(?SHOP_ID), Client) + ). + +get_wallet_account_simple(C) -> + Client = cfg(client, C), + ?assertMatch( + #domain_WalletAccount{}, + pm_client_party:get_wallet_account_simple(?wallet(?WALLET_ID), Client) + ). + +get_account_state_simple(C) -> + Client = cfg(client, C), + DomainRevision = pm_domain:head(), + #domain_ShopAccount{settlement = AccountID} = + pm_client_party:get_shop_account(?shop(?SHOP_ID), DomainRevision, Client), + ?assertMatch( + #payproc_AccountState{account_id = AccountID}, + pm_client_party:get_account_state_simple(AccountID, Client) + ). + +get_shop_account(C) -> + Client = cfg(client, C), + DomainRevision = pm_domain:head(), + ?assertMatch( + #domain_ShopAccount{}, + pm_client_party:get_shop_account(?shop(?SHOP_ID), DomainRevision, Client) + ). + +get_shop_account_non_existant_version(C) -> + Client = cfg(client, C), + ?assertMatch( + {exception, #payproc_PartyNotFound{}}, + pm_client_party:get_shop_account(?shop(?SHOP_ID), ?NON_EXISTANT_DOMAIN_REVISION, Client) + ). + +get_wallet_account(C) -> + Client = cfg(client, C), + DomainRevision = pm_domain:head(), + ?assertMatch( + #domain_WalletAccount{}, + pm_client_party:get_wallet_account(?wallet(?WALLET_ID), DomainRevision, Client) + ). + +get_wallet_account_non_existant_version(C) -> + Client = cfg(client, C), + ?assertMatch( + {exception, #payproc_PartyNotFound{}}, + pm_client_party:get_wallet_account(?wallet(?WALLET_ID), ?NON_EXISTANT_DOMAIN_REVISION, Client) + ). + +get_account_state(C) -> + Client = cfg(client, C), + DomainRevision = pm_domain:head(), + #domain_ShopAccount{settlement = AccountID} = + pm_client_party:get_shop_account(?shop(?SHOP_ID), DomainRevision, Client), + ?assertMatch( + #payproc_AccountState{account_id = AccountID}, + pm_client_party:get_account_state(AccountID, DomainRevision, Client) + ). + +get_account_state_non_existant_version(C) -> + Client = cfg(client, C), + ?assertMatch( + {exception, #payproc_PartyNotFound{}}, + pm_client_party:get_account_state(42, ?NON_EXISTANT_DOMAIN_REVISION, Client) + ). + +get_account_state_account_notfound(C) -> + Client = cfg(client, C), + DomainRevision = pm_domain:head(), + ?assertMatch( + {exception, #payproc_AccountNotFound{}}, + pm_client_party:get_account_state(?NON_EXISTANT_ACCOUNT_ID, DomainRevision, Client) + ). + +%% + +compute_terms_ok(C) -> + Client = cfg(client, C), + DomainRevision = pm_domain:head(), + Varset = #payproc_Varset{ + payment_tool = {bank_card, #domain_BankCard{token = <<>>, bin = <<>>, last_digits = <<>>}}, + currency = ?cur(<<"RUB">>), + amount = ?cash(100, <<"RUB">>), + party_ref = ?party(<<"PARTYID1">>) + }, + ?assertMatch( + #domain_TermSet{ + payments = #domain_PaymentsServiceTerms{ + currencies = {value, _}, + categories = {value, _}, + payment_methods = {value, _}, + cash_limit = {value, _} + } + }, + pm_client_party:compute_terms(?trms(4), DomainRevision, Varset, Client) + ). + +compute_terms_hierarchy_not_found(C) -> + Client = cfg(client, C), + DomainRevision = pm_domain:head(), + ?assertMatch( + {exception, #payproc_TermSetHierarchyNotFound{}}, + pm_client_party:compute_terms(?trms(42), DomainRevision, #payproc_Varset{}, Client) + ). + +compute_payment_institution(C) -> + Client = cfg(client, C), + DomainRevision = pm_domain:head(), + TermsFun = fun(PartyRef) -> + #domain_PaymentInstitution{} = + pm_client_party:compute_payment_institution( + ?pinst(4), + DomainRevision, + #payproc_Varset{party_ref = PartyRef}, + Client + ) + end, + T1 = TermsFun(?party(<<"12345">>)), + T2 = TermsFun(?party(<<"67890">>)), + ?assert_different_term_sets(T1, T2). + +%% Compute providers + +compute_provider_ok(C) -> + Client = cfg(client, C), + DomainRevision = pm_domain:head(), + Varset = #payproc_Varset{ + currency = ?cur(<<"RUB">>) + }, + CashFlow = ?cfpost( + {system, settlement}, + {provider, settlement}, + {product, + {min_of, + ?ordset([ + ?fixed(10, <<"RUB">>), + ?share(5, 100, operation_amount, round_half_towards_zero) + ])}} + ), + #domain_Provider{ + terms = #domain_ProvisionTermSet{ + payments = #domain_PaymentsProvisionTerms{ + allow = {constant, true}, + cash_flow = {value, [CashFlow]} + }, + recurrent_paytools = #domain_RecurrentPaytoolsProvisionTerms{ + cash_value = {value, ?cash(1000, <<"RUB">>)} + }, + extension = #domain_ExtendedProvisionTerms{ + skip_recurrent = true + } + } + } = pm_client_party:compute_provider(?prv(1), DomainRevision, Varset, Client). + +compute_provider_not_found(C) -> + Client = cfg(client, C), + DomainRevision = pm_domain:head(), + {exception, #payproc_ProviderNotFound{}} = + (catch pm_client_party:compute_provider( + ?prv(?WRONG_DMT_OBJ_ID), DomainRevision, #payproc_Varset{}, Client + )). + +compute_provider_terminal_terms_ok(C) -> + Client = cfg(client, C), + DomainRevision = pm_domain:head(), + Varset = #payproc_Varset{ + payment_tool = {bank_card, #domain_BankCard{token = <<>>, bin = <<>>, last_digits = <<>>}}, + currency = ?cur(<<"RUB">>) + }, + CashFlow = ?cfpost( + {system, settlement}, + {provider, settlement}, + {product, + {min_of, + ?ordset([ + ?fixed(10, <<"RUB">>), + ?share(5, 100, operation_amount, round_half_towards_zero) + ])}} + ), + PaymentMethods = ?ordset([?pmt(bank_card, ?bank_card(<<"visa">>))]), + #domain_ProvisionTermSet{ + payments = #domain_PaymentsProvisionTerms{ + cash_flow = {value, [CashFlow]}, + payment_methods = {value, PaymentMethods}, + turnover_limits = + {value, [ + %% In ordset fashion + #domain_TurnoverLimit{ + ref = ?lim(<<"p_card_day_count">>), + upper_boundary = 1, + domain_revision = _ + }, + #domain_TurnoverLimit{ + ref = ?lim(<<"payment_card_month_amount_rub">>), + upper_boundary = 7500000, + domain_revision = _ + }, + #domain_TurnoverLimit{ + ref = ?lim(<<"payment_card_month_count">>), + upper_boundary = 10, + domain_revision = _ + }, + #domain_TurnoverLimit{ + ref = ?lim(<<"payment_day_amount_rub">>), + upper_boundary = 5000000, + domain_revision = _ + } + ]}, + allow_exchange = {constant, true} + }, + recurrent_paytools = #domain_RecurrentPaytoolsProvisionTerms{ + cash_value = {value, ?cash(1000, <<"RUB">>)} + }, + extension = #domain_ExtendedProvisionTerms{ + skip_recurrent = true + } + } = pm_client_party:compute_provider_terminal_terms( + ?prv(1), ?trm(1), DomainRevision, Varset, Client + ). + +compute_provider_terminal_terms_global_allow_ok(C) -> + Client = cfg(client, C), + DomainRevision = pm_domain:head(), + Varset0 = #payproc_Varset{ + amount = ?cash(100, <<"RUB">>), + party_ref = ?party(<<"PARTYID1">>) + }, + ?assertEqual( + #domain_ProvisionTermSet{ + payments = #domain_PaymentsProvisionTerms{ + allow = {constant, false}, + global_allow = {constant, false} + } + }, + pm_client_party:compute_provider_terminal_terms( + ?prv(3), ?trm(5), DomainRevision, Varset0, Client + ) + ), + Varset1 = Varset0#payproc_Varset{party_ref = ?party(<<"PARTYID2">>)}, + ?assertEqual( + #domain_ProvisionTermSet{ + payments = #domain_PaymentsProvisionTerms{ + allow = {constant, true}, + global_allow = {constant, false} + } + }, + pm_client_party:compute_provider_terminal_terms( + ?prv(3), ?trm(5), DomainRevision, Varset1, Client + ) + ), + Varset2 = Varset0#payproc_Varset{amount = ?cash(101, <<"RUB">>)}, + ?assertEqual( + #domain_ProvisionTermSet{ + payments = #domain_PaymentsProvisionTerms{ + allow = {constant, false}, + global_allow = {constant, true} + } + }, + pm_client_party:compute_provider_terminal_terms( + ?prv(3), ?trm(5), DomainRevision, Varset2, Client + ) + ). + +compute_provider_terminal_terms_not_found(C) -> + Client = cfg(client, C), + DomainRevision = pm_domain:head(), + {exception, #payproc_TerminalNotFound{}} = + pm_client_party:compute_provider_terminal_terms( + ?prv(1), + ?trm(?WRONG_DMT_OBJ_ID), + DomainRevision, + #payproc_Varset{}, + Client + ), + {exception, #payproc_ProviderNotFound{}} = + pm_client_party:compute_provider_terminal_terms( + ?prv(?WRONG_DMT_OBJ_ID), + ?trm(1), + DomainRevision, + #payproc_Varset{}, + Client + ), + {exception, #payproc_ProviderNotFound{}} = + pm_client_party:compute_provider_terminal_terms( + ?prv(?WRONG_DMT_OBJ_ID), + ?trm(?WRONG_DMT_OBJ_ID), + DomainRevision, + #payproc_Varset{}, + Client + ). + +compute_provider_terminal_terms_undefined_terms(C) -> + Client = cfg(client, C), + DomainRevision = pm_domain:head(), + ?assertMatch( + {exception, #payproc_ProvisionTermSetUndefined{}}, + pm_client_party:compute_provider_terminal_terms( + ?prv(2), + ?trm(4), + DomainRevision, + #payproc_Varset{}, + Client + ) + ). + +compute_provider_terminal_ok(C) -> + Client = cfg(client, C), + Revision = pm_domain:head(), + Varset = #payproc_Varset{ + currency = ?cur(<<"RUB">>) + }, + ExpectedCashflow = ?cfpost( + {system, settlement}, + {provider, settlement}, + {product, + {min_of, + ?ordset([ + ?fixed(10, <<"RUB">>), + ?share(5, 100, operation_amount, round_half_towards_zero) + ])}} + ), + ExpectedPaymentMethods = ?ordset([ + ?pmt(bank_card, ?bank_card(<<"visa">>)) + ]), + ?assertMatch( + #payproc_ProviderTerminal{ + ref = ?trm(1), + name = <<"Brominal 1">>, + description = <<"Brominal 1">>, + provider = #payproc_ProviderDetails{ + ref = ?prv(1), + name = <<"Brovider">>, + description = <<"A provider but bro">> + }, + proxy = #domain_ProxyDefinition{ + name = <<"Dummy proxy">>, + url = <<"http://dummy.proxy/">>, + options = #{ + <<"proxy">> := <<"def">>, + <<"pro">> := <<"vader">>, + <<"term">> := <<"inal">>, + <<"override_proxy">> := <<"proxydef">>, + <<"override_provider">> := <<"provider">>, + <<"override_terminal">> := <<"terminal">> + } + }, + terms = #domain_ProvisionTermSet{ + payments = #domain_PaymentsProvisionTerms{ + cash_flow = {value, [ExpectedCashflow]}, + payment_methods = {value, ExpectedPaymentMethods} + }, + recurrent_paytools = #domain_RecurrentPaytoolsProvisionTerms{ + cash_value = {value, ?cash(1000, <<"RUB">>)} + } + } + }, + pm_client_party:compute_provider_terminal(?trm(1), Revision, Varset, Client) + ). + +compute_provider_terminal_empty_varset_ok(C) -> + Client = cfg(client, C), + Revision = pm_domain:head(), + ?assertMatch( + #payproc_ProviderTerminal{ + ref = ?trm(1), + provider = #payproc_ProviderDetails{ + ref = ?prv(1) + }, + proxy = #domain_ProxyDefinition{ + url = <<"http://dummy.proxy/">>, + options = #{ + <<"override_proxy">> := <<"proxydef">>, + <<"override_provider">> := <<"provider">>, + <<"override_terminal">> := <<"terminal">> + } + }, + terms = undefined + }, + pm_client_party:compute_provider_terminal(?trm(1), Revision, undefined, Client) + ). + +compute_provider_terminal_not_found(C) -> + Client = cfg(client, C), + DomainRevision = pm_domain:head(), + ?assertMatch( + {exception, #payproc_TerminalNotFound{}}, + pm_client_party:compute_provider_terminal( + ?trm(?WRONG_DMT_OBJ_ID), + DomainRevision, + #payproc_Varset{}, + Client + ) + ). + +compute_globals_ok(C) -> + Client = cfg(client, C), + DomainRevision = pm_domain:head(), + Varset = #payproc_Varset{}, + #domain_Globals{ + external_account_set = {value, ?eas(1)} + } = pm_client_party:compute_globals(DomainRevision, Varset, Client). + +compute_payment_routing_ruleset_ok(C) -> + Client = cfg(client, C), + DomainRevision = pm_domain:head(), + Varset = #payproc_Varset{ + party_ref = ?party(<<"67890">>) + }, + #domain_RoutingRuleset{ + name = <<"Rule#1">>, + decisions = + {candidates, [ + #domain_RoutingCandidate{ + terminal = ?trm(2), + allowed = {constant, true} + }, + #domain_RoutingCandidate{ + terminal = ?trm(3), + allowed = {constant, true} + }, + #domain_RoutingCandidate{ + terminal = ?trm(1), + allowed = {constant, true} + } + ]} + } = pm_client_party:compute_routing_ruleset(?ruleset(1), DomainRevision, Varset, Client). + +-spec compute_payment_routing_ruleset_with_trust_level_ok(_) -> _. +compute_payment_routing_ruleset_with_trust_level_ok(C) -> + Client = cfg(client, C), + DomainRevision = pm_domain:head(), + Varset = #payproc_Varset{ + trust_level = well_known + }, + #domain_RoutingRuleset{ + name = <<"Rule#10">>, + decisions = + {candidates, [ + #domain_RoutingCandidate{ + terminal = ?trm(1), + allowed = {constant, true} + } + ]} + } = pm_client_party:compute_routing_ruleset(?ruleset(10), DomainRevision, Varset, Client). + +compute_payment_routing_ruleset_irreducible(C) -> + Client = cfg(client, C), + DomainRevision = pm_domain:head(), + Varset = #payproc_Varset{}, + #domain_RoutingRuleset{ + name = <<"Rule#1">>, + decisions = + {delegates, [ + #domain_RoutingDelegate{ + allowed = + {condition, {party, #domain_PartyCondition{party_ref = ?party(<<"12345">>)}}}, + ruleset = ?ruleset(2) + }, + #domain_RoutingDelegate{ + allowed = + {condition, {party, #domain_PartyCondition{party_ref = ?party(<<"67890">>)}}}, + ruleset = ?ruleset(3) + }, + #domain_RoutingDelegate{ + allowed = {constant, true}, + ruleset = ?ruleset(4) + } + ]} + } = pm_client_party:compute_routing_ruleset(?ruleset(1), DomainRevision, Varset, Client). + +compute_payment_routing_ruleset_not_found(C) -> + Client = cfg(client, C), + DomainRevision = pm_domain:head(), + {exception, #payproc_RuleSetNotFound{}} = + (catch pm_client_party:compute_routing_ruleset( + ?ruleset(5), DomainRevision, #payproc_Varset{}, Client + )). + +%% + +compute_pred_w_partial_all_of(_) -> + Revision = pm_domain:head(), + Predicate = + {all_of, [ + {constant, true}, + {condition, {currency_is, ?cur(<<"CNY">>)}}, + Cond1 = {condition, {category_is, ?cat(42)}}, + Cond2 = {condition, {shop_location_is, {url, <<"https://thisiswhyimbroke.com">>}}} + ]}, + ?assertMatch( + {all_of, [Cond1, Cond2]}, + pm_selector:reduce_predicate( + Predicate, + #{currency => ?cur(<<"CNY">>)}, + Revision + ) + ). + +compute_pred_w_irreducible_criterion(_) -> + CriterionRef = ?crit(1), + pm_ct_domain:with( + [ + pm_ct_fixture:construct_criterion( + CriterionRef, + <<"HAHA">>, + {condition, {currency_is, ?cur(<<"KZT">>)}} + ) + ], + fun(Revision) -> + ?assertMatch( + {criterion, CriterionRef}, + pm_selector:reduce_predicate({criterion, CriterionRef}, #{}, Revision) + ) + end + ). + +compute_pred_w_partially_irreducible_criterion(_) -> + CriterionRef = ?crit(1), + pm_ct_domain:with( + [ + pm_ct_fixture:construct_criterion( + CriterionRef, + <<"HAHA GOT ME">>, + {all_of, [ + {constant, true}, + {is_not, {condition, {currency_is, ?cur(<<"KZT">>)}}} + ]} + ) + ], + fun(Revision) -> + ?assertMatch( + {is_not, {condition, {currency_is, ?cur(<<"KZT">>)}}}, + pm_selector:reduce_predicate({criterion, CriterionRef}, #{}, Revision) + ) + end + ). + +%% + +-spec constuct_limits_domain_fixture() -> [pm_domain:object()]. +constuct_limits_domain_fixture() -> + [ + {limit_config, #domain_LimitConfigObject{ + ref = #domain_LimitConfigRef{id = LimitID}, + data = #limiter_config_LimitConfig{ + processor_type = <<"TurnoverProcessor">>, + started_at = <<"2000-01-01T00:00:00Z">>, + shard_size = 12, + time_range_type = {calendar, {month, #limiter_config_TimeRangeTypeCalendarMonth{}}}, + context_type = {payment_processing, #limiter_config_LimitContextTypePaymentProcessing{}}, + type = + {turnover, #limiter_config_LimitTypeTurnover{ + metric = {amount, #limiter_config_LimitTurnoverAmount{currency = <<"RUB">>}} + }}, + scopes = ordsets:from_list([{shop, #limiter_config_LimitScopeEmptyDetails{}}]), + description = <<"description">>, + op_behaviour = #limiter_config_OperationLimitBehaviour{ + invoice_payment_refund = {subtraction, #limiter_config_Subtraction{}} + } + } + }} + || LimitID <- [ + <<"payment_card_month_count">>, + <<"payment_card_month_amount_rub">>, + <<"payment_day_amount_rub">>, + <<"p_card_day_count">> + ] + ]. + +-spec construct_domain_fixture(dmsl_domain_thrift:'PartyConfigRef'(), dmsl_domain_conf_v2_thrift:'Version'()) -> + [pm_domain:object()]. +construct_domain_fixture(PartyRef, PrevRev) -> + TestTermSet = #domain_TermSet{ + payments = #domain_PaymentsServiceTerms{ + currencies = {value, ordsets:from_list([?cur(<<"RUB">>)])}, + categories = {value, ordsets:from_list([?cat(1)])} + } + }, + DefaultTermSet = #domain_TermSet{ + payments = #domain_PaymentsServiceTerms{ + currencies = + {value, + ordsets:from_list([ + ?cur(<<"RUB">>), + ?cur(<<"USD">>) + ])}, + categories = + {value, + ordsets:from_list([ + ?cat(2), + ?cat(3) + ])}, + payment_methods = + {value, + ordsets:from_list([ + ?pmt(bank_card, ?bank_card(<<"visa">>)) + ])} + } + }, + + TermSet = #domain_TermSet{ + recurrent_paytools = #domain_RecurrentPaytoolsServiceTerms{ + payment_methods = + {decisions, [ + mk_payment_decision( + {bank_card, #domain_BankCardCondition{ + definition = {issuer_bank_is, ?bank(1)} + }}, + [ + ?pmt(bank_card, ?bank_card(<<"visa">>)), + ?pmt(crypto_currency, ?crypta(<<"bitcoin">>)) + ] + ), + mk_payment_decision( + {bank_card, #domain_BankCardCondition{definition = {empty_cvv_is, true}}}, + [] + ), + mk_payment_decision( + {bank_card, #domain_BankCardCondition{}}, + [?pmt(bank_card, ?bank_card(<<"visa">>))] + ), + mk_payment_decision( + {payment_terminal, #domain_PaymentTerminalCondition{}}, + [?pmt(crypto_currency, ?crypta(<<"bitcoin">>))] + ), + #domain_PaymentMethodDecision{ + if_ = {constant, true}, + then_ = {value, ordsets:from_list([])} + } + ]} + }, + payments = #domain_PaymentsServiceTerms{ + cash_limit = + {value, #domain_CashRange{ + lower = {inclusive, #domain_Cash{amount = 1000, currency = ?cur(<<"RUB">>)}}, + upper = {exclusive, #domain_Cash{amount = 4200000, currency = ?cur(<<"RUB">>)}} + }}, + fees = + {value, [ + ?cfpost( + {merchant, settlement}, + {system, settlement}, + ?share(45, 1000, operation_amount) + ) + ]} + }, + wallets = #domain_WalletServiceTerms{ + currencies = {value, ordsets:from_list([?cur(<<"RUB">>)])}, + wallet_limit = + {decisions, [ + #domain_CashLimitDecision{ + if_ = {condition, {currency_is, ?cur(<<"RUB">>)}}, + then_ = + {value, + ?cashrng( + {inclusive, ?cash(0, <<"RUB">>)}, + {exclusive, ?cash(5000001, <<"RUB">>)} + )} + }, + #domain_CashLimitDecision{ + if_ = {condition, {currency_is, ?cur(<<"USD">>)}}, + then_ = + {value, + ?cashrng( + {inclusive, ?cash(0, <<"USD">>)}, + {exclusive, ?cash(10000001, <<"USD">>)} + )} + } + ]}, + withdrawals = #domain_WithdrawalServiceTerms{ + methods = + {decisions, [ + mk_payment_decision( + {bank_card, #domain_BankCardCondition{ + definition = { + payment_system, + #domain_PaymentSystemCondition{ + payment_system_is = ?pmt_sys(<<"visa">>) + } + } + }}, + [?pmt(bank_card, ?bank_card(<<"visa">>))] + ), + mk_payment_decision( + {digital_wallet, #domain_DigitalWalletCondition{ + definition = + {payment_service_is, ?pmt_srv(<<"qiwi">>)} + }}, + [?pmt(bank_card, ?bank_card(<<"visa">>))] + ), + mk_payment_decision( + {mobile_commerce, #domain_MobileCommerceCondition{ + definition = {operator_is, ?mob(<<"mts">>)} + }}, + [?pmt(bank_card, ?bank_card(<<"visa">>))] + ), + mk_payment_decision( + {crypto_currency, #domain_CryptoCurrencyCondition{ + definition = {crypto_currency_is, ?crypta(<<"bitcoin">>)} + }}, + [?pmt(bank_card, ?bank_card(<<"visa">>))] + ), + #domain_PaymentMethodDecision{ + if_ = {constant, true}, + then_ = {value, ordsets:from_list([])} + } + ]} + } + } + }, + Decision1 = + {delegates, [ + #domain_RoutingDelegate{ + allowed = + {condition, {party, #domain_PartyCondition{party_ref = ?party(<<"12345">>)}}}, + ruleset = ?ruleset(2) + }, + #domain_RoutingDelegate{ + allowed = + {condition, {party, #domain_PartyCondition{party_ref = ?party(<<"67890">>)}}}, + ruleset = ?ruleset(3) + }, + #domain_RoutingDelegate{ + allowed = {constant, true}, + ruleset = ?ruleset(4) + } + ]}, + Decision2 = + {candidates, [ + #domain_RoutingCandidate{ + allowed = {constant, true}, + terminal = ?trm(1) + } + ]}, + Decision3 = + {candidates, [ + #domain_RoutingCandidate{ + allowed = + {condition, {party, #domain_PartyCondition{party_ref = ?party(<<"67890">>)}}}, + terminal = ?trm(2) + }, + #domain_RoutingCandidate{ + allowed = {constant, true}, + terminal = ?trm(3) + }, + #domain_RoutingCandidate{ + allowed = {constant, true}, + terminal = ?trm(1) + } + ]}, + Decision4 = + {candidates, [ + #domain_RoutingCandidate{ + allowed = {constant, true}, + terminal = ?trm(3) + } + ]}, + Decision10 = + {candidates, [ + #domain_RoutingCandidate{ + allowed = {condition, {trust_level_is, well_known}}, + terminal = ?trm(1) + } + ]}, + [ + pm_ct_fixture:construct_currency(?cur(<<"RUB">>)), + pm_ct_fixture:construct_currency(?cur(<<"USD">>)), + pm_ct_fixture:construct_currency(?cur(<<"KZT">>)), + + pm_ct_fixture:construct_category(?cat(1), <<"Test category">>, test), + pm_ct_fixture:construct_category(?cat(2), <<"Generic Store">>, live), + pm_ct_fixture:construct_category(?cat(3), <<"Guns & Booze">>, live), + pm_ct_fixture:construct_category(?cat(4), <<"Tech Store">>, live), + pm_ct_fixture:construct_category(?cat(5), <<"Burger Boutique">>, live), + + pm_ct_fixture:construct_payment_system(?pmt_sys(<<"visa">>), <<"Visa">>), + pm_ct_fixture:construct_payment_system(?pmt_sys(<<"mastercard">>), <<"Mastercard">>), + pm_ct_fixture:construct_payment_system(?pmt_sys(<<"maestro">>), <<"Maestro">>), + pm_ct_fixture:construct_payment_system(?pmt_sys(<<"jcb">>), <<"JCB">>), + pm_ct_fixture:construct_payment_service(?pmt_srv(<<"alipay">>), <<"Euroset">>), + pm_ct_fixture:construct_payment_service(?pmt_srv(<<"qiwi">>), <<"Qiwi">>), + pm_ct_fixture:construct_mobile_operator(?mob(<<"mts">>), <<"MTS">>), + pm_ct_fixture:construct_crypto_currency(?crypta(<<"bitcoin">>), <<"Bitcoin">>), + pm_ct_fixture:construct_tokenized_service(?token_srv(<<"applepay">>), <<"Apple Pay">>), + pm_ct_fixture:construct_payment_service(?pmt_srv(<<"generic">>), <<"Generic">>), + pm_ct_fixture:construct_payment_service(?pmt_srv(<<"generic1">>), <<"Generic1">>), + + pm_ct_fixture:construct_payment_method(?pmt(bank_card, ?bank_card(<<"visa">>))), + pm_ct_fixture:construct_payment_method(?pmt(bank_card, ?bank_card(<<"mastercard">>))), + pm_ct_fixture:construct_payment_method(?pmt(bank_card, ?bank_card(<<"maestro">>))), + pm_ct_fixture:construct_payment_method(?pmt(bank_card, ?bank_card(<<"jcb">>))), + pm_ct_fixture:construct_payment_method( + ?pmt(bank_card, ?token_bank_card(<<"visa">>, <<"applepay">>)) + ), + pm_ct_fixture:construct_payment_method(?pmt(payment_terminal, ?pmt_srv(<<"alipay">>))), + pm_ct_fixture:construct_payment_method(?pmt(digital_wallet, ?pmt_srv(<<"qiwi">>))), + pm_ct_fixture:construct_payment_method(?pmt(mobile, ?mob(<<"mts">>))), + pm_ct_fixture:construct_payment_method(?pmt(crypto_currency, ?crypta(<<"bitcoin">>))), + pm_ct_fixture:construct_payment_method(?pmt(generic, ?gnrc(?pmt_srv(<<"generic">>)))), + pm_ct_fixture:construct_payment_method(?pmt(generic, ?gnrc(?pmt_srv(<<"generic1">>)))), + + pm_ct_fixture:construct_payment_method(?pmt(payment_terminal, ?pmt_srv(<<"euroset">>))), + pm_ct_fixture:construct_payment_method(?pmt(bank_card, ?bank_card_no_cvv(<<"visa">>))), + + pm_ct_fixture:construct_proxy( + ?prx(1), + <<"Dummy proxy">>, + <<"http://dummy.proxy/">>, + #{ + <<"proxy">> => <<"def">>, + <<"override_proxy">> => <<"proxydef">>, + <<"override_provider">> => <<"proxydef">>, + <<"override_terminal">> => <<"proxydef">> + } + ), + + pm_ct_fixture:construct_inspector(?insp(1), <<"Dummy Inspector">>, ?prx(1)), + pm_ct_fixture:construct_system_account_set(?sas(1)), + pm_ct_fixture:construct_system_account_set(?sas(2)), + pm_ct_fixture:construct_external_account_set(?eas(1)), + + pm_ct_fixture:construct_business_schedule(?bussched(1)), + + pm_ct_fixture:construct_payment_routing_ruleset(?ruleset(1), <<"Rule#1">>, Decision1), + pm_ct_fixture:construct_payment_routing_ruleset(?ruleset(2), <<"Rule#2">>, Decision2), + pm_ct_fixture:construct_payment_routing_ruleset(?ruleset(3), <<"Rule#3">>, Decision3), + pm_ct_fixture:construct_payment_routing_ruleset(?ruleset(4), <<"Rule#4">>, Decision4), + pm_ct_fixture:construct_payment_routing_ruleset(?ruleset(10), <<"Rule#10">>, Decision10), + + {payment_institution, #domain_PaymentInstitutionObject{ + ref = ?pinst(1), + data = #domain_PaymentInstitution{ + name = <<"Test Inc.">>, + system_account_set = {value, ?sas(1)}, + inspector = {value, ?insp(1)}, + residences = [], + realm = test + } + }}, + + {payment_institution, #domain_PaymentInstitutionObject{ + ref = ?pinst(2), + data = #domain_PaymentInstitution{ + name = <<"Chetky Payments Inc.">>, + system_account_set = {value, ?sas(2)}, + inspector = {value, ?insp(1)}, + residences = [], + realm = live + } + }}, + + {payment_institution, #domain_PaymentInstitutionObject{ + ref = ?pinst(3), + data = #domain_PaymentInstitution{ + name = <<"Chetky Payments Inc.">>, + system_account_set = {value, ?sas(2)}, + inspector = {value, ?insp(1)}, + residences = [], + realm = live + } + }}, + + {payment_institution, #domain_PaymentInstitutionObject{ + ref = ?pinst(4), + data = #domain_PaymentInstitution{ + name = <<"Chetky Payments Inc.">>, + system_account_set = + {decisions, [ + #domain_SystemAccountSetDecision{ + if_ = ?partycond(<<"12345">>, undefined), + then_ = + {value, ?sas(2)} + }, + #domain_SystemAccountSetDecision{ + if_ = ?partycond(<<"67890">>, undefined), + then_ = + {value, ?sas(1)} + } + ]}, + inspector = {value, ?insp(1)}, + residences = [], + realm = live + } + }}, + + {payment_institution, #domain_PaymentInstitutionObject{ + ref = ?pinst(5), + data = #domain_PaymentInstitution{ + name = <<"N.E. Chetky Payments GmbH">>, + system_account_set = {value, ?sas(2)}, + inspector = {value, ?insp(1)}, + residences = [], + realm = live + } + }}, + + %% Party, shop and wallet + pm_ct_fixture:construct_party(?party(<<"12345">>)), + pm_ct_fixture:construct_party(?party(<<"67890">>)), + pm_ct_fixture:construct_party(?party(<<"PARTYID1">>)), + pm_ct_fixture:construct_party(?party(<<"PARTYID2">>)), + pm_ct_fixture:construct_party(PartyRef), + pm_ct_fixture:construct_shop( + ?shop(?SHOP_ID), + ?pinst(1), + ?trms(2), + pm_ct_fixture:construct_shop_account(<<"RUB">>), + PartyRef, + <<"http://example.com">>, + ?cat(1) + ), + pm_ct_fixture:construct_wallet( + ?wallet(?WALLET_ID), + ?pinst(1), + ?trms(2), + pm_ct_fixture:construct_wallet_account(<<"RUB">>), + PartyRef + ), + + {globals, #domain_GlobalsObject{ + ref = #domain_GlobalsRef{}, + data = #domain_Globals{ + external_account_set = + {decisions, [ + #domain_ExternalAccountSetDecision{ + if_ = {constant, true}, + then_ = {value, ?eas(1)} + } + ]}, + payment_institutions = ?ordset([?pinst(1), ?pinst(2), ?pinst(5)]) + } + }}, + pm_ct_fixture:construct_term_set_hierarchy(?trms(1), undefined, TestTermSet), + pm_ct_fixture:construct_term_set_hierarchy(?trms(2), undefined, DefaultTermSet), + pm_ct_fixture:construct_term_set_hierarchy(?trms(3), ?trms(2), TermSet), + pm_ct_fixture:construct_term_set_hierarchy( + ?trms(4), + ?trms(3), + #domain_TermSet{ + payments = #domain_PaymentsServiceTerms{ + currencies = + {value, + ordsets:from_list([ + ?cur(<<"RUB">>) + ])}, + categories = + {value, + ordsets:from_list([ + ?cat(2) + ])}, + payment_methods = + {value, + ordsets:from_list([ + ?pmt(bank_card, ?bank_card(<<"visa">>)) + ])} + } + } + ), + + {bank, #domain_BankObject{ + ref = ?bank(1), + data = #domain_Bank{ + name = <<"Test BIN range">>, + description = <<"Test BIN range">>, + bins = ordsets:from_list([<<"1234">>, <<"5678">>]) + } + }}, + + {provider, #domain_ProviderObject{ + ref = ?prv(1), + data = #domain_Provider{ + name = <<"Brovider">>, + description = <<"A provider but bro">>, + realm = test, + proxy = #domain_Proxy{ + ref = ?prx(1), + additional = #{ + <<"pro">> => <<"vader">>, + <<"override_provider">> => <<"provider">>, + <<"override_terminal">> => <<"provider">> + } + }, + accounts = pm_ct_fixture:construct_provider_account_set([?cur(<<"RUB">>)]), + terms = #domain_ProvisionTermSet{ + payments = #domain_PaymentsProvisionTerms{ + allow = {constant, true}, + currencies = {value, ?ordset([?cur(<<"RUB">>)])}, + categories = {value, ?ordset([?cat(1)])}, + payment_methods = + {value, + ?ordset([ + ?pmt(bank_card, ?bank_card(<<"visa">>)), + ?pmt(bank_card, ?bank_card(<<"mastercard">>)) + ])}, + cash_limit = + {value, + ?cashrng( + {inclusive, ?cash(1000, <<"RUB">>)}, + {exclusive, ?cash(1000000000, <<"RUB">>)} + )}, + cash_flow = + {decisions, [ + #domain_CashFlowDecision{ + if_ = {condition, {currency_is, ?cur(<<"RUB">>)}}, + then_ = + {value, [ + ?cfpost( + {system, settlement}, + {provider, settlement}, + {product, + {min_of, + ?ordset([ + ?fixed(10, <<"RUB">>), + ?share( + 5, + 100, + operation_amount, + round_half_towards_zero + ) + ])}} + ) + ]} + }, + #domain_CashFlowDecision{ + if_ = {condition, {currency_is, ?cur(<<"USD">>)}}, + then_ = + {value, [ + ?cfpost( + {system, settlement}, + {provider, settlement}, + {product, + {min_of, + ?ordset([ + ?fixed(10, <<"USD">>), + ?share( + 5, + 100, + operation_amount, + round_half_towards_zero + ) + ])}} + ) + ]} + } + ]}, + turnover_limits = + {decisions, [ + #domain_TurnoverLimitDecision{ + if_ = + {condition, + {payment_tool, + {bank_card, #domain_BankCardCondition{ + definition = + {issuer_bank_is, #domain_BankRef{id = 1}} + }}}}, + then_ = + {value, + ?ordset([ + #domain_TurnoverLimit{ + ref = ?lim(<<"payment_card_month_count">>), + upper_boundary = 5, + domain_revision = PrevRev + }, + %% Common limits + #domain_TurnoverLimit{ + ref = ?lim(<<"payment_card_month_amount_rub">>), + upper_boundary = 7500000, + domain_revision = PrevRev + }, + #domain_TurnoverLimit{ + ref = ?lim(<<"payment_day_amount_rub">>), + upper_boundary = 5000000, + domain_revision = PrevRev + }, + #domain_TurnoverLimit{ + ref = ?lim(<<"p_card_day_count">>), + upper_boundary = 1, + domain_revision = PrevRev + } + ])} + }, + #domain_TurnoverLimitDecision{ + if_ = + {is_not, + {condition, + {payment_tool, + {bank_card, #domain_BankCardCondition{ + definition = + {issuer_bank_is, #domain_BankRef{ + id = 1 + }} + }}}}}, + then_ = + {value, + ?ordset([ + #domain_TurnoverLimit{ + ref = ?lim(<<"payment_card_month_count">>), + upper_boundary = 10, + domain_revision = PrevRev + }, + %% Common limits + #domain_TurnoverLimit{ + ref = ?lim(<<"payment_card_month_amount_rub">>), + upper_boundary = 7500000, + domain_revision = PrevRev + }, + #domain_TurnoverLimit{ + ref = ?lim(<<"payment_day_amount_rub">>), + upper_boundary = 5000000, + domain_revision = PrevRev + }, + #domain_TurnoverLimit{ + ref = ?lim(<<"p_card_day_count">>), + upper_boundary = 1, + domain_revision = PrevRev + } + ])} + } + ]} + }, + recurrent_paytools = #domain_RecurrentPaytoolsProvisionTerms{ + categories = {value, ?ordset([?cat(1)])}, + payment_methods = + {value, + ?ordset([ + ?pmt(bank_card, ?bank_card(<<"visa">>)), + ?pmt(bank_card, ?bank_card(<<"mastercard">>)) + ])}, + cash_value = + {decisions, [ + #domain_CashValueDecision{ + if_ = {condition, {currency_is, ?cur(<<"RUB">>)}}, + then_ = {value, ?cash(1000, <<"RUB">>)} + }, + #domain_CashValueDecision{ + if_ = {condition, {currency_is, ?cur(<<"USD">>)}}, + then_ = {value, ?cash(1000, <<"USD">>)} + } + ]} + }, + extension = #domain_ExtendedProvisionTerms{ + skip_recurrent = true + } + } + } + }}, + + {provider, #domain_ProviderObject{ + ref = ?prv(2), + data = #domain_Provider{ + name = <<"Provider 2">>, + description = <<"Provider without terms">>, + realm = test, + proxy = #domain_Proxy{ref = ?prx(1), additional = #{}}, + accounts = pm_ct_fixture:construct_provider_account_set([?cur(<<"RUB">>)]) + } + }}, + + {provider, #domain_ProviderObject{ + ref = ?prv(3), + data = #domain_Provider{ + name = <<"Brovider">>, + description = <<"A provider but bro">>, + realm = test, + proxy = #domain_Proxy{ + ref = ?prx(1), + additional = #{ + <<"pro">> => <<"vader">> + } + }, + terms = #domain_ProvisionTermSet{ + payments = #domain_PaymentsProvisionTerms{ + allow = ?partycond(<<"PARTYID1">>, undefined), + global_allow = + {condition, + {cost_in, + ?cashrng( + {inclusive, ?cash(100, <<"RUB">>)}, + {inclusive, ?cash(100, <<"RUB">>)} + )}} + } + } + } + }}, + + {terminal, #domain_TerminalObject{ + ref = ?trm(1), + data = #domain_Terminal{ + name = <<"Brominal 1">>, + description = <<"Brominal 1">>, + provider_ref = ?prv(1), + options = #{ + <<"term">> => <<"inal">>, + <<"override_terminal">> => <<"terminal">> + }, + terms = #domain_ProvisionTermSet{ + payments = #domain_PaymentsProvisionTerms{ + payment_methods = + {value, + ?ordset([ + ?pmt(bank_card, ?bank_card(<<"visa">>)) + ])}, + allow_exchange = {constant, true} + } + } + } + }}, + {terminal, #domain_TerminalObject{ + ref = ?trm(2), + data = #domain_Terminal{ + name = <<"Brominal 2">>, + description = <<"Brominal 2">>, + provider_ref = ?prv(1), + terms = #domain_ProvisionTermSet{ + payments = #domain_PaymentsProvisionTerms{ + payment_methods = + {value, + ?ordset([ + ?pmt(bank_card, ?bank_card(<<"visa">>)) + ])} + } + } + } + }}, + {terminal, #domain_TerminalObject{ + ref = ?trm(3), + data = #domain_Terminal{ + name = <<"Brominal 3">>, + description = <<"Brominal 3">>, + provider_ref = ?prv(1), + terms = #domain_ProvisionTermSet{ + payments = #domain_PaymentsProvisionTerms{ + payment_methods = + {value, + ?ordset([ + ?pmt(bank_card, ?bank_card(<<"visa">>)) + ])} + } + } + } + }}, + {terminal, #domain_TerminalObject{ + ref = ?trm(4), + data = #domain_Terminal{ + name = <<"Terminal 4">>, + description = <<"Terminal without terms">>, + provider_ref = ?prv(2) + } + }}, + + {terminal, #domain_TerminalObject{ + ref = ?trm(5), + data = #domain_Terminal{ + name = <<"Brominal 5">>, + description = <<"Brominal 5">>, + provider_ref = ?prv(3), + options = #{ + <<"term">> => <<"inal">>, + <<"override_terminal">> => <<"terminal">> + }, + terms = #domain_ProvisionTermSet{ + payments = #domain_PaymentsProvisionTerms{ + allow = ?partycond(<<"PARTYID2">>, undefined), + global_allow = + {condition, + {cost_in, + ?cashrng( + {inclusive, ?cash(101, <<"RUB">>)}, + {inclusive, ?cash(101, <<"RUB">>)} + )}} + } + } + } + }} + ]. + +mk_payment_decision(PaymentTool, PaymentMethods) -> + #domain_PaymentMethodDecision{ + if_ = {condition, {payment_tool, PaymentTool}}, + then_ = {value, ordsets:from_list(PaymentMethods)} + }. diff --git a/apps/pm_client/src/pm_client.app.src b/apps/pm_client/src/pm_client.app.src new file mode 100644 index 00000000..a668f281 --- /dev/null +++ b/apps/pm_client/src/pm_client.app.src @@ -0,0 +1,11 @@ +{application, pm_client, [ + {description, "Party Management client"}, + {vsn, "0"}, + {registered, []}, + {applications, [ + kernel, + stdlib, + woody, + pm_proto + ]} +]}. diff --git a/apps/pm_client/src/pm_client_api.erl b/apps/pm_client/src/pm_client_api.erl new file mode 100644 index 00000000..0cda8345 --- /dev/null +++ b/apps/pm_client/src/pm_client_api.erl @@ -0,0 +1,38 @@ +-module(pm_client_api). + +-export([new/1]). +-export([call/4]). + +-export_type([t/0]). + +%% + +-type t() :: woody_context:ctx(). + +-spec new(woody_context:ctx()) -> t(). +new(Context) -> + Context. + +-spec call(Name :: atom(), woody:func(), [any()], t()) -> {ok, _Response} | {exception, _} | {error, _}. +call(ServiceName, Function, Args, Context) -> + Service = pm_proto:get_service(ServiceName), + Request = {Service, Function, list_to_tuple(Args)}, + Opts = get_opts(ServiceName), + try + woody_client:call(Request, Opts, Context) + catch + error:Error:ST -> + {error, {Error, ST}} + end. + +get_opts(ServiceName) -> + EventHandlerOpts = genlib_app:env(party_management, scoper_event_handler_options, #{}), + Opts0 = #{ + event_handler => {scoper_woody_event_handler, EventHandlerOpts} + }, + case maps:get(ServiceName, genlib_app:env(party_management, services), undefined) of + #{} = Opts -> + maps:merge(Opts, Opts0); + _ -> + Opts0 + end. diff --git a/apps/pm_client/src/pm_client_party.erl b/apps/pm_client/src/pm_client_party.erl new file mode 100644 index 00000000..f34ab6e4 --- /dev/null +++ b/apps/pm_client/src/pm_client_party.erl @@ -0,0 +1,181 @@ +-module(pm_client_party). + +-export([start/2]). +-export([stop/1]). + +-export([compute_terms/4]). +-export([compute_payment_institution/4]). + +-export([get_shop_account_simple/2]). +-export([get_wallet_account_simple/2]). +-export([get_account_state_simple/2]). +-export([get_shop_account/3]). +-export([get_wallet_account/3]). +-export([get_account_state/3]). + +-export([compute_provider/4]). +-export([compute_provider_terminal/4]). +-export([compute_provider_terminal_terms/5]). +-export([compute_globals/3]). +-export([compute_routing_ruleset/4]). + +%% GenServer + +-behaviour(gen_server). + +-export([init/1]). +-export([handle_call/3]). +-export([handle_cast/2]). + +%% + +-type party_ref() :: dmsl_domain_thrift:'PartyConfigRef'(). +-type domain_revision() :: dmsl_domain_thrift:'DataRevision'(). +-type shop_ref() :: dmsl_domain_thrift:'ShopConfigRef'(). +-type wallet_ref() :: dmsl_domain_thrift:'WalletConfigRef'(). +-type shop_account_id() :: dmsl_domain_thrift:'AccountID'(). + +-type termset_hierarchy_ref() :: dmsl_domain_thrift:'TermSetHierarchyRef'(). +-type payment_intitution_ref() :: dmsl_domain_thrift:'PaymentInstitutionRef'(). +-type varset() :: dmsl_payproc_thrift:'Varset'(). + +-type provider_ref() :: dmsl_domain_thrift:'ProviderRef'(). +-type terminal_ref() :: dmsl_domain_thrift:'TerminalRef'(). +-type routing_ruleset_ref() :: dmsl_domain_thrift:'RoutingRulesetRef'(). + +-spec start(party_ref(), pm_client_api:t()) -> pid(). +start(PartyRef, ApiClient) -> + {ok, Pid} = gen_server:start(?MODULE, {PartyRef, ApiClient}, []), + Pid. + +-spec stop(pid()) -> ok. +stop(Client) -> + _ = exit(Client, shutdown), + ok. + +%% + +-spec compute_terms(termset_hierarchy_ref(), domain_revision(), varset(), pid()) -> + dmsl_domain_thrift:'TermSet'() | woody_error:business_error(). +compute_terms(Ref, DomainRevision, Varset, Client) -> + call(Client, 'ComputeTerms', [Ref, DomainRevision, Varset]). + +-spec compute_payment_institution(payment_intitution_ref(), domain_revision(), varset(), pid()) -> + dmsl_domain_thrift:'PaymentInstitution'() | woody_error:business_error(). +compute_payment_institution(Ref, DomainRevision, Varset, Client) -> + call(Client, 'ComputePaymentInstitution', [Ref, DomainRevision, Varset]). + +-spec get_account_state_simple(shop_account_id(), pid()) -> + dmsl_payproc_thrift:'AccountState'() | woody_error:business_error(). +get_account_state_simple(AccountID, Client) -> + call(Client, 'GetAccountStateSimple', with_party_ref([AccountID])). + +-spec get_account_state(shop_account_id(), domain_revision(), pid()) -> + dmsl_payproc_thrift:'AccountState'() | woody_error:business_error(). +get_account_state(AccountID, DomainRevision, Client) -> + call(Client, 'GetAccountState', with_party_ref([AccountID, DomainRevision])). + +-spec get_shop_account_simple(shop_ref(), pid()) -> + dmsl_domain_thrift:'ShopAccount'() | woody_error:business_error(). +get_shop_account_simple(ShopRef, Client) -> + call(Client, 'GetShopAccountSimple', with_party_ref([ShopRef])). + +-spec get_shop_account(shop_ref(), domain_revision(), pid()) -> + dmsl_domain_thrift:'ShopAccount'() | woody_error:business_error(). +get_shop_account(ShopRef, DomainRevision, Client) -> + call(Client, 'GetShopAccount', with_party_ref([ShopRef, DomainRevision])). + +-spec get_wallet_account_simple(wallet_ref(), pid()) -> + dmsl_domain_thrift:'WalletAccount'() | woody_error:business_error(). +get_wallet_account_simple(WalletRef, Client) -> + call(Client, 'GetWalletAccountSimple', with_party_ref([WalletRef])). + +-spec get_wallet_account(wallet_ref(), domain_revision(), pid()) -> + dmsl_domain_thrift:'WalletAccount'() | woody_error:business_error(). +get_wallet_account(WalletRef, DomainRevision, Client) -> + call(Client, 'GetWalletAccount', with_party_ref([WalletRef, DomainRevision])). + +-spec compute_provider(provider_ref(), domain_revision(), varset(), pid()) -> + dmsl_domain_thrift:'Provider'() | woody_error:business_error(). +compute_provider(ProviderRef, Revision, Varset, Client) -> + call(Client, 'ComputeProvider', [ProviderRef, Revision, Varset]). + +-spec compute_provider_terminal( + terminal_ref(), + domain_revision(), + varset() | undefined, + pid() +) -> dmsl_payproc_thrift:'ProviderTerminal'() | woody_error:business_error(). +compute_provider_terminal(TerminalRef, Revision, Varset, Client) -> + call(Client, 'ComputeProviderTerminal', [TerminalRef, Revision, Varset]). + +-spec compute_provider_terminal_terms( + provider_ref(), + terminal_ref(), + domain_revision(), + varset(), + pid() +) -> dmsl_domain_thrift:'ProvisionTermSet'() | woody_error:business_error(). +compute_provider_terminal_terms(ProviderRef, TerminalRef, Revision, Varset, Client) -> + Args = [ProviderRef, TerminalRef, Revision, Varset], + call(Client, 'ComputeProviderTerminalTerms', Args). + +-spec compute_globals(domain_revision(), varset(), pid()) -> + dmsl_domain_thrift:'Globals'() | woody_error:business_error(). +compute_globals(Revision, Varset, Client) -> + call(Client, 'ComputeGlobals', [Revision, Varset]). + +-spec compute_routing_ruleset(routing_ruleset_ref(), domain_revision(), varset(), pid()) -> + dmsl_domain_thrift:'RoutingRuleset'() | woody_error:business_error(). +compute_routing_ruleset(RoutingRuleSetRef, Revision, Varset, Client) -> + call(Client, 'ComputeRoutingRuleset', [RoutingRuleSetRef, Revision, Varset]). + +call(Client, Function, Args) -> + map_result_error(gen_server:call(Client, {call, Function, Args})). + +map_result_error({ok, Result}) -> + Result; +map_result_error({exception, _} = Exception) -> + Exception; +map_result_error({error, Error}) -> + error(Error). + +%% + +-record(state, { + party_ref :: party_ref(), + client :: pm_client_api:t() +}). + +-type state() :: #state{}. +-type callref() :: {pid(), Tag :: reference()}. + +-spec init({party_ref(), pm_client_api:t()}) -> {ok, state()}. +init({PartyRef, ApiClient}) -> + {ok, #state{ + party_ref = PartyRef, + client = ApiClient + }}. + +-spec handle_call(term(), callref(), state()) -> {reply, term(), state()} | {noreply, state()}. +handle_call({call, Function, ArgsIn}, _From, #state{client = Client} = St) -> + Args = lists:map( + fun + (Fun) when is_function(Fun, 1) -> Fun(St); + (Arg) -> Arg + end, + ArgsIn + ), + Result = pm_client_api:call(party_management, Function, Args, Client), + {reply, Result, St}; +handle_call(Call, _From, State) -> + _ = logger:warning("unexpected call received: ~tp", [Call]), + {noreply, State}. + +-spec handle_cast(_, state()) -> {noreply, state()}. +handle_cast(Cast, State) -> + _ = logger:warning("unexpected cast received: ~tp", [Cast]), + {noreply, State}. + +with_party_ref(Args) -> + [fun(St) -> St#state.party_ref end | Args]. diff --git a/apps/pm_proto/src/pm_proto.app.src b/apps/pm_proto/src/pm_proto.app.src new file mode 100644 index 00000000..efcf8fd2 --- /dev/null +++ b/apps/pm_proto/src/pm_proto.app.src @@ -0,0 +1,11 @@ +{application, pm_proto, [ + {description, "Processing protocol definitions"}, + {vsn, "0"}, + {registered, []}, + {applications, [ + kernel, + stdlib, + thrift, + damsel + ]} +]}. diff --git a/apps/pm_proto/src/pm_proto.erl b/apps/pm_proto/src/pm_proto.erl new file mode 100644 index 00000000..8b58b565 --- /dev/null +++ b/apps/pm_proto/src/pm_proto.erl @@ -0,0 +1,30 @@ +-module(pm_proto). + +-export([get_service/1]). + +-export([get_service_spec/1]). +-export([get_service_spec/2]). + +-export_type([service/0]). +-export_type([service_spec/0]). + +%% + +-define(VERSION_PREFIX, "/v1"). + +-type service() :: woody:service(). +-type service_spec() :: {Path :: string(), service()}. + +-spec get_service(Name :: atom()) -> service(). +get_service(party_management) -> + {dmsl_payproc_thrift, 'PartyManagement'}; +get_service(accounter) -> + {dmsl_accounter_thrift, 'Accounter'}. + +-spec get_service_spec(Name :: atom()) -> service_spec(). +get_service_spec(Name) -> + get_service_spec(Name, #{}). + +-spec get_service_spec(Name :: atom(), Opts :: #{namespace => binary()}) -> service_spec(). +get_service_spec(party_management = Name, #{}) -> + {?VERSION_PREFIX ++ "/processing/partymgmt", get_service(Name)}. diff --git a/apps/pm_proto/src/pm_proto_utils.erl b/apps/pm_proto/src/pm_proto_utils.erl new file mode 100644 index 00000000..397c58fc --- /dev/null +++ b/apps/pm_proto/src/pm_proto_utils.erl @@ -0,0 +1,163 @@ +-module(pm_proto_utils). + +-export([serialize/2]). +-export([deserialize/2]). + +-export([serialize_function_args/2]). +-export([deserialize_function_args/2]). + +-export([serialize_function_reply/2]). +-export([deserialize_function_reply/2]). + +-export([serialize_function_exception/2]). +-export([deserialize_function_exception/2]). + +-export([record_to_proplist/2]). + +%% Types + +%% TODO: move it to the thrift runtime lib? + +-type thrift_type() :: + thrift_base_type() + | thrift_collection_type() + | thrift_enum_type() + | thrift_struct_type(). + +-type thrift_base_type() :: + bool + | double + | i8 + | i16 + | i32 + | i64 + | string. + +-type thrift_collection_type() :: + {list, thrift_type()} + | {set, thrift_type()} + | {map, thrift_type(), thrift_type()}. + +-type thrift_enum_type() :: + {enum, thrift_type_ref()}. + +-type thrift_struct_type() :: + {struct, thrift_struct_flavor(), thrift_type_ref() | thrift_struct_def()}. + +-type thrift_struct_flavor() :: struct | union | exception. + +-type thrift_type_ref() :: {module(), Name :: atom()}. + +-type thrift_struct_def() :: + list({ + Tag :: pos_integer(), + Requireness :: required | optional | undefined, + Type :: thrift_struct_type(), + Name :: atom(), + Default :: any() + }). + +-type thrift_fun_ref() :: {Service :: atom(), Function :: atom()}. +-type thrift_fun_full_ref() :: {module(), thrift_fun_ref()}. +-type thrift_exception() :: tuple(). + +-export_type([thrift_type/0]). +-export_type([thrift_exception/0]). +-export_type([thrift_fun_full_ref/0]). +-export_type([thrift_fun_ref/0]). + +%% API + +-spec serialize_function_args(thrift_fun_full_ref(), woody:args()) -> binary(). +serialize_function_args({Module, {Service, Function}}, Args) when is_tuple(Args) -> + ArgsType = Module:function_info(Service, Function, params_type), + serialize(ArgsType, Args). + +-spec serialize_function_reply(thrift_fun_full_ref(), term()) -> binary(). +serialize_function_reply({Module, {Service, Function}}, Data) -> + ArgsType = Module:function_info(Service, Function, reply_type), + serialize(ArgsType, Data). + +-spec serialize_function_exception(thrift_fun_full_ref(), thrift_exception()) -> binary(). +serialize_function_exception(FunctionRef, Exception) -> + ExceptionType = get_fun_exception_type(FunctionRef), + Name = find_exception_name(FunctionRef, Exception), + serialize(ExceptionType, {Name, Exception}). + +-spec serialize(thrift_type(), term()) -> binary(). +serialize(Type, Data) -> + Codec0 = thrift_strict_binary_codec:new(), + case thrift_strict_binary_codec:write(Codec0, Type, Data) of + {ok, Codec1} -> + thrift_strict_binary_codec:close(Codec1); + {error, Reason} -> + erlang:error({thrift, {protocol, Reason}}) + end. + +-spec deserialize(thrift_type(), binary()) -> term(). +deserialize(Type, Data) -> + Codec0 = thrift_strict_binary_codec:new(Data), + case thrift_strict_binary_codec:read(Codec0, Type) of + {ok, Result, Codec1} -> + case thrift_strict_binary_codec:close(Codec1) of + <<>> -> + Result; + Leftovers -> + erlang:error({thrift, {protocol, {excess_binary_data, Leftovers}}}) + end; + {error, Reason} -> + erlang:error({thrift, {protocol, Reason}}) + end. + +-spec deserialize_function_args(thrift_fun_full_ref(), binary()) -> woody:args(). +deserialize_function_args({Module, {Service, Function}}, Data) -> + ArgsType = Module:function_info(Service, Function, params_type), + deserialize(ArgsType, Data). + +-spec deserialize_function_reply(thrift_fun_full_ref(), binary()) -> term(). +deserialize_function_reply({Module, {Service, Function}}, Data) -> + ArgsType = Module:function_info(Service, Function, reply_type), + deserialize(ArgsType, Data). + +-spec deserialize_function_exception(thrift_fun_full_ref(), binary()) -> thrift_exception(). +deserialize_function_exception(FunctionRef, Data) -> + ExceptionType = get_fun_exception_type(FunctionRef), + {_Name, Exception} = deserialize(ExceptionType, Data), + Exception. + +%% + +-spec record_to_proplist(Record :: tuple(), RecordInfo :: [atom()]) -> [{atom(), _}]. +record_to_proplist(Record, RecordInfo) -> + element( + 1, + lists:foldl( + fun(RecordField, {L, N}) -> + case element(N, Record) of + V when V /= undefined -> + {[{RecordField, V} | L], N + 1}; + undefined -> + {L, N + 1} + end + end, + {[], 1 + 1}, + RecordInfo + ) + ). + +-spec get_fun_exception_type(thrift_fun_full_ref()) -> thrift_type(). +get_fun_exception_type({Module, {Service, Function}}) -> + DeclaredType = Module:function_info(Service, Function, exceptions), + % В сгенерированном коде исключения объявлены как структура. + % Для удобства работы, преобразуем тип в union. + {struct, struct, Exceptions} = DeclaredType, + {struct, union, Exceptions}. + +-spec find_exception_name(thrift_fun_full_ref(), thrift_exception()) -> Name :: atom(). +find_exception_name({Module, {Service, Function}}, Exception) -> + case thrift_processor_codec:match_exception({Module, Service}, Function, Exception) of + {ok, {_Type, Name}} -> + Name; + {error, bad_exception} -> + erlang:error({thrift, {unknown_exception, Exception}}) + end. diff --git a/compose.tracing.yaml b/compose.tracing.yaml index 8dd255b0..d2690829 100644 --- a/compose.tracing.yaml +++ b/compose.tracing.yaml @@ -13,9 +13,6 @@ services: limiter: environment: *otlp_enabled - party-management: - environment: *otlp_enabled - testrunner: environment: <<: *otlp_enabled diff --git a/compose.yaml b/compose.yaml index f8842668..2567cd3b 100644 --- a/compose.yaml +++ b/compose.yaml @@ -16,8 +16,6 @@ services: condition: service_healthy dmt: condition: service_healthy - party-management: - condition: service_healthy limiter: condition: service_healthy shumway: @@ -111,24 +109,6 @@ services: timeout: 1s retries: 20 - party-management: - image: ghcr.io/valitydev/party-management:sha-e3fe0dc - command: /opt/party-management/bin/party-management foreground - volumes: - - ./test/party-management/sys.config:/opt/party-management/releases/0.1/sys.config - depends_on: - db: - condition: service_healthy - dmt: - condition: service_started - shumway: - condition: service_started - healthcheck: - test: "/opt/party-management/bin/party-management ping" - interval: 10s - timeout: 5s - retries: 10 - cubasty: image: ghcr.io/valitydev/cubasty:sha-f94b6a0-epic-fix_matched volumes: @@ -147,7 +127,7 @@ services: image: postgres:17 command: -c 'max_connections=1000' environment: - POSTGRES_MULTIPLE_DATABASES: "hellgate,fistful,bender,dmt,party_management,shumway,liminator,customer_storage" + POSTGRES_MULTIPLE_DATABASES: "hellgate,fistful,bender,dmt,shumway,liminator,customer_storage" POSTGRES_PASSWORD: "postgres" volumes: - ./test/postgres/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d diff --git a/config/sys.config b/config/sys.config index 8647be8e..4d69a0a2 100644 --- a/config/sys.config +++ b/config/sys.config @@ -1,3 +1,4 @@ +%% TODO Consolidate `services` urls w/ transport opts into singular config entry [ {kernel, [ {logger_level, info}, @@ -217,6 +218,7 @@ {woody, #{ % disabled | safe | aggressive cache_mode => safe, + aggressive_caching_timeout => 30000, options => #{ woody_client => #{ event_handler => @@ -236,9 +238,20 @@ }} ]}, + {party_management, [ + {scoper_event_handler_options, #{ + event_handler_opts => #{ + formatter_opts => #{ + max_length => 1000 + } + } + }}, + {services, #{ + accounter => "http://shumway:8022/accounter" + }} + ]}, + {dmt_client, [ - % milliseconds - {cache_update_interval, 5000}, {max_cache_size, #{ elements => 20, % 50Mb diff --git a/elvis.config b/elvis.config index 666bef17..97780436 100644 --- a/elvis.config +++ b/elvis.config @@ -29,7 +29,9 @@ %% Project settings {elvis_style, invalid_dynamic_call, #{ ignore => [ - hg_proto_utils + hg_proto_utils, + pm_party, + pm_proto_utils ] }}, {elvis_style, god_modules, #{ @@ -38,7 +40,9 @@ hg_invoice_payment, hg_client_invoicing, ff_withdrawal, - ff_deposit + ff_deposit, + pm_client_party, + party_client_thrift ] }}, {elvis_style, state_record_and_type, #{ @@ -116,14 +120,22 @@ hg_invoice_tests_SUITE, hg_invoice_template_tests_SUITE, hg_direct_recurrent_tests_SUITE, - ff_withdrawal_adjustment_SUITE + ff_withdrawal_adjustment_SUITE, + pm_ct_fixture, + pm_party_tests_SUITE ] }}, {elvis_style, no_debug_call, #{}}, {elvis_style, no_throw, disable}, {elvis_style, no_import, disable}, {elvis_style, private_data_types, disable}, - {elvis_style, export_used_types, disable} + {elvis_style, export_used_types, disable}, + {elvis_style, no_catch_expressions, #{ + ignore => [ + pm_party_tests_SUITE, + party_client_base_pm_tests_SUITE + ] + }} ] }, #{ diff --git a/rebar.config b/rebar.config index e8f4c7ef..c0abc208 100644 --- a/rebar.config +++ b/rebar.config @@ -1,4 +1,4 @@ -% Common project erlang options. +%% Common project erlang options. {erl_opts, [ % mandatory debug_info, @@ -41,7 +41,6 @@ {payproc_errors, {git, "https://github.com/valitydev/payproc-errors-erlang.git", {branch, "master"}}}, {mg_proto, {git, "https://github.com/valitydev/machinegun-proto.git", {branch, "master"}}}, {dmt_client, {git, "https://github.com/valitydev/dmt-client.git", {tag, "v2.0.3"}}}, - {party_client, {git, "https://github.com/valitydev/party-client-erlang.git", {tag, "v2.0.1"}}}, {bender_client, {git, "https://github.com/valitydev/bender-client-erlang.git", {tag, "v1.1.0"}}}, {erl_health, {git, "https://github.com/valitydev/erlang-health.git", {branch, "master"}}}, {fault_detector_proto, {git, "https://github.com/valitydev/fault-detector-proto.git", {branch, "master"}}}, @@ -87,6 +86,7 @@ {profiles, [ {prod, [ {deps, [ + % for introspection on production {recon, "2.5.2"}, {logger_logstash_formatter, {git, "https://github.com/valitydev/logger_logstash_formatter.git", {ref, "08a66a6"}}} @@ -169,5 +169,6 @@ {shell, [ {config, "config/sys.config"}, + %% TODO Maybe list other core apps for shell start {apps, [hellgate, hg_client, hg_proto, routing, recon]} ]}. diff --git a/rebar.lock b/rebar.lock index 5f6f7417..67cf797c 100644 --- a/rebar.lock +++ b/rebar.lock @@ -111,10 +111,6 @@ {pkg,<<"opentelemetry_exporter">>,<<"1.8.0">>}, 0}, {<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.3.1">>},2}, - {<<"party_client">>, - {git,"https://github.com/valitydev/party-client-erlang.git", - {ref,"88cb5a9b5abd9bb437222de168bba096edd10882"}}, - 0}, {<<"payproc_errors">>, {git,"https://github.com/valitydev/payproc-errors-erlang.git", {ref,"8ae8586239ef68098398acf7eb8363d9ec3b3234"}}, diff --git a/test/party-management/sys.config b/test/party-management/sys.config deleted file mode 100644 index b3c514ab..00000000 --- a/test/party-management/sys.config +++ /dev/null @@ -1,132 +0,0 @@ -%% -*- mode: erlang -*- -[ - {kernel, [ - {logger_level, info}, - {logger, [ - {handler, default, logger_std_h, #{ - config => #{ - type => standard_io, - sync_mode_qlen => 20 - } - }} - ]} - ]}, - - {scoper, [ - {storage, scoper_storage_logger} - ]}, - - {party_management, [ - {scoper_event_handler_options, #{ - event_handler_opts => #{ - formatter_opts => #{ - max_length => 1000 - } - } - }}, - {services, #{ - accounter => "http://shumway:8022/accounter" - }}, - %% see `pm_party_cache:cache_options/0` - {cache_options, #{ - % 200Mb, cache memory quota in bytes - memory => 209715200, - ttl => 3600, - size => 3000 - }}, - {health_check, #{ - memory => {erl_health, cg_memory, [70]}, - dmt_client => {dmt_client, health_check, []} - }} - ]}, - - {epg_connector, [ - {databases, #{ - default_db => #{ - host => "db", - port => 5432, - database => "party_management", - username => "party_management", - password => "postgres" - } - }}, - {pools, #{ - default_pool => #{ - database => default_db, - size => 30 - } - }} - ]}, - - {progressor, [ - {call_wait_timeout, 20}, - {defaults, #{ - storage => #{ - client => prg_pg_backend, - options => #{ - pool => default_pool - } - }, - retry_policy => #{ - initial_timeout => 5, - backoff_coefficient => 1.0, - %% seconds - max_timeout => 180, - max_attempts => 3, - non_retryable_errors => [] - }, - task_scan_timeout => 1, - worker_pool_size => 100, - process_step_timeout => 30 - }}, - - {namespaces, #{ - 'party' => #{ - processor => #{ - client => machinery_prg_backend, - options => #{ - namespace => 'party', - handler => {pm_party_machine, #{}}, - schema => party_management_machinery_schema - } - } - } - }} - ]}, - - {dmt_client, [ - % milliseconds - {cache_update_interval, 5000}, - % milliseconds - {cache_server_call_timeout, 30000}, - {max_cache_size, #{ - elements => 20, - % 50Mb - memory => 52428800 - }}, - {woody_event_handlers, [ - {scoper_woody_event_handler, #{ - event_handler_opts => #{ - formatter_opts => #{ - max_length => 1000 - } - } - }} - ]}, - {service_urls, #{ - 'AuthorManagement' => <<"http://dmt:8022/v1/domain/author">>, - 'Repository' => <<"http://dmt:8022/v1/domain/repository">>, - 'RepositoryClient' => <<"http://dmt:8022/v1/domain/repository_client">> - }} - ]}, - - {snowflake, [{machine_id, 1}]}, - - {prometheus, [ - {collectors, [default]} - ]}, - - {hackney, [ - {mod_metrics, woody_hackney_prometheus} - ]} -].