diff --git a/Makefile b/Makefile index d33a164d..224656b3 100644 --- a/Makefile +++ b/Makefile @@ -108,6 +108,15 @@ common-test: common-test.%: apps/hellgate/test/hg_%_tests_SUITE.erl $(REBAR) ct --cover --suite=$^ $(if $(CT_CASE),--case=$(strip $(CT_CASE))) +common-ff-server-test.%: apps/ff_server/test/ff_%_SUITE.erl + $(REBAR) ct --cover --suite=$^ $(if $(CT_CASE),--case=$(strip $(CT_CASE))) + +common-ff-transfer-test.%: apps/ff_transfer/test/ff_%_SUITE.erl + $(REBAR) ct --cover --suite=$^ $(if $(CT_CASE),--case=$(strip $(CT_CASE))) + +common-fistful-test.%: apps/fistful/test/ff_%_SUITE.erl + $(REBAR) ct --cover --suite=$^ $(if $(CT_CASE),--case=$(strip $(CT_CASE))) + cover: $(REBAR) covertool generate diff --git a/apps/ff_cth/src/ct_domain.erl b/apps/ff_cth/src/ct_domain.erl index 91f59716..d8a19784 100644 --- a/apps/ff_cth/src/ct_domain.erl +++ b/apps/ff_cth/src/ct_domain.erl @@ -27,6 +27,7 @@ -export([globals/2]). -export([withdrawal_provider/4]). -export([withdrawal_provider/5]). +-export([withdrawal_provider/6]). -export([withdrawal_terminal/2]). -export([withdrawal_terminal/3]). @@ -120,8 +121,8 @@ create_wallet(WalletID, PartyID, Currency, TermsRef, PaymentInstRef) -> ?DTP('ProvisionTermSet') | undefined ) -> object(). withdrawal_provider(Ref, ProxyRef, Realm, TermSet) -> - {ok, AccountID} = ct_helper:create_account(<<"RUB">>), - withdrawal_provider(AccountID, Ref, ProxyRef, Realm, TermSet). + {ok, SettlementAccountID} = ct_helper:create_account(<<"RUB">>), + withdrawal_provider(SettlementAccountID, Ref, ProxyRef, Realm, TermSet). -spec withdrawal_provider( ff_account:account_id(), @@ -130,7 +131,19 @@ withdrawal_provider(Ref, ProxyRef, Realm, TermSet) -> ?DTP('PaymentInstitutionRealm'), ?DTP('ProvisionTermSet') | undefined ) -> object(). -withdrawal_provider(AccountID, ?prv(ID) = Ref, ProxyRef, Realm, TermSet) -> +withdrawal_provider(AccountID, Ref, ProxyRef, Realm, TermSet) -> + {ok, GuaranteeAccountID} = ct_helper:create_account(<<"RUB">>), + withdrawal_provider(AccountID, GuaranteeAccountID, Ref, ProxyRef, Realm, TermSet). + +-spec withdrawal_provider( + ff_account:account_id(), + ff_account:account_id() | undefined, + ?DTP('ProviderRef'), + ?DTP('ProxyRef'), + ?DTP('PaymentInstitutionRealm'), + ?DTP('ProvisionTermSet') | undefined +) -> object(). +withdrawal_provider(SettlementAccountID, GuaranteeAccountID, ?prv(ID) = Ref, ProxyRef, Realm, TermSet) -> {provider, #domain_ProviderObject{ ref = Ref, data = #domain_Provider{ @@ -140,7 +153,10 @@ withdrawal_provider(AccountID, ?prv(ID) = Ref, ProxyRef, Realm, TermSet) -> realm = Realm, terms = TermSet, accounts = #{ - ?cur(<<"RUB">>) => #domain_ProviderAccount{settlement = AccountID} + ?cur(<<"RUB">>) => #domain_ProviderAccount{ + settlement = SettlementAccountID, + guarantee = GuaranteeAccountID + } } } }}. diff --git a/apps/ff_cth/src/ct_payment_system.erl b/apps/ff_cth/src/ct_payment_system.erl index b117a849..40b1651f 100644 --- a/apps/ff_cth/src/ct_payment_system.erl +++ b/apps/ff_cth/src/ct_payment_system.erl @@ -405,6 +405,38 @@ domain_config(Config, Options) -> } } }, + GuaranteeProviderTermSet = #domain_ProvisionTermSet{ + wallet = #domain_WalletProvisionTerms{ + withdrawals = #domain_WithdrawalProvisionTerms{ + currencies = {value, ?ordset([?cur(<<"RUB">>)])}, + 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, guarantee}, + {product, + {min_of, + ?ordset([ + ?fixed(10, <<"RUB">>), + ?share(5, 100, operation_amount, round_half_towards_zero) + ])}} + ) + ]} + } + ]} + } + } + }, Default = [ ct_domain:globals(?eas(1), [?payinst(1)]), ct_domain:external_account_set(?eas(1), <<"Default">>, ?cur(<<"RUB">>)), @@ -455,6 +487,10 @@ domain_config(Config, Options) -> routing_ruleset( ?ruleset(?PAYINST1_ROUTING_POLICIES + 4), {delegates, [ + delegate( + condition(cost_in, {800, <<"RUB">>}), + ?ruleset(?PAYINST1_ROUTING_POLICIES + 50) + ), delegate( condition(cost_in, {300, 302, <<"RUB">>}), ?ruleset(?PAYINST1_ROUTING_POLICIES + 5) @@ -754,6 +790,13 @@ domain_config(Config, Options) -> ]} ), + routing_ruleset( + ?ruleset(?PAYINST1_ROUTING_POLICIES + 50), + {candidates, [ + candidate({constant, true}, ?trm(1801)) + ]} + ), + routing_ruleset( ?ruleset(?PAYINST1_ROUTING_PROHIBITIONS), <<"PayInst1 Withdrawal Prohibitions">>, @@ -979,6 +1022,7 @@ domain_config(Config, Options) -> ct_domain:withdrawal_provider(?prv(11), ?prx(8), live, ProviderTermSet), ct_domain:withdrawal_provider(?prv(16), ?prx(2), live, undefined), ct_domain:withdrawal_provider(?prv(17), ?prx(2), live, ProviderTermSet), + ct_domain:withdrawal_provider(?prv(18), ?prx(2), live, GuaranteeProviderTermSet), ct_domain:term_set_hierarchy(?trms(1), default_termset(Options)), ct_domain:term_set_hierarchy(?trms(2), company_termset(Options)), @@ -1023,6 +1067,7 @@ domain_config(Config, Options) -> ct_domain:withdrawal_terminal(?trm(1101), ?prv(11)), ct_domain:withdrawal_terminal(?trm(1701), ?prv(17)), + ct_domain:withdrawal_terminal(?trm(1801), ?prv(18)), ct_domain:withdrawal_terminal( ?trm(1708), ?prv(17), diff --git a/apps/ff_server/src/ff_cash_flow_codec.erl b/apps/ff_server/src/ff_cash_flow_codec.erl index 0c580004..8487262d 100644 --- a/apps/ff_server/src/ff_cash_flow_codec.erl +++ b/apps/ff_server/src/ff_cash_flow_codec.erl @@ -118,10 +118,32 @@ final_cash_flow_symmetry_test() -> volume => {100, <<"EUR">>} } end, + ProviderGuaranteePosting = #{ + sender => #{ + account => #{ + realm => test, + party_id => genlib:unique(), + currency => <<"RUB">>, + account_id => 456 + }, + type => {provider, settlement} + }, + receiver => #{ + account => #{ + realm => test, + party_id => genlib:unique(), + currency => <<"RUB">>, + account_id => 789 + }, + type => {provider, guarantee} + }, + volume => {100, <<"RUB">>} + }, CashFlow = #{ postings => [ PostingFn(), - PostingFn() + PostingFn(), + ProviderGuaranteePosting ] }, ?assertEqual(CashFlow, unmarshal(final_cash_flow, marshal(final_cash_flow, CashFlow))). diff --git a/apps/ff_server/test/ff_withdrawal_handler_SUITE.erl b/apps/ff_server/test/ff_withdrawal_handler_SUITE.erl index f7b65406..f870861a 100644 --- a/apps/ff_server/test/ff_withdrawal_handler_SUITE.erl +++ b/apps/ff_server/test/ff_withdrawal_handler_SUITE.erl @@ -9,6 +9,7 @@ -include_lib("fistful_proto/include/fistful_fistful_thrift.hrl"). -include_lib("fistful_proto/include/fistful_fistful_base_thrift.hrl"). -include_lib("fistful_proto/include/fistful_cashflow_thrift.hrl"). +-include_lib("fistful_proto/include/fistful_account_thrift.hrl"). -include_lib("fistful_proto/include/fistful_transfer_thrift.hrl"). -include_lib("ff_cth/include/ct_domain.hrl"). @@ -48,6 +49,7 @@ -export([create_adjustment_invalid_operation_amount_error_test/1]). -export([create_adjustment_change_body_ok_test/1]). -export([withdrawal_state_content_test/1]). +-export([withdrawal_with_provider_guarantee_account_test/1]). -export([trace_withdrawal_test/1]). -export([create_withdrawal_with_changed_body_test/1]). @@ -98,7 +100,8 @@ groups() -> create_adjustment_already_has_body_error_test, create_adjustment_invalid_operation_amount_error_test, create_adjustment_change_body_ok_test, - withdrawal_state_content_test + withdrawal_state_content_test, + withdrawal_with_provider_guarantee_account_test ]} ]. @@ -936,6 +939,68 @@ withdrawal_state_content_test(_C) -> ?assertNotEqual(undefined, WithdrawalState#wthd_WithdrawalState.effective_route), ?assertNotEqual(undefined, WithdrawalState#wthd_WithdrawalState.status). +-spec withdrawal_with_provider_guarantee_account_test(config()) -> test_return(). +withdrawal_with_provider_guarantee_account_test(_C) -> + %% 800 RUB is routed to provider 18 / terminal 1801, whose terms post the fee to guarantee + Cash = make_cash({800, <<"RUB">>}), + GuaranteeAccountID = provider_guarantee_account_id(18), + Ctx = ct_objects:build_default_ctx(), + #{withdrawal_id := WithdrawalID} = ct_objects:prepare_standard_environment(Ctx#{body => Cash}), + succeeded = ct_objects:await_final_withdrawal_status(WithdrawalID), + + %% the provider fee is the lesser of 10 RUB and 5% of 800 RUB, posted to the guarantee account + {ok, #wthd_WithdrawalState{ + effective_final_cash_flow = #cashflow_FinalCashFlow{postings = Postings} + }} = call_withdrawal('Get', {WithdrawalID, #'fistful_base_EventRange'{}}), + [ + #cashflow_FinalCashFlowPosting{ + source = #cashflow_FinalCashFlowAccount{account_type = {system, settlement}}, + destination = #cashflow_FinalCashFlowAccount{ + account_type = {provider, guarantee}, + account = #'account_Account'{account_id = GuaranteeAccountID} + }, + volume = #fistful_base_Cash{amount = 10, currency = #'fistful_base_CurrencyRef'{symbolic_code = <<"RUB">>}} + } + ] = [ + P + || #cashflow_FinalCashFlowPosting{ + destination = #cashflow_FinalCashFlowAccount{account_type = {provider, guarantee}} + } = P <- + Postings + ], + + %% the same posting must survive in the serialized transfer event + Range = {undefined, undefined}, + EncodedRange = ff_codec:marshal(event_range, Range), + {ok, Events} = call_withdrawal('GetEvents', {WithdrawalID, EncodedRange}), + [CreatedEvent] = [ + E + || #wthd_Event{change = {transfer, #wthd_TransferChange{payload = {created, _}}}} = E <- Events + ], + #wthd_Event{ + change = + {transfer, #wthd_TransferChange{ + payload = + {created, #transfer_CreatedChange{ + transfer = #transfer_Transfer{ + cashflow = #cashflow_FinalCashFlow{postings = EventPostings} + } + }} + }} + } = CreatedEvent, + [ + #cashflow_FinalCashFlowPosting{ + destination = #cashflow_FinalCashFlowAccount{account_type = {provider, guarantee}} + } + ] = [ + P + || #cashflow_FinalCashFlowPosting{ + destination = #cashflow_FinalCashFlowAccount{account_type = {provider, guarantee}} + } = P <- + EventPostings + ], + ok. + %% Internals call_withdrawal_session(Fun, Args) -> @@ -969,3 +1034,8 @@ make_cash({Amount, Currency}) -> amount = Amount, currency = #'fistful_base_CurrencyRef'{symbolic_code = Currency} }. + +provider_guarantee_account_id(ProviderID) -> + {ok, Provider} = ff_payouts_provider:get(ProviderID, ct_domain_config:head()), + #{guarantee := GuaranteeAccount} = maps:get(<<"RUB">>, ff_payouts_provider:accounts(Provider)), + ff_account:account_id(GuaranteeAccount). diff --git a/apps/ff_transfer/src/ff_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl index 3a530de2..266040ea 100644 --- a/apps/ff_transfer/src/ff_withdrawal.erl +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -1121,7 +1121,9 @@ make_final_cash_flow(DomainRevision, Withdrawal) -> #{provider_id := ProviderID} = Route, {ok, Provider} = ff_payouts_provider:get(ProviderID, DomainRevision), ProviderAccounts = ff_payouts_provider:accounts(Provider), - ProviderAccount = maps:get(CurrencyID, ProviderAccounts, undefined), + ProviderAccount = maps:get(CurrencyID, ProviderAccounts, #{}), + ProviderSettlementAccount = maps:get(settlement, ProviderAccount, undefined), + ProviderGuaranteeAccount = maps:get(guarantee, ProviderAccount, undefined), #domain_WalletConfig{payment_institution = PaymentInstitutionRef} = Wallet, {ok, PaymentInstitution} = ff_payment_institution:get(PaymentInstitutionRef, PartyVarset, DomainRevision), @@ -1144,7 +1146,8 @@ make_final_cash_flow(DomainRevision, Withdrawal) -> {wallet, receiver_destination} => DestinationAccount, {system, settlement} => SettlementAccount, {system, subagent} => SubagentAccount, - {provider, settlement} => ProviderAccount + {provider, settlement} => ProviderSettlementAccount, + {provider, guarantee} => ProviderGuaranteeAccount }), {ok, FinalCashFlow} = ff_cash_flow:finalize(CashFlowPlan, Accounts, Constants), diff --git a/apps/ff_transfer/test/ff_withdrawal_SUITE.erl b/apps/ff_transfer/test/ff_withdrawal_SUITE.erl index ce5ad64d..3c881a83 100644 --- a/apps/ff_transfer/test/ff_withdrawal_SUITE.erl +++ b/apps/ff_transfer/test/ff_withdrawal_SUITE.erl @@ -50,6 +50,7 @@ -export([provider_terminal_terms_merging_test/1]). -export([force_status_change_test/1]). -export([withdrawal_without_termset_test/1]). +-export([provider_guarantee_account_test/1]). %% Internal types @@ -122,7 +123,8 @@ groups() -> provider_terminal_terms_merging_test ]}, {non_parallel, [], [ - use_quote_revisions_test + use_quote_revisions_test, + provider_guarantee_account_test ]}, {withdrawal_repair, [], [ force_status_change_test @@ -800,6 +802,56 @@ withdrawal_without_termset_test(C) -> ), ok. +-spec provider_guarantee_account_test(config()) -> test_return(). +provider_guarantee_account_test(C) -> + %% 800 RUB is routed to provider 18 / terminal 1801, whose terms post the fee to guarantee + Cash = {800, <<"RUB">>}, + #{ + wallet_id := WalletID, + destination_id := DestinationID, + party_id := PartyID + } = prepare_standard_environment(Cash, C), + {SettlementAccountID, GuaranteeAccountID} = provider_account_ids(18), + %% the settlement account is shared between the suite cases, so only its change is asserted + SettlementBefore = get_account_amount(SettlementAccountID), + GuaranteeBefore = get_account_amount(GuaranteeAccountID), + WithdrawalID = genlib:bsuuid(), + WithdrawalParams = #{ + id => WithdrawalID, + destination_id => DestinationID, + wallet_id => WalletID, + party_id => PartyID, + body => Cash, + external_id => WithdrawalID + }, + ok = ff_withdrawal_machine:create(WithdrawalParams, ff_entity_context:new()), + ?assertEqual(succeeded, await_final_withdrawal_status(WithdrawalID)), + ?assertEqual(?FINAL_BALANCE(0, <<"RUB">>), get_wallet_balance(WalletID)), + ?assertEqual(?FINAL_BALANCE(640, <<"RUB">>), get_destination_balance(DestinationID)), + + Withdrawal = get_withdrawal(WithdrawalID), + #{ + postings := Postings + } = ff_withdrawal:effective_final_cash_flow(Withdrawal), + %% the provider fee is the lesser of 10 RUB and 5% of 800 RUB, posted to the guarantee account + [ + #{ + sender := #{type := {system, settlement}}, + receiver := #{type := {provider, guarantee}, account := ReceiverAccount}, + volume := Volume + } + ] = [ + P + || #{receiver := #{type := {provider, guarantee}}} = P <- Postings + ], + ?assertEqual(GuaranteeAccountID, maps:get(account_id, ReceiverAccount)), + ?assertEqual({10, <<"RUB">>}, Volume), + + %% the provider fee is credited to the guarantee account only, the settlement one is left intact + ?assertEqual(GuaranteeBefore + 10, get_account_amount(GuaranteeAccountID)), + ?assertEqual(SettlementBefore, get_account_amount(SettlementAccountID)), + ok. + -spec unknown_test(config()) -> test_return(). unknown_test(_C) -> WithdrawalID = <<"unknown_withdrawal">>, @@ -1035,6 +1087,28 @@ await_wallet_balance({Amount, Currency}, ID) -> get_wallet_balance(ID) -> ct_objects:get_wallet_balance(ID). +get_destination_balance(ID) -> + {ok, Machine} = ff_destination_machine:get(ID), + Destination = ff_destination_machine:destination(Machine), + get_account_balance(ff_destination:account(Destination)). + +get_account_balance(AccountID) when is_integer(AccountID) -> + {ok, {Amounts, Currency}} = ff_accounting:balance(AccountID, <<"RUB">>), + {ff_indef:current(Amounts), ff_indef:to_range(Amounts), Currency}; +get_account_balance(Account) -> + {ok, {Amounts, Currency}} = ff_accounting:balance(Account), + {ff_indef:current(Amounts), ff_indef:to_range(Amounts), Currency}. + +get_account_amount(AccountID) -> + {Amount, _Range, _Currency} = get_account_balance(AccountID), + Amount. + +provider_account_ids(ProviderID) -> + {ok, Provider} = ff_payouts_provider:get(ProviderID, ct_domain_config:head()), + #{settlement := SettlementAccount, guarantee := GuaranteeAccount} = + maps:get(<<"RUB">>, ff_payouts_provider:accounts(Provider)), + {ff_account:account_id(SettlementAccount), ff_account:account_id(GuaranteeAccount)}. + create_crypto_destination(PartyID, _C) -> ID = genlib:bsuuid(), Resource = diff --git a/apps/ff_transfer/test/ff_withdrawal_adjustment_SUITE.erl b/apps/ff_transfer/test/ff_withdrawal_adjustment_SUITE.erl index 64d965b6..920f311b 100644 --- a/apps/ff_transfer/test/ff_withdrawal_adjustment_SUITE.erl +++ b/apps/ff_transfer/test/ff_withdrawal_adjustment_SUITE.erl @@ -2,6 +2,7 @@ -include_lib("stdlib/include/assert.hrl"). -include_lib("damsel/include/dmsl_domain_thrift.hrl"). +-include_lib("ff_cth/include/ct_domain.hrl"). %% Common test API @@ -29,6 +30,7 @@ -export([adjustment_can_not_change_domain_revision_to_same/1]). -export([adjustment_can_not_change_domain_revision_with_failed_status/1]). -export([adjustment_can_change_domain_revision_test/1]). +-export([adjustment_can_change_cash_flow_to_guarantee_account_test/1]). -export([adjustment_fail_change_body_succeed_test/1]). -export([adjustment_can_change_body_on_succeeded_test/1]). -export([adjustment_change_cash_flow_then_change_body_test/1]). @@ -82,7 +84,8 @@ groups() -> {non_parallel, [], [ adjustment_can_change_domain_revision_test, adjustment_change_cash_flow_then_change_body_test, - adjustment_change_body_then_change_cash_flow_test + adjustment_change_body_then_change_cash_flow_test, + adjustment_can_change_cash_flow_to_guarantee_account_test ]} ]. @@ -392,6 +395,52 @@ adjustment_can_change_domain_revision_test(C) -> ?assertEqual(?FINAL_BALANCE(0, <<"RUB">>), get_wallet_balance(WalletID)), ?assertEqual(?FINAL_BALANCE(80, <<"RUB">>), get_destination_balance(DestinationID)). +-spec adjustment_can_change_cash_flow_to_guarantee_account_test(config()) -> test_return(). +adjustment_can_change_cash_flow_to_guarantee_account_test(C) -> + ProviderID = 1, + InitialProviderFee = 2, + AdjustedProviderFee = 5, + ?FINAL_BALANCE(StartProviderAmount, <<"RUB">>) = get_provider_balance(ProviderID, ct_domain_config:head()), + %% pin withdrawal to a domain revision with a fixed 2 RUB fee on settlement + _SettlementRevision = configure_provider_settlement_cashflow(ProviderID), + #{ + withdrawal_id := WithdrawalID, + wallet_id := WalletID, + destination_id := DestinationID + } = prepare_standard_environment({100, <<"RUB">>}, C), + ?assertEqual(?FINAL_BALANCE(0, <<"RUB">>), get_wallet_balance(WalletID)), + ?assertEqual(?FINAL_BALANCE(80, <<"RUB">>), get_destination_balance(DestinationID)), + Withdrawal = get_withdrawal(WithdrawalID), + #{provider_id := ProviderID} = ff_withdrawal:route(Withdrawal), + DomainRevision = ff_withdrawal:domain_revision(Withdrawal), + ?assertEqual( + ?FINAL_BALANCE(StartProviderAmount + InitialProviderFee, <<"RUB">>), + get_provider_balance(ProviderID, DomainRevision) + ), + + %% switch the provider fee cash flow from the settlement to the guarantee account + GuaranteeRevision = configure_provider_guarantee_cashflow(ProviderID, AdjustedProviderFee), + ?assertEqual(?FINAL_BALANCE(0, <<"RUB">>), get_provider_balance(ProviderID, GuaranteeRevision, guarantee)), + + AdjustmentID = process_adjustment(WithdrawalID, #{ + change => {change_cash_flow, GuaranteeRevision}, + external_id => <<"true_unique_id">> + }), + ?assertMatch(succeeded, get_adjustment_status(WithdrawalID, AdjustmentID)), + ?assertEqual(succeeded, get_withdrawal_status(WithdrawalID)), + assert_adjustment_same_revisions(WithdrawalID, AdjustmentID), + %% the old settlement posting is inverted, the new one is applied to the guarantee account + ?assertEqual( + ?FINAL_BALANCE(StartProviderAmount, <<"RUB">>), + get_provider_balance(ProviderID, GuaranteeRevision, settlement) + ), + ?assertEqual( + ?FINAL_BALANCE(AdjustedProviderFee, <<"RUB">>), + get_provider_balance(ProviderID, GuaranteeRevision, guarantee) + ), + ?assertEqual(?FINAL_BALANCE(0, <<"RUB">>), get_wallet_balance(WalletID)), + ?assertEqual(?FINAL_BALANCE(80, <<"RUB">>), get_destination_balance(DestinationID)). + -spec adjustment_fail_change_body_succeed_test(config()) -> test_return(). adjustment_fail_change_body_succeed_test(C) -> #{ @@ -639,10 +688,57 @@ get_destination_balance(ID) -> get_account_balance(ff_destination:account(Destination)). get_provider_balance(ProviderID, DomainRevision) -> + get_provider_balance(ProviderID, DomainRevision, settlement). + +get_provider_balance(ProviderID, DomainRevision, AccountType) -> {ok, Provider} = ff_payouts_provider:get(ProviderID, DomainRevision), ProviderAccounts = ff_payouts_provider:accounts(Provider), - ProviderAccount = maps:get(<<"RUB">>, ProviderAccounts, undefined), - get_account_balance(ProviderAccount). + ProviderAccount = maps:get(<<"RUB">>, ProviderAccounts, #{}), + get_account_balance(maps:get(AccountType, ProviderAccount, undefined)). + +configure_provider_settlement_cashflow(ProviderID) -> + configure_provider_cashflow(ProviderID, [ + ?cfpost( + {system, settlement}, + {provider, settlement}, + ?fixed(2, <<"RUB">>) + ) + ]). + +configure_provider_guarantee_cashflow(ProviderID, ProviderFee) -> + configure_provider_cashflow(ProviderID, [ + ?cfpost( + {system, settlement}, + {provider, guarantee}, + ?fixed(ProviderFee, <<"RUB">>) + ) + ]). + +configure_provider_cashflow(ProviderID, CashFlow) -> + ProviderRef = #domain_ProviderRef{id = ProviderID}, + #domain_Provider{} = Provider = ct_domain_config:get({provider, ProviderRef}), + _ = ct_domain_config:upsert( + {provider, #domain_ProviderObject{ + ref = ProviderRef, + data = Provider#domain_Provider{ + terms = #domain_ProvisionTermSet{ + wallet = #domain_WalletProvisionTerms{ + withdrawals = #domain_WithdrawalProvisionTerms{ + currencies = {value, ?ordset([?cur(<<"RUB">>)])}, + cash_limit = + {value, + ?cashrng( + {inclusive, ?cash(0, <<"RUB">>)}, + {exclusive, ?cash(10000000, <<"RUB">>)} + )}, + cash_flow = {value, CashFlow} + } + } + } + } + }} + ), + ct_domain_config:head(). get_account_balance(Account) -> {ok, {Amounts, Currency}} = ff_accounting:balance(Account), diff --git a/apps/fistful/src/ff_cash_flow.erl b/apps/fistful/src/ff_cash_flow.erl index 208c9178..6a726693 100644 --- a/apps/fistful/src/ff_cash_flow.erl +++ b/apps/fistful/src/ff_cash_flow.erl @@ -77,7 +77,8 @@ | {wallet, receiver_destination} | {system, settlement} | {system, subagent} - | {provider, settlement}. + | {provider, settlement} + | {provider, guarantee}. -type final_account() :: #{ account := account(), diff --git a/apps/fistful/src/ff_payouts_provider.erl b/apps/fistful/src/ff_payouts_provider.erl index d657d3de..974392b7 100644 --- a/apps/fistful/src/ff_payouts_provider.erl +++ b/apps/fistful/src/ff_payouts_provider.erl @@ -11,7 +11,12 @@ }. -type id() :: dmsl_domain_thrift:'ObjectID'(). --type accounts() :: #{ff_currency:id() => ff_account:account()}. +-type accounts() :: #{ff_currency:id() => provider_account()}. + +-type provider_account() :: #{ + settlement := ff_account:account(), + guarantee => ff_account:account() +}. -type provider_ref() :: dmsl_domain_thrift:'ProviderRef'(). -type term_set() :: dmsl_domain_thrift:'ProvisionTermSet'(). @@ -20,6 +25,7 @@ -export_type([id/0]). -export_type([provider/0]). +-export_type([provider_account/0]). -export_type([provider_ref/0]). -export_type([provision_terms/0]). -export_type([domain_revision/0]). @@ -115,14 +121,30 @@ decode_accounts(Realm, Accounts) -> maps:fold( fun(CurrencyRef, ProviderAccount, Acc) -> #domain_CurrencyRef{symbolic_code = CurrencyID} = CurrencyRef, - #domain_ProviderAccount{settlement = AccountID} = ProviderAccount, - Account = ff_account:build(Realm, AccountID, CurrencyID), - Acc#{CurrencyID => Account} + Acc#{CurrencyID => decode_provider_account(ProviderAccount, CurrencyID, Realm)} end, #{}, Accounts ). +decode_provider_account( + #domain_ProviderAccount{ + settlement = SettlementID, + guarantee = GuaranteeID + }, + CurrencyID, + Realm +) -> + genlib_map:compact(#{ + settlement => decode_account(SettlementID, CurrencyID, Realm), + guarantee => decode_account(GuaranteeID, CurrencyID, Realm) + }). + +decode_account(undefined, _CurrencyID, _Realm) -> + undefined; +decode_account(AccountID, CurrencyID, Realm) -> + ff_account:build(Realm, AccountID, CurrencyID). + decode_adapter(#domain_Proxy{ref = ProxyRef, additional = ProviderOpts}) -> Proxy = unwrap(ff_domain_config:object({proxy, ProxyRef})), #domain_ProxyDefinition{ diff --git a/apps/hellgate/src/hg_accounting.erl b/apps/hellgate/src/hg_accounting.erl index 94a0f00f..f7ae1565 100644 --- a/apps/hellgate/src/hg_accounting.erl +++ b/apps/hellgate/src/hg_accounting.erl @@ -125,11 +125,18 @@ collect_merchant_account_map(PartyConfigRef, {ShopConfigRef, #domain_ShopConfig{ -spec collect_provider_account_map(payment(), provider(), route(), map()) -> map(). collect_provider_account_map(Payment, #domain_Provider{accounts = ProviderAccounts}, Route, Acc) -> Currency = get_currency(get_payment_cost(Payment)), - ProviderAccount = hg_payment_institution:choose_provider_account(Currency, ProviderAccounts), - Acc#{ - provider => Route, - {provider, settlement} => ProviderAccount#domain_ProviderAccount.settlement - }. + #domain_ProviderAccount{ + settlement = Settlement, + guarantee = Guarantee + } = hg_payment_institution:choose_provider_account(Currency, ProviderAccounts), + maps:merge( + Acc, + genlib_map:compact(#{ + provider => Route, + {provider, settlement} => Settlement, + {provider, guarantee} => Guarantee + }) + ). -spec collect_system_account_map(payment(), payment_institution(), revision(), map()) -> map(). collect_system_account_map(Payment, PaymentInstitution, Revision, Acc) -> diff --git a/apps/hellgate/src/hg_invoice_payment.erl b/apps/hellgate/src/hg_invoice_payment.erl index c650fdff..35937b48 100644 --- a/apps/hellgate/src/hg_invoice_payment.erl +++ b/apps/hellgate/src/hg_invoice_payment.erl @@ -2353,14 +2353,33 @@ process_cash_flow_building(_Action, St) -> allocation => Allocation, exchange_context => ExchangeContext }), - FinalCashflow = calculate_cashflow(Context, Opts), - _ = rollback_unused_payment_limits(St), - _Clock = hg_accounting:hold( - construct_payment_plan_id(St), - {1, FinalCashflow} - ), - Events = [?cash_flow_changed(FinalCashflow)], - {next, {Events, timeout}}. + try calculate_cashflow(Context, Opts) of + FinalCashflow -> + _ = rollback_unused_payment_limits(St), + _Clock = hg_accounting:hold( + construct_payment_plan_id(St), + {1, FinalCashflow} + ), + {next, {[?cash_flow_changed(FinalCashflow)], timeout}} + catch + error:{misconfiguration, _} = Error -> + %% TODO Validate route-specific cash flow accounts during routing and consider + %% cascading to another candidate when the selected route is misconfigured. + ?LOG_MD(warning, "Cash flow building failed due to misconfiguration, route: ~p, error: ~p", [ + Route, Error + ]), + Failure = construct_cashflow_failure(Error), + Routes = get_candidate_routes(St), + _ = rollback_payment_limits(Routes, get_iter(St), St, [ignore_business_error, ignore_not_found]), + {done, {[?payment_status_changed(?failed(Failure))], timeout}} + end. + +construct_cashflow_failure({misconfiguration, Details}) -> + {failure, #domain_Failure{ + code = <<"misconfiguration">>, + sub = #domain_SubFailure{code = <<"cash_flow">>}, + reason = genlib:format(Details) + }}. %% diff --git a/apps/hellgate/test/hg_ct_domain.hrl b/apps/hellgate/test/hg_ct_domain.hrl index 04952162..0c593577 100644 --- a/apps/hellgate/test/hg_ct_domain.hrl +++ b/apps/hellgate/test/hg_ct_domain.hrl @@ -47,6 +47,7 @@ -define(cashrng(Lower, Upper), #domain_CashRange{lower = Lower, upper = Upper}). -define(prvacc(Stl), #domain_ProviderAccount{settlement = Stl}). +-define(prvacc(Stl, Grt), #domain_ProviderAccount{settlement = Stl, guarantee = Grt}). -define(partycond(Ref, Def), {condition, {party, #domain_PartyCondition{party_ref = Ref, definition = Def}}}). -define(fixed(Amount, Currency), diff --git a/apps/hellgate/test/hg_ct_fixture.erl b/apps/hellgate/test/hg_ct_fixture.erl index 265d9aac..1df9124f 100644 --- a/apps/hellgate/test/hg_ct_fixture.erl +++ b/apps/hellgate/test/hg_ct_fixture.erl @@ -162,7 +162,7 @@ construct_provider_account_set(Currencies) -> ok = op_context:save(op_context:key(hellgate), op_context:create()), AccountSet = lists:foldl( fun(Cur = ?cur(Code), Acc) -> - Acc#{Cur => ?prvacc(hg_accounting:create_account(Code))} + Acc#{Cur => ?prvacc(hg_accounting:create_account(Code), hg_accounting:create_account(Code))} end, #{}, Currencies diff --git a/apps/hellgate/test/hg_invoice_cashflow_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_cashflow_tests_SUITE.erl new file mode 100644 index 00000000..1d1b2968 --- /dev/null +++ b/apps/hellgate/test/hg_invoice_cashflow_tests_SUITE.erl @@ -0,0 +1,321 @@ +-module(hg_invoice_cashflow_tests_SUITE). + +-include_lib("hellgate/include/hg_invoice.hrl"). +-include_lib("hellgate/include/payment_events.hrl"). +-include_lib("hellgate/include/invoice_events.hrl"). +-include_lib("stdlib/include/assert.hrl"). +-include("hg_ct_domain.hrl"). +-include("hg_ct_invoice.hrl"). + +-export([all/0]). +-export([groups/0]). +-export([init_per_suite/1]). +-export([end_per_suite/1]). +-export([init_per_group/2]). +-export([end_per_group/2]). +-export([init_per_testcase/2]). +-export([end_per_testcase/2]). + +%% Tests +-export([payment_with_provider_settlement_account/1]). +-export([payment_with_provider_guarantee_account/1]). +-export([payment_undefined_provider_guarantee_account/1]). +-export([payment_adjustment_to_provider_guarantee_account/1]). + +-type config() :: hg_ct_helper:config(). +-type test_case_name() :: hg_ct_helper:test_case_name(). +-type group_name() :: hg_ct_helper:group_name(). +-type test_return() :: _ | no_return(). + +%% Supervisor +-behaviour(supervisor). + +-export([init/1]). + +-spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. +init([]) -> + {ok, {#{strategy => one_for_all, intensity => 1, period => 1}, []}}. + +-spec all() -> [test_case_name() | {group, group_name()}]. +all() -> + [ + payment_with_provider_settlement_account, + payment_with_provider_guarantee_account, + payment_undefined_provider_guarantee_account, + payment_adjustment_to_provider_guarantee_account + ]. + +-spec groups() -> [{group_name(), list(), [test_case_name()]}]. +groups() -> + []. + +-spec init_per_suite(config()) -> config(). +init_per_suite(C) -> + CowboySpec = hg_dummy_provider:get_http_cowboy_spec(), + {Apps, Ret} = hg_ct_helper:start_apps([ + woody, + scoper, + dmt_client, + bender_client, + party_client, + hg_proto, + epg_connector, + progressor, + hellgate, + {cowboy, CowboySpec}, + snowflake + ]), + RootUrl = maps:get(hellgate_root_url, Ret), + _ = hg_limiter_helper:init_per_suite(C), + _ = hg_domain:upsert(hg_invoice_dummy_data:construct_domain_fixture()), + PartyConfigRef = #domain_PartyConfigRef{id = hg_utils:unique_id()}, + PartyClient = {party_client:create_client(), party_client:create_context()}, + ok = op_context:save(op_context:key(hellgate), op_context:create()), + ShopConfigRef = hg_ct_helper:create_party_and_shop( + PartyConfigRef, ?cat(1), <<"RUB">>, ?trms(1), ?pinst(1), PartyClient + ), + ok = op_context:cleanup(hellgate), + {ok, SupPid} = supervisor:start_link(?MODULE, []), + _ = unlink(SupPid), + ok = hg_invoice_helper:start_kv_store(SupPid), + C1 = [ + {party_config_ref, PartyConfigRef}, + {shop_config_ref, ShopConfigRef}, + {root_url, RootUrl}, + {test_sup, SupPid}, + {apps, Apps} + | C + ], + ok = hg_invoice_helper:start_proxies([{hg_dummy_provider, 1, C1}, {hg_dummy_inspector, 2, C1}]), + [{base_domain_revision, hg_domain:head()} | C1]. + +-spec end_per_suite(config()) -> _. +end_per_suite(C) -> + _ = hg_domain:cleanup(), + _ = application:stop(progressor), + _ = hg_ct_helper:cleanup_progressor_namespaces(), + _ = [application:stop(App) || App <- cfg(apps, C)], + hg_invoice_helper:stop_kv_store(cfg(test_sup, C)), + exit(cfg(test_sup, C), shutdown). + +-spec init_per_group(group_name(), config()) -> config(). +init_per_group(_, C) -> + C. + +-spec end_per_group(group_name(), config()) -> _. +end_per_group(_Group, _C) -> + ok. + +-spec init_per_testcase(test_case_name(), config()) -> config(). +init_per_testcase(Name, C) -> + _ = hg_domain:reset(cfg(base_domain_revision, C)), + ApiClient = hg_ct_helper:create_client(cfg(root_url, C)), + Client = hg_client_invoicing:start_link(ApiClient), + ok = op_context:save(op_context:key(hellgate), op_context:create()), + C1 = [{client, Client} | C], + case Name of + payment_with_provider_guarantee_account -> + ok = configure_provider_guarantee_cashflow(), + C1; + payment_undefined_provider_guarantee_account -> + ok = unset_provider_guarantee_account(), + ok = configure_provider_guarantee_cashflow(), + C1; + _ -> + C1 + end. + +-spec end_per_testcase(test_case_name(), config()) -> config(). +end_per_testcase(_, C) -> + ok = op_context:cleanup(hellgate), + C. + +%% Tests + +-spec payment_with_provider_settlement_account(config()) -> test_return(). +payment_with_provider_settlement_account(C) -> + Amount = 42000, + {CashFlow, Route} = execute_payment(Amount, C), + #domain_ProviderAccount{settlement = SettlementAccountID} = provider_rub_account(), + [ + #domain_FinalCashFlowPosting{ + source = #domain_FinalCashFlowAccount{ + account_id = SettlementAccountID, + transaction_account = + {provider, #domain_ProviderTransactionAccount{ + type = settlement, + owner = #domain_ProviderTransactionAccountOwner{ + provider_ref = ?prv(1), + terminal_ref = ?trm(1) + } + }} + }, + volume = ?cash(Amount, <<"RUB">>) + } + ] = lookup_posting(CashFlow, {provider, settlement}, {merchant, settlement}), + assert_route(Route), + ok. + +-spec payment_with_provider_guarantee_account(config()) -> test_return(). +payment_with_provider_guarantee_account(C) -> + Amount = 42000, + #domain_ProviderAccount{guarantee = GuaranteeAccountID} = provider_rub_account(), + {CashFlow, Route} = execute_payment(Amount, C), + [ + #domain_FinalCashFlowPosting{ + source = #domain_FinalCashFlowAccount{ + account_id = GuaranteeAccountID, + transaction_account = + {provider, #domain_ProviderTransactionAccount{ + type = guarantee, + owner = #domain_ProviderTransactionAccountOwner{ + provider_ref = ?prv(1), + terminal_ref = ?trm(1) + } + }} + }, + volume = ?cash(Amount, <<"RUB">>) + } + ] = lookup_posting(CashFlow, {provider, guarantee}, {merchant, settlement}), + #{own_amount := GuaranteeBalance} = hg_accounting:get_balance(GuaranteeAccountID), + ?assertEqual(-Amount, GuaranteeBalance), + assert_route(Route), + ok. + +-spec payment_undefined_provider_guarantee_account(config()) -> test_return(). +payment_undefined_provider_guarantee_account(C) -> + Client = cfg(client, C), + #domain_ProviderAccount{guarantee = undefined} = provider_rub_account(), + + InvoiceID = hg_invoice_helper:start_invoice( + <<"undefined provider guarantee account">>, hg_invoice_helper:make_due_date(10), 42000, C + ), + PaymentParams = hg_invoice_helper:make_payment_params(?pmt_sys(<<"visa-ref">>)), + ?payment_state(?payment(PaymentID)) = hg_client_invoicing:start_payment(InvoiceID, PaymentParams, Client), + Route = hg_invoice_helper:start_payment_ev(InvoiceID, Client), + assert_route(Route), + + ?payment_ev(PaymentID, ?payment_status_changed(?failed({failure, Failure}))) = + hg_invoice_helper:next_change(InvoiceID, Client), + #domain_Failure{ + code = <<"misconfiguration">>, + sub = #domain_SubFailure{code = <<"cash_flow">>} + } = Failure, + #payproc_InvoicePayment{ + payment = #domain_InvoicePayment{status = ?failed({failure, Failure})}, + route = Route, + cash_flow = undefined + } = hg_client_invoicing:get_payment(InvoiceID, PaymentID, Client), + ok. + +-spec payment_adjustment_to_provider_guarantee_account(config()) -> test_return(). +payment_adjustment_to_provider_guarantee_account(C) -> + Amount = 42000, + Client = cfg(client, C), + InvoiceID = hg_invoice_helper:start_invoice( + <<"provider cashflow adjustment">>, hg_invoice_helper:make_due_date(10), Amount, C + ), + PaymentParams = hg_invoice_helper:make_payment_params(?pmt_sys(<<"visa-ref">>)), + PaymentID = hg_invoice_helper:execute_payment(InvoiceID, PaymentParams, Client), + #payproc_InvoicePayment{route = Route, cash_flow = CashFlow} = + hg_client_invoicing:get_payment(InvoiceID, PaymentID, Client), + [_] = lookup_posting(CashFlow, {provider, settlement}, {merchant, settlement}), + assert_route(Route), + + #domain_ProviderAccount{guarantee = GuaranteeAccountID} = provider_rub_account(), + #{own_amount := GuaranteeBalanceBefore} = hg_accounting:get_balance(GuaranteeAccountID), + ok = configure_provider_guarantee_cashflow(), + Params = #payproc_InvoicePaymentAdjustmentParams{ + reason = <<"switch to provider guarantee">>, + scenario = + {cash_flow, #domain_InvoicePaymentAdjustmentCashFlow{ + domain_revision = hg_domain:head() + }} + }, + ?adjustment(AdjustmentID, ?adjustment_pending()) = + Adjustment = + hg_client_invoicing:create_payment_adjustment(InvoiceID, PaymentID, Params, Client), + ?payment_ev(PaymentID, ?adjustment_ev(AdjustmentID, ?adjustment_created(Adjustment))) = + hg_invoice_helper:next_change(InvoiceID, Client), + [ + ?payment_ev(PaymentID, ?adjustment_ev(AdjustmentID, ?adjustment_status_changed(?adjustment_processed()))), + ?payment_ev(PaymentID, ?adjustment_ev(AdjustmentID, ?adjustment_status_changed(?adjustment_captured(_)))) + ] = hg_invoice_helper:next_changes(InvoiceID, 2, Client), + + #domain_InvoicePaymentAdjustment{new_cash_flow = NewCashFlow} = + hg_client_invoicing:get_payment_adjustment(InvoiceID, PaymentID, AdjustmentID, Client), + [ + #domain_FinalCashFlowPosting{ + source = #domain_FinalCashFlowAccount{account_id = GuaranteeAccountID} + } + ] = lookup_posting(NewCashFlow, {provider, guarantee}, {merchant, settlement}), + #{own_amount := GuaranteeBalance} = hg_accounting:get_balance(GuaranteeAccountID), + ?assertEqual(GuaranteeBalanceBefore - Amount, GuaranteeBalance), + ok. + +%% Internals + +execute_payment(Amount, C) -> + Client = cfg(client, C), + InvoiceID = hg_invoice_helper:start_invoice( + <<"provider cashflow">>, hg_invoice_helper:make_due_date(10), Amount, C + ), + PaymentParams = hg_invoice_helper:make_payment_params(?pmt_sys(<<"visa-ref">>)), + PaymentID = hg_invoice_helper:execute_payment(InvoiceID, PaymentParams, Client), + #payproc_InvoicePayment{route = Route, cash_flow = CashFlow} = + hg_client_invoicing:get_payment(InvoiceID, PaymentID, Client), + {CashFlow, Route}. + +provider_rub_account() -> + #domain_Provider{accounts = ProviderAccounts} = hg_domain:get({provider, ?prv(1)}), + maps:get(?cur(<<"RUB">>), ProviderAccounts). + +unset_provider_guarantee_account() -> + Currency = ?cur(<<"RUB">>), + Provider0 = #domain_Provider{accounts = Accounts0} = hg_domain:get({provider, ?prv(1)}), + ProviderAccount0 = maps:get(Currency, Accounts0), + ProviderAccount1 = ProviderAccount0#domain_ProviderAccount{guarantee = undefined}, + Provider1 = Provider0#domain_Provider{accounts = Accounts0#{Currency => ProviderAccount1}}, + _ = hg_domain:upsert({provider, #domain_ProviderObject{ref = ?prv(1), data = Provider1}}), + ok. + +configure_provider_guarantee_cashflow() -> + Terminal0 = #domain_Terminal{terms = Terms0} = hg_domain:get({terminal, ?trm(1)}), + PaymentTerms0 = Terms0#domain_ProvisionTermSet.payments, + CashFlow = [ + ?cfpost( + {provider, guarantee}, + {merchant, settlement}, + ?share(1, 1, operation_amount) + ), + ?cfpost( + {system, settlement}, + {provider, settlement}, + ?fixed(10, <<"RUB">>) + ) + ], + PaymentTerms1 = PaymentTerms0#domain_PaymentsProvisionTerms{cash_flow = {value, CashFlow}}, + Terminal1 = Terminal0#domain_Terminal{ + terms = Terms0#domain_ProvisionTermSet{payments = PaymentTerms1} + }, + _ = hg_domain:upsert({terminal, #domain_TerminalObject{ref = ?trm(1), data = Terminal1}}), + ok. + +lookup_posting(CashFlow, Source, Destination) -> + lists:filter( + fun( + #domain_FinalCashFlowPosting{ + source = #domain_FinalCashFlowAccount{account_type = SourceAccount}, + destination = #domain_FinalCashFlowAccount{account_type = DestinationAccount} + } + ) -> + Source =:= SourceAccount andalso Destination =:= DestinationAccount + end, + CashFlow + ). + +assert_route(#domain_PaymentRoute{provider = ?prv(1), terminal = ?trm(1)}) -> + ok. + +cfg(Key, Config) -> + hg_ct_helper:cfg(Key, Config). diff --git a/compose.yaml b/compose.yaml index 6aeaf1fc..e2ce9635 100644 --- a/compose.yaml +++ b/compose.yaml @@ -33,7 +33,7 @@ services: command: /sbin/init dmt: - image: ghcr.io/valitydev/dominant-v2:sha-44843aa + image: ghcr.io/valitydev/dominant-v2:sha-93f6e4a command: /opt/dmt/bin/dmt foreground environment: DMT_KAFKA_ENABLED: "0" @@ -115,7 +115,7 @@ services: retries: 20 party-management: - image: ghcr.io/valitydev/party-management:sha-89e6247 + image: ghcr.io/valitydev/party-management:sha-2690a46 command: /opt/party-management/bin/party-management foreground volumes: - ./test/party-management/sys.config:/opt/party-management/releases/0.1/sys.config diff --git a/rebar.config b/rebar.config index 89c948c4..02bdf063 100644 --- a/rebar.config +++ b/rebar.config @@ -36,7 +36,7 @@ {woody, {git, "https://github.com/valitydev/woody_erlang.git", {tag, "v1.1.2"}}}, {scoper, {git, "https://github.com/valitydev/scoper.git", {tag, "v1.1.0"}}}, {thrift, {git, "https://github.com/valitydev/thrift_erlang.git", {tag, "v1.0.0"}}}, - {damsel, {git, "https://github.com/valitydev/damsel.git", {tag, "v2.2.46"}}}, + {damsel, {git, "https://github.com/valitydev/damsel.git", {tag, "v2.2.47"}}}, {exrates_proto, {git, "https://github.com/valitydev/exrates-proto.git", {branch, "master"}}}, {mg_proto, {git, "https://github.com/valitydev/machinegun-proto.git", {branch, "master"}}}, {dmt_client, {git, "https://github.com/valitydev/dmt-client.git", {tag, "v2.0.3"}}}, diff --git a/rebar.lock b/rebar.lock index 564478d6..4e351c94 100644 --- a/rebar.lock +++ b/rebar.lock @@ -31,7 +31,7 @@ {<<"ctx">>,{pkg,<<"ctx">>,<<"0.6.0">>},2}, {<<"damsel">>, {git,"https://github.com/valitydev/damsel.git", - {ref,"77d93d016468742051687953ddf65aa02d3bf91e"}}, + {ref,"8d6174bddedc6d9aefa407fdc1d54877b8686ff9"}}, 0}, {<<"dmt_client">>, {git,"https://github.com/valitydev/dmt-client.git", @@ -69,7 +69,7 @@ 0}, {<<"fistful_proto">>, {git,"https://github.com/valitydev/fistful-proto.git", - {ref,"94b75cef84a6e798887a7e20a66674ce24f3111e"}}, + {ref,"50ccb60bfb17ff67c2c89501ac4d052565acc359"}}, 0}, {<<"genlib">>, {git,"https://github.com/valitydev/genlib.git",