mirror of
https://github.com/valitydev/fistful-server.git
synced 2024-11-06 02:35:18 +00:00
DC-104: withdrawals routing (#54)
* DC-104: withdrawals routing shittycode typhoon * linter fix * renaming * remove default IdentityID * postmerge fix * better identity IDs in tests
This commit is contained in:
parent
487cfaf843
commit
27bea54b03
@ -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)}
|
||||
|
@ -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
|
||||
}
|
||||
}}.
|
||||
|
@ -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))]),
|
||||
|
@ -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) ->
|
||||
|
@ -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) ->
|
||||
|
@ -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)
|
||||
}}.
|
||||
|
@ -3,7 +3,7 @@
|
||||
%%%
|
||||
%%% TODOs
|
||||
%%%
|
||||
%%% - Anything remotely similar to routing!
|
||||
%%% - Replace with ff_payouts_provider after update!
|
||||
%%%
|
||||
|
||||
-module(ff_withdrawal_provider).
|
||||
|
@ -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)}.
|
||||
|
||||
|
17
apps/fistful/src/ff_cash.erl
Normal file
17
apps/fistful/src/ff_cash.erl
Normal file
@ -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}
|
||||
}.
|
@ -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
|
||||
|
@ -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()) ->
|
||||
|
133
apps/fistful/src/ff_payment_institution.erl
Normal file
133
apps/fistful/src/ff_payment_institution.erl
Normal file
@ -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.
|
129
apps/fistful/src/ff_payouts_provider.erl
Normal file
129
apps/fistful/src/ff_payouts_provider.erl
Normal file
@ -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)
|
||||
}.
|
||||
|
40
apps/fistful/src/hg_cash_range.erl
Normal file
40
apps/fistful/src/hg_cash_range.erl
Normal file
@ -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.
|
47
apps/fistful/src/hg_condition.erl
Normal file
47
apps/fistful/src/hg_condition.erl
Normal file
@ -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.
|
91
apps/fistful/src/hg_payment_tool.erl
Normal file
91
apps/fistful/src/hg_payment_tool.erl
Normal file
@ -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.
|
||||
|
162
apps/fistful/src/hg_selector.erl
Normal file
162
apps/fistful/src/hg_selector.erl
Normal file
@ -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.
|
@ -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 => []}}
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user