diff --git a/apps/ff_cth/include/ct_domain.hrl b/apps/ff_cth/include/ct_domain.hrl index cfc7075..9a30dbd 100644 --- a/apps/ff_cth/include/ct_domain.hrl +++ b/apps/ff_cth/include/ct_domain.hrl @@ -18,6 +18,7 @@ -define(eas(ID), #domain_ExternalAccountSetRef{id = ID}). -define(insp(ID), #domain_InspectorRef{id = ID}). -define(payinst(ID), #domain_PaymentInstitutionRef{id = ID}). +-define(wthdr_prv(ID), #domain_WithdrawalProviderRef{id = ID}). -define(cash(Amount, SymCode), #domain_Cash{amount = Amount, currency = ?cur(SymCode)} diff --git a/apps/ff_cth/src/ct_domain.erl b/apps/ff_cth/src/ct_domain.erl index 57261e8..0309d7d 100644 --- a/apps/ff_cth/src/ct_domain.erl +++ b/apps/ff_cth/src/ct_domain.erl @@ -12,6 +12,7 @@ -export([inspector/4]). -export([proxy/2]). -export([proxy/3]). +-export([proxy/4]). -export([system_account_set/4]). -export([external_account_set/4]). -export([term_set_hierarchy/1]). @@ -19,6 +20,7 @@ -export([term_set_hierarchy/3]). -export([timed_term_set/1]). -export([globals/2]). +-export([withdrawal_provider/4]). %% @@ -30,6 +32,46 @@ -type object() :: dmsl_domain_thrift:'DomainObject'(). +-spec withdrawal_provider(?dtp('WithdrawalProviderRef'), ?dtp('ProxyRef'), binary(), ct_helper:config()) -> + object(). + +withdrawal_provider(Ref, ProxyRef, IdentityID, C) -> + AccountID = account(<<"RUB">>, C), + {withdrawal_provider, #domain_WithdrawalProviderObject{ + ref = Ref, + data = #domain_WithdrawalProvider{ + name = <<"WithdrawalProvider">>, + proxy = #domain_Proxy{ref = ProxyRef, additional = #{}}, + identity = IdentityID, + withdrawal_terms = #domain_WithdrawalProvisionTerms{ + currencies = {value, ?ordset([])}, + payout_methods = {value, ?ordset([])}, + cash_limit = {value, ?cashrng( + {inclusive, ?cash( 0, <<"RUB">>)}, + {exclusive, ?cash(10000000, <<"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) + ])}} + ) + ]} + } + ]} + }, + accounts = #{ + ?cur(<<"RUB">>) => #domain_ProviderAccount{settlement = AccountID} + } + } + }}. + -spec currency(?dtp('CurrencyRef')) -> object(). @@ -120,22 +162,29 @@ inspector(Ref, Name, ProxyRef, Additional) -> } }}. --spec proxy(?dtp('ProxyRef'), binary()) -> +-spec proxy(?dtp('ProxyRef'), Name :: binary()) -> object(). proxy(Ref, Name) -> - proxy(Ref, Name, #{}). + proxy(Ref, Name, <<>>). --spec proxy(?dtp('ProxyRef'), binary(), ?dtp('ProxyOptions')) -> +-spec proxy(?dtp('ProxyRef'), Name :: binary(), URL :: binary()) -> object(). -proxy(Ref, Name, Opts) -> +proxy(Ref, Name, URL) -> + proxy(Ref, Name, URL, #{}). + + +-spec proxy(?dtp('ProxyRef'), Name :: binary(), URL :: binary(), ?dtp('ProxyOptions')) -> + object(). + +proxy(Ref, Name, URL, Opts) -> {proxy, #domain_ProxyObject{ ref = Ref, data = #domain_ProxyDefinition{ name = Name, description = <<>>, - url = <<>>, + url = URL, options = Opts } }}. diff --git a/apps/ff_cth/src/ct_payment_system.erl b/apps/ff_cth/src/ct_payment_system.erl index de23a1f..c24065e 100644 --- a/apps/ff_cth/src/ct_payment_system.erl +++ b/apps/ff_cth/src/ct_payment_system.erl @@ -14,7 +14,9 @@ services => map(), domain_config => list(), default_termset => dmsl_domain_thrift:'TermSet'(), - company_termset => dmsl_domain_thrift:'TermSet'() + company_termset => dmsl_domain_thrift:'TermSet'(), + payment_inst_identity_id => id(), + provider_identity_id => id() }. -opaque system() :: #{ started_apps := [atom()], @@ -48,7 +50,11 @@ shutdown(C) -> %% Internals -spec do_setup(options(), config()) -> config(). -do_setup(Options, C0) -> +do_setup(Options0, C0) -> + Options = Options0#{ + payment_inst_identity_id => genlib:unique(), + provider_identity_id => genlib:unique() + }, {ok, Processing0} = start_processing_apps(Options), C1 = ct_helper:makeup_cfg([ct_helper:woody_ctx()], [{services, services(Options)} | C0]), ok = ff_woody_ctx:set(ct_helper:get_woody_ctx(C1)), @@ -128,7 +134,7 @@ start_processing_apps(Options) -> setup_dominant(Options, C) -> ok = ct_domain_config:upsert(domain_config(Options, C)). -configure_processing_apps(_Options) -> +configure_processing_apps(Options) -> ok = set_app_env( [ff_transfer, withdrawal, system, accounts, settlement, <<"RUB">>], create_company_account() @@ -140,7 +146,8 @@ configure_processing_apps(_Options) -> ok = set_app_env( [ff_transfer, withdrawal, provider, <<"mocketbank">>, accounts, <<"RUB">>], create_company_account() - ). + ), + ok = create_crunch_identity(Options). construct_handler(Module, Suffix, BeConf) -> {{fistful, Module}, @@ -196,6 +203,14 @@ get_eventsink_routes(BeConf) -> DepositRoute ]). +create_crunch_identity(Options) -> + PartyID = create_party(), + PaymentInstIdentityID = payment_inst_identity_id(Options), + PaymentInstIdentityID = create_identity(PaymentInstIdentityID, PartyID, <<"good-one">>, <<"church">>), + ProviderIdentityID = provider_identity_id(Options), + ProviderIdentityID = create_identity(ProviderIdentityID, PartyID, <<"good-one">>, <<"church">>), + ok. + create_company_account() -> PartyID = create_party(), IdentityID = create_company_identity(PartyID), @@ -205,19 +220,22 @@ create_company_account() -> {ok, [{created, Account}]} = ff_account:create(PartyID, Identity, Currency), Account. -create_company_identity(Party) -> - create_identity(Party, <<"good-one">>, <<"church">>). +create_company_identity(PartyID) -> + create_identity(PartyID, <<"good-one">>, <<"church">>). create_party() -> ID = genlib:bsuuid(), _ = ff_party:create(ID), ID. -create_identity(Party, ProviderID, ClassID) -> +create_identity(PartyID, ProviderID, ClassID) -> ID = genlib:unique(), + create_identity(ID, PartyID, ProviderID, ClassID). + +create_identity(ID, PartyID, ProviderID, ClassID) -> ok = ff_identity_machine:create( ID, - #{party => Party, provider => ProviderID, class => ClassID}, + #{party => PartyID, provider => ProviderID, class => ClassID}, ff_ctx:new() ), ID. @@ -374,6 +392,12 @@ services(Options) -> -include_lib("ff_cth/include/ct_domain.hrl"). +payment_inst_identity_id(Options) -> + maps:get(payment_inst_identity_id, Options). + +provider_identity_id(Options) -> + maps:get(provider_identity_id, Options). + domain_config(Options, C) -> Default = [ @@ -389,7 +413,10 @@ domain_config(Options, C) -> providers = {value, ?ordset([])}, inspector = {value, ?insp(1)}, residences = ['rus'], - realm = live + realm = live, + wallet_system_account_set = {value, ?sas(1)}, + identity = payment_inst_identity_id(Options), + withdrawal_providers = {value, ?ordset([?wthdr_prv(1)])} } }}, @@ -397,6 +424,9 @@ domain_config(Options, C) -> ct_domain:inspector(?insp(1), <<"Low Life">>, ?prx(1), #{<<"risk_score">> => <<"low">>}), ct_domain:proxy(?prx(1), <<"Inspector proxy">>), + ct_domain:proxy(?prx(2), <<"Mocket proxy">>, <<"http://adapter-mocketbank:8022/proxy/mocketbank/p2p-credit">>), + + ct_domain:withdrawal_provider(?wthdr_prv(1), ?prx(2), provider_identity_id(Options), C), ct_domain:contract_template(?tmpl(1), ?trms(1)), ct_domain:term_set_hierarchy(?trms(1), [ct_domain:timed_term_set(default_termset(Options))]), diff --git a/apps/ff_server/src/ff_withdrawal_eventsink_publisher.erl b/apps/ff_server/src/ff_withdrawal_eventsink_publisher.erl index adae47f..33dbfe1 100644 --- a/apps/ff_server/src/ff_withdrawal_eventsink_publisher.erl +++ b/apps/ff_server/src/ff_withdrawal_eventsink_publisher.erl @@ -173,7 +173,7 @@ marshal(withdrawal_route_changed, #{ provider_id := ProviderID }) -> #wthd_RouteChange{ - id = marshal(id, ProviderID) + id = marshal(id, genlib:to_binary(ProviderID)) }; marshal(T, V) -> diff --git a/apps/ff_server/src/ff_withdrawal_session_eventsink_publisher.erl b/apps/ff_server/src/ff_withdrawal_session_eventsink_publisher.erl index 2c2f251..d253530 100644 --- a/apps/ff_server/src/ff_withdrawal_session_eventsink_publisher.erl +++ b/apps/ff_server/src/ff_withdrawal_session_eventsink_publisher.erl @@ -68,7 +68,7 @@ marshal(session, #{ id = marshal(id, SessionID), status = marshal(session_status, SessionStatus), withdrawal = marshal(withdrawal, Withdrawal), - provider = marshal(id, ProviderID) + provider = marshal(id, genlib:to_binary(ProviderID)) }; marshal(session_status, active) -> diff --git a/apps/ff_transfer/src/ff_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl index 39368d0..7fd2439 100644 --- a/apps/ff_transfer/src/ff_withdrawal.erl +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -19,7 +19,8 @@ -type events() :: ff_transfer_machine:events(ff_transfer:event(transfer_params(), route())). -type event() :: ff_transfer_machine:event(ff_transfer:event(transfer_params(), route())). -type route() :: ff_transfer:route(#{ - provider_id := id() + % TODO I'm now sure about this change, it may crash old events. Or not. )) + provider_id := pos_integer() | id() }). -export_type([withdrawal/0]). @@ -228,19 +229,81 @@ do_process_transfer(idle, Withdrawal) -> {error, _Reason}. create_route(Withdrawal) -> #{ + wallet_id := WalletID, destination_id := DestinationID } = params(Withdrawal), + Body = body(Withdrawal), do(fun () -> + Wallet = ff_wallet_machine:wallet(unwrap(wallet, ff_wallet_machine:get(WalletID))), + PaymentInstitutionID = unwrap(ff_party:get_wallet_payment_institution_id(Wallet)), + PaymentInstitution = unwrap(ff_payment_institution:get(PaymentInstitutionID)), DestinationMachine = unwrap(destination, ff_destination:get_machine(DestinationID)), Destination = ff_destination:get(DestinationMachine), - ProviderID = unwrap(route, ff_withdrawal_provider:choose(Destination, body(Withdrawal))), + VS = unwrap(collect_varset(Body, Wallet, Destination)), + ProviderID = unwrap(ff_payment_institution:compute_withdrawal_provider(PaymentInstitution, VS)), {continue, [{route_changed, #{provider_id => ProviderID}}]} end). + + -spec create_p_transfer(withdrawal()) -> {ok, process_result()} | {error, _Reason}. create_p_transfer(Withdrawal) -> + #{provider_id := ProviderID} = route(Withdrawal), + case is_integer(ProviderID) of + true -> + create_p_transfer_new_style(Withdrawal); + false when is_binary(ProviderID) -> + create_p_transfer_old_style(Withdrawal) + end. + +create_p_transfer_new_style(Withdrawal) -> + #{ + wallet_id := WalletID, + wallet_account := WalletAccount, + destination_id := DestinationID, + destination_account := DestinationAccount, + wallet_cash_flow_plan := WalletCashFlowPlan + } = params(Withdrawal), + {_Amount, CurrencyID} = body(Withdrawal), + #{provider_id := ProviderID} = route(Withdrawal), + do(fun () -> + Provider = unwrap(provider, ff_payouts_provider:get(ProviderID)), + ProviderAccounts = ff_payouts_provider:accounts(Provider), + ProviderAccount = maps:get(CurrencyID, ProviderAccounts, undefined), + + Wallet = ff_wallet_machine:wallet(unwrap(wallet, ff_wallet_machine:get(WalletID))), + PaymentInstitutionID = unwrap(ff_party:get_wallet_payment_institution_id(Wallet)), + PaymentInstitution = unwrap(ff_payment_institution:get(PaymentInstitutionID)), + DestinationMachine = unwrap(destination, ff_destination:get_machine(DestinationID)), + Destination = ff_destination:get(DestinationMachine), + VS = unwrap(collect_varset(body(Withdrawal), Wallet, Destination)), + SystemAccounts = unwrap(ff_payment_institution:compute_system_accounts(PaymentInstitution, VS)), + + SystemAccount = maps:get(CurrencyID, SystemAccounts, #{}), + SettlementAccount = maps:get(settlement, SystemAccount, undefined), + SubagentAccount = maps:get(subagent, SystemAccount, undefined), + + ProviderFee = ff_payouts_provider:compute_fees(Provider, VS), + + CashFlowPlan = unwrap(provider_fee, ff_cash_flow:add_fee(WalletCashFlowPlan, ProviderFee)), + FinalCashFlow = unwrap(cash_flow, finalize_cash_flow( + CashFlowPlan, + WalletAccount, + DestinationAccount, + SettlementAccount, + SubagentAccount, + ProviderAccount, + body(Withdrawal) + )), + PTransferID = construct_p_transfer_id(id(Withdrawal)), + PostingsTransferEvents = unwrap(p_transfer, ff_postings_transfer:create(PTransferID, FinalCashFlow)), + {continue, [{p_transfer, Ev} || Ev <- PostingsTransferEvents]} + end). + +% TODO backward compatibility, remove after successful update +create_p_transfer_old_style(Withdrawal) -> #{ wallet_account := WalletAccount, destination_account := DestinationAccount, @@ -360,3 +423,32 @@ finalize_cash_flow(CashFlowPlan, WalletAccount, DestinationAccount, ff_transfer:event(). maybe_migrate(Ev) -> ff_transfer:maybe_migrate(Ev, withdrawal). + +collect_varset({_, CurrencyID} = Body, Wallet, Destination) -> + Currency = #domain_CurrencyRef{symbolic_code = CurrencyID}, + IdentityID = ff_wallet:identity(Wallet), + do(fun() -> + IdentityMachine = unwrap(ff_identity_machine:get(IdentityID)), + Identity = ff_identity_machine:identity(IdentityMachine), + PartyID = ff_identity:party(Identity), + PaymentTool = construct_payment_tool(ff_destination:resource(Destination)), + #{ + currency => Currency, + cost => ff_cash:encode(Body), + % TODO it's not fair, because it's PAYOUT not PAYMENT tool. + payment_tool => PaymentTool, + party_id => PartyID, + wallet_id => ff_wallet:id(Wallet), + payout_method => #domain_PayoutMethodRef{id = wallet_info} + } + end). + +-spec construct_payment_tool(ff_destination:resource()) -> + dmsl_domain_thrift:'PaymentTool'(). +construct_payment_tool({bank_card, ResourceBankCard}) -> + {bank_card, #domain_BankCard{ + token = maps:get(token, ResourceBankCard), + payment_system = maps:get(payment_system, ResourceBankCard), + bin = maps:get(bin, ResourceBankCard), + masked_pan = maps:get(masked_pan, ResourceBankCard) + }}. diff --git a/apps/ff_transfer/src/ff_withdrawal_provider.erl b/apps/ff_transfer/src/ff_withdrawal_provider.erl index 81fc7d2..9de1759 100644 --- a/apps/ff_transfer/src/ff_withdrawal_provider.erl +++ b/apps/ff_transfer/src/ff_withdrawal_provider.erl @@ -3,7 +3,7 @@ %%% %%% TODOs %%% -%%% - Anything remotely similar to routing! +%%% - Replace with ff_payouts_provider after update! %%% -module(ff_withdrawal_provider). diff --git a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl index bf8448f..1ffaf54 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl @@ -198,8 +198,14 @@ create_session(ID, Data, #{destination := DestinationID, provider_id := Provider status => active }. --spec get_adapter_with_opts(ff_withdrawal_provider:id()) -> adapter_with_opts(). -get_adapter_with_opts(ProviderID) -> +-spec get_adapter_with_opts(ff_payouts_provider:id() | ff_withdrawal_provider:id()) -> adapter_with_opts(). +get_adapter_with_opts(ProviderID) when is_integer(ProviderID) -> + %% new_style + Provider = unwrap(ff_payouts_provider:get(ProviderID)), + {ff_payouts_provider:adapter(Provider), ff_payouts_provider:adapter_opts(Provider)}; +get_adapter_with_opts(ProviderID) when is_binary(ProviderID) -> + %% old style + %% TODO remove after update {ok, Provider} = ff_withdrawal_provider:get(ProviderID), {ff_withdrawal_provider:adapter(Provider), ff_withdrawal_provider:adapter_opts(Provider)}. diff --git a/apps/fistful/src/ff_cash.erl b/apps/fistful/src/ff_cash.erl new file mode 100644 index 0000000..061f8b3 --- /dev/null +++ b/apps/fistful/src/ff_cash.erl @@ -0,0 +1,17 @@ +-module(ff_cash). + +-include_lib("dmsl/include/dmsl_domain_thrift.hrl"). + +-export([decode/1]). +-export([encode/1]). + +-spec decode(dmsl_domain_thrift:'Cash'()) -> ff_transaction:body(). +decode(#domain_Cash{amount = Amount, currency = Currency}) -> + {Amount, Currency#domain_CurrencyRef.symbolic_code}. + +-spec encode(ff_transaction:body()) -> dmsl_domain_thrift:'Cash'(). +encode({Amount, CurrencyID}) -> + #domain_Cash{ + amount = Amount, + currency = #domain_CurrencyRef{symbolic_code = CurrencyID} + }. diff --git a/apps/fistful/src/ff_cash_flow.erl b/apps/fistful/src/ff_cash_flow.erl index ec82b5c..ad274bd 100644 --- a/apps/fistful/src/ff_cash_flow.erl +++ b/apps/fistful/src/ff_cash_flow.erl @@ -5,6 +5,7 @@ -export([gather_used_accounts/1]). -export([finalize/3]). -export([add_fee/2]). +-export([decode_domain_postings/1]). %% Domain types -type plan_posting() :: #{ @@ -66,6 +67,7 @@ type => plan_account() }. +-export_type([plan_posting/0]). -export_type([plan_volume/0]). -export_type([plan_constant/0]). -export_type([plan_operation/0]). @@ -117,6 +119,61 @@ finalize(Plan, Accounts, Constants) -> add_fee(#{postings := PlanPostings} = Plan, #{postings := FeePostings}) -> {ok, Plan#{postings => PlanPostings ++ FeePostings}}. +%% Domain cash flow unmarshalling + +-spec decode_domain_postings(dmsl_domain_thrift:'CashFlow'()) -> + [plan_posting()]. +decode_domain_postings(DomainPostings) -> + [decode_domain_posting(P) || P <- DomainPostings]. + +-spec decode_domain_posting(dmsl_domain_thrift:'CashFlowPosting'()) -> + plan_posting(). +decode_domain_posting( + #domain_CashFlowPosting{ + source = Source, + destination = Destination, + volume = Volume, + details = Details + } +) -> + #{ + sender => decode_domain_plan_account(Source), + receiver => decode_domain_plan_account(Destination), + volume => decode_domain_plan_volume(Volume), + details => Details + }. + +-spec decode_domain_plan_account(dmsl_domain_thrift:'CashFlowAccount'()) -> + ff_cash_flow:plan_account(). +decode_domain_plan_account({_AccountNS, _AccountType} = Account) -> + Account. + +-spec decode_domain_plan_volume(dmsl_domain_thrift:'CashVolume'()) -> + ff_cash_flow:plan_volume(). +decode_domain_plan_volume({fixed, #domain_CashVolumeFixed{cash = Cash}}) -> + {fixed, ff_cash:decode(Cash)}; +decode_domain_plan_volume({share, Share}) -> + #domain_CashVolumeShare{ + parts = Parts, + 'of' = Of, + rounding_method = RoundingMethod + } = Share, + {share, {decode_rational(Parts), Of, decode_rounding_method(RoundingMethod)}}; +decode_domain_plan_volume({product, {Fun, CVs}}) -> + {product, {Fun, lists:map(fun decode_domain_plan_volume/1, CVs)}}. + +-spec decode_rounding_method(dmsl_domain_thrift:'RoundingMethod'() | undefined) -> + ff_cash_flow:rounding_method(). +decode_rounding_method(undefined) -> + default; +decode_rounding_method(RoundingMethod) -> + RoundingMethod. + +-spec decode_rational(dmsl_base_thrift:'Rational'()) -> + genlib_rational:t(). +decode_rational(#'Rational'{p = P, q = Q}) -> + genlib_rational:new(P, Q). + %% Internals %% Finalizing diff --git a/apps/fistful/src/ff_party.erl b/apps/fistful/src/ff_party.erl index 0aa3e31..b067442 100644 --- a/apps/fistful/src/ff_party.erl +++ b/apps/fistful/src/ff_party.erl @@ -48,6 +48,7 @@ -export([get_contract_terms/3]). -export([get_contract_terms/4]). -export([get_withdrawal_cash_flow_plan/1]). +-export([get_wallet_payment_institution_id/1]). %% Internal types -type body() :: ff_transfer:body(). @@ -61,6 +62,7 @@ -type cash_range() :: dmsl_domain_thrift:'CashRange'(). -type timestamp() :: ff_time:timestamp_ms(). -type wallet() :: ff_wallet:wallet(). +-type payment_institution_id() :: ff_payment_institution:id(). -type currency_validation_error() :: {terms_violation, {not_allowed_currency, _Details}}. -type withdrawal_currency_error() :: {invalid_withdrawal_currency, currency_id(), {wallet_currency, currency_id()}}. @@ -102,7 +104,7 @@ is_accessible(ID) -> %% -type contract_prototype() :: #{ - payinst := _PaymentInstitutionRef, + payinst := dmsl_domain_thrift:'PaymentInstitutionRef'(), contract_template := dmsl_domain_thrift:'ContractTemplateRef'(), contractor_level := dmsl_domain_thrift:'ContractorIdentificationLevel'() }. @@ -136,6 +138,25 @@ change_contractor_level(ID, ContractID, ContractorLevel) -> ok end). +-spec get_wallet_payment_institution_id(wallet()) -> Result when + Result :: {ok, payment_institution_id()} | {error, Error}, + Error :: + {party_not_found, id()} | + {contract_not_found, id()} | + {exception, any()}. + +get_wallet_payment_institution_id(Wallet) -> + IdentityID = ff_wallet:identity(Wallet), + do(fun() -> + IdentityMachine = unwrap(ff_identity_machine:get(IdentityID)), + Identity = ff_identity_machine:identity(IdentityMachine), + PartyID = ff_identity:party(Identity), + ContractID = ff_identity:contract(Identity), + Contract = unwrap(do_get_contract(PartyID, ContractID)), + #domain_PaymentInstitutionRef{id = ID} = Contract#domain_Contract.payment_institution, + ID + end). + -spec get_contract_terms(wallet(), body(), timestamp()) -> Result when Result :: {ok, terms()} | {error, Error}, Error :: @@ -149,8 +170,11 @@ get_contract_terms(Wallet, Body, Timestamp) -> do(fun() -> IdentityMachine = unwrap(ff_identity_machine:get(IdentityID)), Identity = ff_identity_machine:identity(IdentityMachine), - ContractID = ff_identity:contract(Identity), PartyID = ff_identity:party(Identity), + ContractID = ff_identity:contract(Identity), + % TODO this is not level itself! Dont know how to get it here. + % Currently we use Contract's level in PartyManagement, but I'm not sure about correctness of this. + % Level = ff_identity:level(Identity), {_Amount, CurrencyID} = Body, TermVarset = #{ amount => Body, @@ -160,20 +184,20 @@ get_contract_terms(Wallet, Body, Timestamp) -> unwrap(get_contract_terms(PartyID, ContractID, TermVarset, Timestamp)) end). --spec get_contract_terms(id(), contract_id(), term_varset(), timestamp()) -> Result when +-spec get_contract_terms(PartyID :: id(), contract_id(), term_varset(), timestamp()) -> Result when Result :: {ok, terms()} | {error, Error}, Error :: {party_not_found, id()} | {party_not_exists_yet, id()} | {exception, any()}. -get_contract_terms(ID, ContractID, Varset, Timestamp) -> +get_contract_terms(PartyID, ContractID, Varset, Timestamp) -> DomainVarset = encode_varset(Varset), - Args = [ID, ContractID, ff_time:to_rfc3339(Timestamp), DomainVarset], + Args = [PartyID, ContractID, ff_time:to_rfc3339(Timestamp), DomainVarset], case call('ComputeWalletTermsNew', Args) of {ok, Terms} -> {ok, Terms}; {exception, #payproc_PartyNotFound{}} -> - {error, {party_not_found, ID}}; + {error, {party_not_found, PartyID}}; {exception, #payproc_PartyNotExistsYet{}} -> - {error, {party_not_exists_yet, ID}}; + {error, {party_not_exists_yet, PartyID}}; {exception, Unexpected} -> {error, {exception, Unexpected}} end. @@ -236,7 +260,7 @@ get_withdrawal_cash_flow_plan(Terms) -> } } } = Terms, - Postings = decode_domain_postings(DomainPostings), + Postings = ff_cash_flow:decode_domain_postings(DomainPostings), {ok, #{postings => Postings}}. %% Internal functions @@ -269,15 +293,17 @@ do_get_party(ID) -> error(Unexpected) end. -% do_get_contract(ID, ContractID) -> -% case call('GetContract', [ID, ContractID]) of -% {ok, #domain_Contract{} = Contract} -> -% Contract; -% {exception, #payproc_ContractNotFound{}} -> -% {error, notfound}; -% {exception, Unexpected} -> -% error(Unexpected) -% end. +do_get_contract(ID, ContractID) -> + case call('GetContract', [ID, ContractID]) of + {ok, #domain_Contract{} = Contract} -> + {ok, Contract}; + {exception, #payproc_PartyNotFound{}} -> + {error, {party_not_found, ID}}; + {exception, #payproc_ContractNotFound{}} -> + {error, {contract_not_found, ContractID}}; + {exception, Unexpected} -> + error(Unexpected) + end. do_create_claim(ID, Changeset) -> case call('CreateClaim', [ID, Changeset]) of @@ -573,73 +599,6 @@ compare_cash( ) -> Fun(A, Am). -%% Domain cash flow unmarshalling - --spec decode_domain_postings(ff_cash_flow:domain_plan_postings()) -> - [ff_cash_flow:plan_posting()]. -decode_domain_postings(DomainPostings) -> - [decode_domain_posting(P) || P <- DomainPostings]. - --spec decode_domain_posting(dmsl_domain_thrift:'CashFlowPosting'()) -> - ff_cash_flow:plan_posting(). -decode_domain_posting( - #domain_CashFlowPosting{ - source = Source, - destination = Destination, - volume = Volume, - details = Details - } -) -> - #{ - sender => decode_domain_plan_account(Source), - receiver => decode_domain_plan_account(Destination), - volume => decode_domain_plan_volume(Volume), - details => Details - }. - --spec decode_domain_plan_account(dmsl_domain_thrift:'CashFlowAccount'()) -> - ff_cash_flow:plan_account(). -decode_domain_plan_account({_AccountNS, _AccountType} = Account) -> - Account. - --spec decode_domain_plan_volume(dmsl_domain_thrift:'CashVolume'()) -> - ff_cash_flow:plan_volume(). -decode_domain_plan_volume({fixed, #domain_CashVolumeFixed{cash = Cash}}) -> - {fixed, decode_domain_cash(Cash)}; -decode_domain_plan_volume({share, Share}) -> - #domain_CashVolumeShare{ - parts = Parts, - 'of' = Of, - rounding_method = RoundingMethod - } = Share, - {share, {decode_rational(Parts), Of, decode_rounding_method(RoundingMethod)}}; -decode_domain_plan_volume({product, {Fun, CVs}}) -> - {product, {Fun, lists:map(fun decode_domain_plan_volume/1, CVs)}}. - --spec decode_rounding_method(dmsl_domain_thrift:'RoundingMethod'() | undefined) -> - ff_cash_flow:rounding_method(). -decode_rounding_method(undefined) -> - default; -decode_rounding_method(RoundingMethod) -> - RoundingMethod. - --spec decode_rational(dmsl_base_thrift:'Rational'()) -> - genlib_rational:t(). -decode_rational(#'Rational'{p = P, q = Q}) -> - genlib_rational:new(P, Q). - --spec decode_domain_cash(domain_cash()) -> - ff_cash_flow:cash(). -decode_domain_cash( - #domain_Cash{ - amount = Amount, - currency = #domain_CurrencyRef{ - symbolic_code = SymbolicCode - } - } -) -> - {Amount, SymbolicCode}. - %% Varset stuff -spec encode_varset(term_varset()) -> diff --git a/apps/fistful/src/ff_payment_institution.erl b/apps/fistful/src/ff_payment_institution.erl new file mode 100644 index 0000000..84354b5 --- /dev/null +++ b/apps/fistful/src/ff_payment_institution.erl @@ -0,0 +1,133 @@ +-module(ff_payment_institution). + +-include_lib("dmsl/include/dmsl_domain_thrift.hrl"). + +-type id() :: dmsl_domain_thrift:'ObjectID'(). +-type payment_institution() :: #{ + id := id(), + system_accounts := dmsl_domain_thrift:'SystemAccountSetSelector'(), + identity := binary(), + providers := dmsl_domain_thrift:'WithdrawalProviderSelector'() +}. + +-type payinst_ref() :: dmsl_domain_thrift:'PaymentInstitutionRef'(). + +-type system_accounts() :: #{ + ff_currency:id() => system_account() +}. + +-type system_account() :: #{ + settlement => ff_account:account(), + subagent => ff_account:account() +}. + +-export_type([id/0]). +-export_type([payment_institution/0]). + +-export([id/1]). + +-export([ref/1]). +-export([get/1]). +-export([compute_withdrawal_provider/2]). +-export([compute_system_accounts/2]). + +%% Pipeline + +-import(ff_pipeline, [do/1, unwrap/1]). + +%% + +-spec id(payment_institution()) -> id(). + +id(#{id := ID}) -> + ID. + +%% + +-spec ref(id()) -> payinst_ref(). + +ref(ID) -> + #domain_PaymentInstitutionRef{id = ID}. + +-spec get(id()) -> + {ok, payment_institution()} | + {error, notfound}. + +get(ID) -> + do(fun () -> + PaymentInstitution = unwrap(ff_domain_config:object({payment_institution, ref(ID)})), + decode(ID, PaymentInstitution) + end). + +-spec compute_withdrawal_provider(payment_institution(), hg_selector:varset()) -> + {ok, ff_payouts_provider:id()} | {error, term()}. + +compute_withdrawal_provider(#{providers := ProviderSelector}, VS) -> + do(fun() -> + Providers = unwrap(hg_selector:reduce_to_value(ProviderSelector, VS)), + %% TODO choose wizely one of them + [#domain_WithdrawalProviderRef{id = ProviderID} | _] = Providers, + ProviderID + end). + +-spec compute_system_accounts(payment_institution(), hg_selector:varset()) -> + {ok, system_accounts()} | {error, term()}. + +compute_system_accounts(PaymentInstitution, VS) -> + #{ + identity := Identity, + system_accounts := SystemAccountsSelector + } = PaymentInstitution, + do(fun() -> + SystemAccountSetRef = unwrap(hg_selector:reduce_to_value(SystemAccountsSelector, VS)), + SystemAccountSet = unwrap(ff_domain_config:object({system_account_set, SystemAccountSetRef})), + decode_system_account_set(Identity, SystemAccountSet) + end). +%% + +decode(ID, #domain_PaymentInstitution{ + wallet_system_account_set = SystemAccounts, + identity = Identity, + withdrawal_providers = Providers +}) -> + #{ + id => ID, + system_accounts => SystemAccounts, + identity => Identity, + providers => Providers + }. + +decode_system_account_set(Identity, #domain_SystemAccountSet{accounts = Accounts}) -> + maps:fold( + fun(CurrencyRef, SystemAccount, Acc) -> + #domain_CurrencyRef{symbolic_code = CurrencyID} = CurrencyRef, + maps:put( + CurrencyID, + decode_system_account(SystemAccount, CurrencyID, Identity), + Acc + ) + end, + #{}, + Accounts + ). + +decode_system_account(SystemAccount, CurrencyID, Identity) -> + #domain_SystemAccount{ + settlement = SettlementAccountID, + subagent = SubagentAccountID + } = SystemAccount, + #{ + settlement => decode_account(SettlementAccountID, CurrencyID, Identity), + subagent => decode_account(SubagentAccountID, CurrencyID, Identity) + }. + +decode_account(AccountID, CurrencyID, Identity) when AccountID =/= undefined -> + #{ + % FIXME + id => Identity, + identity => Identity, + currency => CurrencyID, + accounter_account_id => AccountID + }; +decode_account(undefined, _, _) -> + undefined. diff --git a/apps/fistful/src/ff_payouts_provider.erl b/apps/fistful/src/ff_payouts_provider.erl new file mode 100644 index 0000000..74268d0 --- /dev/null +++ b/apps/fistful/src/ff_payouts_provider.erl @@ -0,0 +1,129 @@ +-module(ff_payouts_provider). + +-include_lib("dmsl/include/dmsl_domain_thrift.hrl"). + +-type withdrawal_provider() :: #{ + id := id(), + identity := ff_identity:id(), + withdrawal_terms := dmsl_domain_thrift:'WithdrawalProvisionTerms'(), + accounts := accounts(), + adapter := ff_adapter:adapter(), + adapter_opts := map() +}. + +-type id() :: dmsl_domain_thrift:'ObjectID'(). +-type accounts() :: #{ff_currency:id() => ff_account:account()}. + +-type withdrawal_provider_ref() :: dmsl_domain_thrift:'WithdrawalProviderRef'(). + +-export_type([id/0]). +-export_type([withdrawal_provider/0]). + +-export([id/1]). +-export([accounts/1]). +-export([adapter/1]). +-export([adapter_opts/1]). + +-export([ref/1]). +-export([get/1]). +-export([compute_fees/2]). + +%% Pipeline + +-import(ff_pipeline, [do/1, unwrap/1]). + +%% + +-spec id(withdrawal_provider()) -> id(). +-spec accounts(withdrawal_provider()) -> accounts(). +-spec adapter(withdrawal_provider()) -> ff_adapter:adapter(). +-spec adapter_opts(withdrawal_provider()) -> map(). + +id(#{id := ID}) -> + ID. + +accounts(#{accounts := Accounts}) -> + Accounts. + +adapter(#{adapter := Adapter}) -> + Adapter. + +adapter_opts(#{adapter_opts := AdapterOpts}) -> + AdapterOpts. + +%% + +-spec ref(id()) -> withdrawal_provider_ref(). + +ref(ID) -> + #domain_WithdrawalProviderRef{id = ID}. + +-spec get(id()) -> + {ok, withdrawal_provider()} | + {error, notfound}. + +get(ID) -> + do(fun () -> + WithdrawalProvider = unwrap(ff_domain_config:object({withdrawal_provider, ref(ID)})), + decode(ID, WithdrawalProvider) + end). + +-spec compute_fees(withdrawal_provider(), hg_selector:varset()) -> ff_cash_flow:cash_flow_fee(). + +compute_fees(#{withdrawal_terms := WithdrawalTerms}, VS) -> + #domain_WithdrawalProvisionTerms{cash_flow = CashFlowSelector} = WithdrawalTerms, + CashFlow = unwrap(hg_selector:reduce_to_value(CashFlowSelector, VS)), + #{ + postings => ff_cash_flow:decode_domain_postings(CashFlow) + }. + +%% + +decode(ID, #domain_WithdrawalProvider{ + proxy = Proxy, + identity = Identity, + withdrawal_terms = WithdrawalTerms, + accounts = Accounts +}) -> + maps:merge( + #{ + id => ID, + identity => Identity, + withdrawal_terms => WithdrawalTerms, + accounts => decode_accounts(Identity, Accounts) + }, + decode_adapter(Proxy) + ). + +decode_accounts(Identity, Accounts) -> + maps:fold( + fun(CurrencyRef, ProviderAccount, Acc) -> + #domain_CurrencyRef{symbolic_code = CurrencyID} = CurrencyRef, + #domain_ProviderAccount{settlement = AccountID} = ProviderAccount, + maps:put( + CurrencyID, + #{ + % FIXME + id => Identity, + identity => Identity, + currency => CurrencyID, + accounter_account_id => AccountID + }, + Acc + ) + end, + #{}, + Accounts + ). + +decode_adapter(#domain_Proxy{ref = ProxyRef, additional = ProviderOpts}) -> + Proxy = unwrap(ff_domain_config:object({proxy, ProxyRef})), + #domain_ProxyDefinition{ + url = URL, + options = ProxyOpts + } = Proxy, + #{ + adapter => ff_woody_client:new(URL), + adapter_opts => maps:merge(ProviderOpts, ProxyOpts) + }. + diff --git a/apps/fistful/src/hg_cash_range.erl b/apps/fistful/src/hg_cash_range.erl new file mode 100644 index 0000000..7a0e088 --- /dev/null +++ b/apps/fistful/src/hg_cash_range.erl @@ -0,0 +1,40 @@ +%% TODO merge with ff_range + +-module(hg_cash_range). +-include_lib("dmsl/include/dmsl_domain_thrift.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}. + +is_inside(Cash, CashRange = #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({misconfiguration, {'Invalid cash range specified', CashRange, Cash}}) + end. + +-define(cash(Amount, SymCode), + #domain_Cash{ + amount = Amount, + currency = #domain_CurrencyRef{symbolic_code = SymCode} + }). + +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/fistful/src/hg_condition.erl b/apps/fistful/src/hg_condition.erl new file mode 100644 index 0000000..2f30a9e --- /dev/null +++ b/apps/fistful/src/hg_condition.erl @@ -0,0 +1,47 @@ +-module(hg_condition). +-include_lib("dmsl/include/dmsl_domain_thrift.hrl"). + +%% + +-export([test/2]). + +%% + +-type condition() :: dmsl_domain_thrift:'Condition'(). +-type varset() :: hg_selector:varset(). + +-spec test(condition(), varset()) -> + 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}) -> + hg_cash_range:is_inside(C, V) =:= within; +test({payment_tool, C}, #{payment_tool := V}) -> + hg_payment_tool:test_condition(C, V); +test({shop_location_is, V}, #{shop := S}) -> + V =:= S#domain_Shop.location; +test({party, V}, #{party_id := PartyID} = VS) -> + test_party(V, PartyID, VS); +test({payout_method_is, V1}, #{payout_method := V2}) -> + V1 =:= V2; +test({identification_level_is, V1}, #{identification_level := V2}) -> + V1 =:= V2; +test(_, #{}) -> + undefined. + +test_party(#domain_PartyCondition{id = PartyID, definition = Def}, PartyID, 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. diff --git a/apps/fistful/src/hg_payment_tool.erl b/apps/fistful/src/hg_payment_tool.erl new file mode 100644 index 0000000..17af393 --- /dev/null +++ b/apps/fistful/src/hg_payment_tool.erl @@ -0,0 +1,91 @@ +%%% Payment tools + +-module(hg_payment_tool). +-include_lib("dmsl/include/dmsl_domain_thrift.hrl"). + +%% +-export([test_condition/2]). + +%% + +-type t() :: dmsl_domain_thrift:'PaymentTool'(). +-type condition() :: dmsl_domain_thrift:'PaymentToolCondition'(). + +%% + +-spec test_condition(condition(), t()) -> boolean() | undefined. + +test_condition({bank_card, C}, {bank_card, V = #domain_BankCard{}}) -> + test_bank_card_condition(C, V); +test_condition({payment_terminal, C}, {payment_terminal, V = #domain_PaymentTerminal{}}) -> + test_payment_terminal_condition(C, V); +test_condition({digital_wallet, C}, {digital_wallet, V = #domain_DigitalWallet{}}) -> + test_digital_wallet_condition(C, V); +test_condition(_PaymentTool, _Condition) -> + false. + +test_bank_card_condition(#domain_BankCardCondition{definition = Def}, V) when Def /= undefined -> + test_bank_card_condition_def(Def, V); +test_bank_card_condition(#domain_BankCardCondition{}, _) -> + true. + +% legacy +test_bank_card_condition_def( + {payment_system_is, Ps}, + #domain_BankCard{payment_system = Ps, token_provider = undefined} +) -> + true; +test_bank_card_condition_def({payment_system_is, _Ps}, #domain_BankCard{}) -> + false; + +test_bank_card_condition_def({payment_system, PaymentSystem}, V) -> + test_payment_system_condition(PaymentSystem, V); +test_bank_card_condition_def({issuer_country_is, IssuerCountry}, V) -> + test_issuer_country_condition(IssuerCountry, V); +test_bank_card_condition_def({issuer_bank_is, BankRef}, V) -> + test_issuer_bank_condition(BankRef, V). + +test_payment_system_condition( + #domain_PaymentSystemCondition{payment_system_is = Ps, token_provider_is = Tp}, + #domain_BankCard{payment_system = Ps, token_provider = Tp} +) -> + true; +test_payment_system_condition(#domain_PaymentSystemCondition{}, #domain_BankCard{}) -> + false. + +test_issuer_country_condition(_Country, #domain_BankCard{issuer_country = undefined}) -> + undefined; +test_issuer_country_condition(Country, #domain_BankCard{issuer_country = TargetCountry}) -> + Country == TargetCountry. + +test_issuer_bank_condition(BankRef, #domain_BankCard{bank_name = BankName, bin = BIN}) -> + %% TODO this is complete bullshitery. Rework this check so we don't need to go to domain_config. + Bank = ff_pipeline:unwrap(ff_domain_config:object({bank, BankRef})), + #domain_Bank{binbase_id_patterns = Patterns, bins = BINs} = Bank, + 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_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({provider_is, V1}, #domain_PaymentTerminal{terminal_type = V2}) -> + V1 =:= V2. + +test_digital_wallet_condition(#domain_DigitalWalletCondition{definition = Def}, V) -> + Def =:= undefined orelse test_digital_wallet_condition_def(Def, V). + +test_digital_wallet_condition_def({provider_is, V1}, #domain_DigitalWallet{provider = V2}) -> + V1 =:= V2. + diff --git a/apps/fistful/src/hg_selector.erl b/apps/fistful/src/hg_selector.erl new file mode 100644 index 0000000..89891d9 --- /dev/null +++ b/apps/fistful/src/hg_selector.erl @@ -0,0 +1,162 @@ +%%% 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 + +-module(hg_selector). + +%% + +-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:'ProviderSelector'() | + dmsl_domain_thrift:'TerminalSelector'() | + dmsl_domain_thrift:'SystemAccountSetSelector'() | + dmsl_domain_thrift:'ExternalAccountSetSelector'() | + dmsl_domain_thrift:'HoldLifetimeSelector'() | + dmsl_domain_thrift:'CashValueSelector'() | + dmsl_domain_thrift:'CumulativeLimitSelector'() | + dmsl_domain_thrift:'WithdrawalProviderSelector'() | + dmsl_domain_thrift:'TimeSpanSelector'(). + +-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_id => dmsl_domain_thrift:'PartyID'(), + shop_id => dmsl_domain_thrift:'ShopID'(), + risk_score => dmsl_domain_thrift:'RiskScore'(), + flow => instant | {hold, dmsl_domain_thrift:'HoldLifetime'()}, + payout_method => dmsl_domain_thrift:'PayoutMethodRef'(), + wallet_id => dmsl_domain_thrift:'WalletID'(), + identification_level => dmsl_domain_thrift:'ContractorIdentificationLevel'() +}. + +-export_type([varset/0]). + +-export([fold/3]). +-export([collect/1]). +-export([reduce/2]). +-export([reduce_to_value/2]). + +-define(const(Bool), {constant, Bool}). + +%% + +-spec fold(FoldWith :: fun((Value :: _, Acc) -> Acc), Acc, t()) -> + Acc when + Acc :: term(). + +fold(FoldWith, Acc, {value, V}) -> + FoldWith(V, Acc); +fold(FoldWith, Acc, {decisions, Ps}) -> + fold_decisions(FoldWith, Acc, Ps). + +fold_decisions(FoldWith, Acc, [{_Type, _, S} | Rest]) -> + fold_decisions(FoldWith, fold(FoldWith, Acc, S), Rest); +fold_decisions(_, Acc, []) -> + Acc. + +-spec collect(t()) -> + [value()]. + +collect(S) -> + fold(fun (V, Acc) -> [V | Acc] end, [], S). + + +-spec reduce_to_value(t(), varset()) -> {ok, value()} | {error, term()}. + +reduce_to_value(Selector, VS) -> + case reduce(Selector, VS) of + {value, Value} -> + {ok, Value}; + _ -> + {error, {misconfiguration, {'Can\'t reduce selector to value', Selector, VS}}} + end. + +-spec reduce(t(), varset()) -> + t(). + +reduce({value, _} = V, _) -> + V; +reduce({decisions, Ps}, VS) -> + case reduce_decisions(Ps, VS) of + [{_Type, ?const(true), S} | _] -> + S; + Ps1 -> + {decisions, Ps1} + end. + +reduce_decisions([{Type, V, S} | Rest], VS) -> + case reduce_predicate(V, VS) of + ?const(false) -> + reduce_decisions(Rest, VS); + V1 -> + case reduce(S, VS) of + {decisions, []} -> + reduce_decisions(Rest, VS); + S1 -> + [{Type, V1, S1} | reduce_decisions(Rest, VS)] + end + end; +reduce_decisions([], _) -> + []. + +reduce_predicate(?const(B), _) -> + ?const(B); + +reduce_predicate({condition, C0}, VS) -> + case reduce_condition(C0, VS) of + ?const(B) -> + ?const(B); + C1 -> + {condition, C1} + end; + +reduce_predicate({is_not, P0}, VS) -> + case reduce_predicate(P0, VS) of + ?const(B) -> + ?const(not B); + P1 -> + {is_not, P1} + end; + +reduce_predicate({all_of, Ps}, VS) -> + reduce_combination(all_of, false, Ps, VS, []); + +reduce_predicate({any_of, Ps}, VS) -> + reduce_combination(any_of, true, Ps, VS, []). + +reduce_combination(Type, Fix, [P | Ps], VS, PAcc) -> + case reduce_predicate(P, VS) of + ?const(Fix) -> + ?const(Fix); + ?const(_) -> + reduce_combination(Type, Fix, Ps, VS, PAcc); + P1 -> + reduce_combination(Type, Fix, Ps, VS, [P1 | PAcc]) + end; +reduce_combination(_, Fix, [], _, []) -> + ?const(not Fix); +reduce_combination(Type, _, [], _, PAcc) -> + {Type, lists:reverse(PAcc)}. + +reduce_condition(C, VS) -> + case hg_condition:test(C, VS) of + B when is_boolean(B) -> + ?const(B); + undefined -> + % Irreducible, return as is + C + end. diff --git a/config/sys.config b/config/sys.config index 706c0f3..f4c0ae2 100644 --- a/config/sys.config +++ b/config/sys.config @@ -83,7 +83,7 @@ id => <<"some_id">>, identity => <<"some_other_id">>, currency => <<"RUB">>, - accounter_account_id => <<"some_third_id">> + accounter_account_id => 123 } }, fee => #{<<"RUB">> => #{postings => []}} diff --git a/docker-compose.sh b/docker-compose.sh index 8d81d33..1de3ab7 100755 --- a/docker-compose.sh +++ b/docker-compose.sh @@ -46,7 +46,7 @@ services: retries: 10 hellgate: - image: dr.rbkmoney.com/rbkmoney/hellgate:8d7f618f6f2e1d8410384797b8f9a76150580f46 + image: dr.rbkmoney.com/rbkmoney/hellgate:a1ea6053fe2d0d446e1c69735ca63ab0d493a87a command: /opt/hellgate/bin/hellgate foreground depends_on: machinegun: @@ -86,7 +86,7 @@ services: retries: 20 dominant: - image: dr.rbkmoney.com/rbkmoney/dominant:3cf6c46d482f0057d117209170c831f5a238d95a + image: dr.rbkmoney.com/rbkmoney/dominant:2f9f7e3d06972bc341bf55e9948435e202b578a2 command: /opt/dominant/bin/dominant foreground depends_on: machinegun: @@ -187,8 +187,8 @@ services: retries: 10 environment: - SPRING_DATASOURCE_PASSWORD=postgres - - SERVICE_NAME=ffmagista - + - SERVICE_NAME=ffmagista + ffmagista-db: image: dr.rbkmoney.com/rbkmoney/postgres:9.6 environment: