From 47be50ec45c8bb6f335ecb42e61d39f8b6e436f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Thu, 15 Jan 2026 17:32:04 +0300 Subject: [PATCH 1/5] Enhances shop decoding logic to include cash limits based on party context; adds meck dependency for testing --- .cursorignore | 1 + apps/capi/src/capi_cash_limits.erl | 288 +++++++++++++++ apps/capi/src/capi_handler_shops.erl | 20 +- apps/capi/test/capi_cash_limits_tests.erl | 291 +++++++++++++++ capi_domain.34746.coverdata | Bin 0 -> 3010 bytes capi_domain.97223.coverdata | Bin 0 -> 3010 bytes capi_domain.97547.coverdata | Bin 0 -> 3010 bytes capi_party.34746.coverdata | Bin 0 -> 839 bytes capi_party.97223.coverdata | Bin 0 -> 839 bytes capi_party.97547.coverdata | Bin 0 -> 839 bytes mock data/provider_8 | 337 ++++++++++++++++++ mock data/provider_9 | 271 ++++++++++++++ mock data/routing_rules_1059 | 43 +++ ...ig_bbe49f63-0ff8-4cc4-99e8-00892a683cec(1) | 43 +++ mock data/terminal_15 | 141 ++++++++ mock data/terminal_16 | 141 ++++++++ rebar.config | 3 + 17 files changed, 1575 insertions(+), 4 deletions(-) create mode 100644 .cursorignore create mode 100644 apps/capi/src/capi_cash_limits.erl create mode 100644 apps/capi/test/capi_cash_limits_tests.erl create mode 100644 capi_domain.34746.coverdata create mode 100644 capi_domain.97223.coverdata create mode 100644 capi_domain.97547.coverdata create mode 100644 capi_party.34746.coverdata create mode 100644 capi_party.97223.coverdata create mode 100644 capi_party.97547.coverdata create mode 100644 mock data/provider_8 create mode 100644 mock data/provider_9 create mode 100644 mock data/routing_rules_1059 create mode 100644 mock data/shop_config_bbe49f63-0ff8-4cc4-99e8-00892a683cec(1) create mode 100644 mock data/terminal_15 create mode 100644 mock data/terminal_16 diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000..8dc1bb0 --- /dev/null +++ b/.cursorignore @@ -0,0 +1 @@ +!_build/** \ No newline at end of file diff --git a/apps/capi/src/capi_cash_limits.erl b/apps/capi/src/capi_cash_limits.erl new file mode 100644 index 0000000..f1cbd8f --- /dev/null +++ b/apps/capi/src/capi_cash_limits.erl @@ -0,0 +1,288 @@ +-module(capi_cash_limits). + +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). +-export([get_shop_limits/3]). + +-type processing_context() :: capi_handler:processing_context(). + +-spec get_shop_limits(binary(), binary(), processing_context()) -> {ok, map()} | {error, not_found}. +get_shop_limits(PartyID, ShopID, Context) -> + logger:debug( + "Cash limits computed from raw DMT selectors without varset for party=~p shop=~p", + [PartyID, ShopID] + ), + case capi_party:get_shop(PartyID, ShopID, Context) of + {error, not_found} -> + {error, not_found}; + {ok, Shop} -> + Currency = Shop#domain_ShopConfig.account#domain_ShopAccount.currency, + Revision = capi_domain:head(), + ShopTerms = compute_shop_terms(Shop#domain_ShopConfig.terms, Revision, Context), + ShopPayment = extract_payment_limit(ShopTerms, Currency), + ShopPartialRefund = extract_partial_refund_limit(ShopTerms, Currency), + TerminalRefs = get_payment_terminal_refs(Shop#domain_ShopConfig.payment_institution, Context), + {TermPayment, TermPartialRefund} = aggregate_terminal_limits( + TerminalRefs, + Currency, + Context + ), + Payment = intersect_optional(ShopPayment, TermPayment), + PartialRefund = intersect_optional(ShopPartialRefund, TermPartialRefund), + EffectiveRefund = pick_refund_limit(Payment, PartialRefund), + {ok, encode_limits(Currency, Payment, EffectiveRefund)} + end. + +compute_shop_terms(TermsRef, Revision, Context) -> + case capi_domain:get_ext({term_set_hierarchy, TermsRef}, Revision, Context) of + {ok, #domain_TermSetHierarchy{term_set = TermSet}} -> + TermSet; + _ -> + undefined + end. + +aggregate_terminal_limits(TerminalRefs, Currency, Context) -> + lists:foldl( + fun(TerminalRef, {PaymentAcc, PartialRefundAcc}) -> + {Payment, PartialRefund} = get_terminal_limits(TerminalRef, Currency, Context), + log_terminal_terms(TerminalRef, Payment, PartialRefund), + {intersect_optional(PaymentAcc, Payment), intersect_optional(PartialRefundAcc, PartialRefund)} + end, + {undefined, undefined}, + TerminalRefs + ). + +get_terminal_limits(TerminalRef, Currency, Context) -> + case capi_domain:get({terminal, TerminalRef}, Context) of + {ok, #domain_TerminalObject{data = #domain_Terminal{provider_ref = ProviderRef, terms = TerminalTerms}}} -> + ProviderTerms = get_provider_terms(ProviderRef, Context), + Payment = intersect_optional( + extract_payment_limit(ProviderTerms, Currency), + extract_payment_limit(TerminalTerms, Currency) + ), + PartialRefund = intersect_optional( + extract_partial_refund_limit(ProviderTerms, Currency), + extract_partial_refund_limit(TerminalTerms, Currency) + ), + {Payment, PartialRefund}; + _ -> + {undefined, undefined} + end. + +get_provider_terms(ProviderRef, Context) -> + case capi_domain:get({provider, ProviderRef}, Context) of + {ok, #domain_ProviderObject{data = #domain_Provider{terms = Terms}}} -> + Terms; + _ -> + undefined + end. + +extract_payment_limit(undefined, _Currency) -> + undefined; +extract_payment_limit(#domain_TermSet{payments = undefined}, _Currency) -> + undefined; +extract_payment_limit(#domain_TermSet{payments = Payments}, Currency) -> + Selector = Payments#domain_PaymentsServiceTerms.cash_limit, + range_from_selector(Selector, Currency); +extract_payment_limit(#domain_ProvisionTermSet{payments = undefined}, _Currency) -> + undefined; +extract_payment_limit(#domain_ProvisionTermSet{payments = Payments}, Currency) -> + Selector = Payments#domain_PaymentsProvisionTerms.cash_limit, + range_from_selector(Selector, Currency). + +extract_partial_refund_limit(undefined, _Currency) -> + undefined; +extract_partial_refund_limit(#domain_TermSet{payments = undefined}, _Currency) -> + undefined; +extract_partial_refund_limit(#domain_TermSet{payments = Payments}, Currency) -> + Refunds = Payments#domain_PaymentsServiceTerms.refunds, + partial_refund_limit_from_refunds(Refunds, Currency); +extract_partial_refund_limit(#domain_ProvisionTermSet{payments = undefined}, _Currency) -> + undefined; +extract_partial_refund_limit(#domain_ProvisionTermSet{payments = Payments}, Currency) -> + Refunds = Payments#domain_PaymentsProvisionTerms.refunds, + partial_refund_limit_from_refunds(Refunds, Currency). + +partial_refund_limit_from_refunds(undefined, _Currency) -> + undefined; +partial_refund_limit_from_refunds(#domain_PaymentRefundsServiceTerms{partial_refunds = undefined}, _Currency) -> + undefined; +partial_refund_limit_from_refunds(#domain_PaymentRefundsServiceTerms{partial_refunds = PartialRefunds}, Currency) -> + Selector = PartialRefunds#domain_PartialRefundsServiceTerms.cash_limit, + range_from_selector(Selector, Currency); +partial_refund_limit_from_refunds(#domain_PaymentRefundsProvisionTerms{partial_refunds = undefined}, _Currency) -> + undefined; +partial_refund_limit_from_refunds(#domain_PaymentRefundsProvisionTerms{partial_refunds = PartialRefunds}, Currency) -> + Selector = PartialRefunds#domain_PartialRefundsProvisionTerms.cash_limit, + range_from_selector(Selector, Currency). + +range_from_selector(undefined, _Currency) -> + undefined; +range_from_selector(Selector, Currency) -> + Ranges = ranges_from_selector(Selector, Currency), + intersect_all(Ranges). + +ranges_from_selector({value, #domain_CashRange{} = Range}, Currency) -> + normalize_range(Range, Currency); +ranges_from_selector({decisions, Decisions}, Currency) when is_list(Decisions) -> + lists:flatmap( + fun(#domain_CashLimitDecision{then_ = Then}) -> + ranges_from_selector(Then, Currency) + end, + Decisions + ); +ranges_from_selector(_, _Currency) -> + []. + +normalize_range(#domain_CashRange{lower = Lower, upper = Upper}, #domain_CurrencyRef{symbolic_code = CurrencyCode}) -> + case {extract_bound(Lower), extract_bound(Upper)} of + {{ok, {LowerType, LowerAmount, CurrencyCode}}, {ok, {UpperType, UpperAmount, CurrencyCode}}} -> + [#{ + currency => CurrencyCode, + lower => {LowerType, LowerAmount}, + upper => {UpperType, UpperAmount} + }]; + _ -> + [] + end; +normalize_range(_Range, _Currency) -> + []. + +extract_bound({inclusive, #domain_Cash{amount = Amount, currency = #domain_CurrencyRef{symbolic_code = Code}}}) -> + {ok, {inclusive, Amount, Code}}; +extract_bound({exclusive, #domain_Cash{amount = Amount, currency = #domain_CurrencyRef{symbolic_code = Code}}}) -> + {ok, {exclusive, Amount, Code}}; +extract_bound(_) -> + error. + +intersect_all([]) -> + undefined; +intersect_all([Range | Rest]) -> + lists:foldl(fun intersect_optional/2, Range, Rest). + +intersect_optional(undefined, Range) -> + Range; +intersect_optional(Range, undefined) -> + Range; +intersect_optional(#{currency := Currency} = R1, #{currency := Currency} = R2) -> + intersect_ranges(R1, R2); +intersect_optional(_R1, _R2) -> + undefined. + +intersect_ranges(#{lower := Lower1, upper := Upper1} = R1, #{lower := Lower2, upper := Upper2}) -> + Lower = max_lower(Lower1, Lower2), + Upper = min_upper(Upper1, Upper2), + case valid_range(Lower, Upper) of + true -> + R1#{lower => Lower, upper => Upper}; + false -> + undefined + end. + +max_lower({Type1, Amount1}, {_Type2, Amount2}) when Amount1 > Amount2 -> + {Type1, Amount1}; +max_lower({_Type1, Amount1}, {Type2, Amount2}) when Amount2 > Amount1 -> + {Type2, Amount2}; +max_lower({Type1, Amount}, {Type2, Amount}) -> + case Type1 =:= exclusive orelse Type2 =:= exclusive of + true -> {exclusive, Amount}; + false -> {inclusive, Amount} + end. + +min_upper({Type1, Amount1}, {_Type2, Amount2}) when Amount1 < Amount2 -> + {Type1, Amount1}; +min_upper({_Type1, Amount1}, {Type2, Amount2}) when Amount2 < Amount1 -> + {Type2, Amount2}; +min_upper({Type1, Amount}, {Type2, Amount}) -> + case Type1 =:= exclusive orelse Type2 =:= exclusive of + true -> {exclusive, Amount}; + false -> {inclusive, Amount} + end. + +valid_range({_, LowerAmount}, {_, UpperAmount}) when LowerAmount < UpperAmount -> + true; +valid_range({inclusive, Amount}, {inclusive, Amount}) -> + true; +valid_range(_, _) -> + false. + +pick_refund_limit(undefined, undefined) -> + undefined; +pick_refund_limit(Payment, undefined) -> + Payment; +pick_refund_limit(undefined, PartialRefund) -> + PartialRefund; +pick_refund_limit(Payment, PartialRefund) -> + intersect_optional(Payment, PartialRefund). + +encode_limits(Currency, Payment, PartialRefund) -> + CurrencyCode = capi_handler_decoder_utils:decode_currency(Currency), + genlib_map:compact(#{ + <<"payment">> => encode_range(CurrencyCode, Payment), + <<"partialRefund">> => encode_range(CurrencyCode, PartialRefund) + }). + +encode_range(_CurrencyCode, undefined) -> + undefined; +encode_range(CurrencyCode, #{lower := Lower, upper := Upper}) -> + #{ + <<"currency">> => CurrencyCode, + <<"lowerBound">> => encode_bound(Lower), + <<"upperBound">> => encode_bound(Upper) + }. + +encode_bound({Type, Amount}) -> + #{ + <<"amount">> => Amount, + <<"inclusive">> => Type =:= inclusive + }. + +get_payment_terminal_refs(PiRef, Context) -> + case capi_domain:get_payment_institution(PiRef, Context) of + {ok, #domain_PaymentInstitution{payment_routing_rules = Rules}} -> + lists:usort(collect_ruleset_terminals(Rules, Context)); + _ -> + [] + end. + +collect_ruleset_terminals(undefined, _Context) -> + []; +collect_ruleset_terminals(#domain_RoutingRules{policies = PoliciesRef}, Context) -> + collect_ruleset_terminals(PoliciesRef, Context, sets:new()); +collect_ruleset_terminals(_, _Context) -> + []. + +collect_ruleset_terminals(#domain_RoutingRulesetRef{} = Ref, Context, Seen) -> + case sets:is_element(Ref, Seen) of + true -> + []; + false -> + Seen1 = sets:add_element(Ref, Seen), + case capi_domain:get({routing_rules, Ref}, Context) of + {ok, #domain_RoutingRulesObject{data = Ruleset}} -> + collect_ruleset_terminals(Ruleset, Context, Seen1); + _ -> + [] + end + end; +collect_ruleset_terminals(#domain_RoutingRuleset{decisions = Decisions}, Context, Seen) -> + collect_terminals_from_decisions(Decisions, Context, Seen); +collect_ruleset_terminals(_, _Context, _Seen) -> + []. + +collect_terminals_from_decisions({candidates, Candidates}, _Context, _Seen) -> + [C#domain_RoutingCandidate.terminal || C <- Candidates]; +collect_terminals_from_decisions({delegates, Delegates}, Context, Seen) -> + lists:flatmap( + fun(#domain_RoutingDelegate{ruleset = Ref}) -> + collect_ruleset_terminals(Ref, Context, Seen) + end, + Delegates + ); +collect_terminals_from_decisions(_, _Context, _Seen) -> + []. + +log_terminal_terms(TerminalRef, Payment, PartialRefund) -> + logger:debug( + "Cash limits for terminal ~p: payment=~p partial_refund=~p", + [TerminalRef, Payment, PartialRefund] + ). diff --git a/apps/capi/src/capi_handler_shops.erl b/apps/capi/src/capi_handler_shops.erl index b2fbea7..02b4ab6 100644 --- a/apps/capi/src/capi_handler_shops.erl +++ b/apps/capi/src/capi_handler_shops.erl @@ -48,7 +48,7 @@ prepare('GetShopByIDForParty' = OperationID, Req, Context) -> Process = fun() -> case capi_party:get_shop(PartyID, ShopID, Context) of {ok, Shop} -> - {ok, {200, #{}, decode_shop(ShopID, Shop)}}; + {ok, {200, #{}, decode_shop(ShopID, Shop, PartyID, Context, true)}}; {error, not_found} -> {ok, general_error(404, <<"Shop not found">>)} end @@ -70,7 +70,7 @@ get_shops_for_party(PartyID, Context) -> {ok, ShopsWithIDs} -> {ok, lists:map( - fun({ShopID, Shop}) -> decode_shop(ShopID, Shop) end, + fun({ShopID, Shop}) -> decode_shop(ShopID, Shop, PartyID, Context, false) end, ShopsWithIDs )}; {error, not_found} -> @@ -87,7 +87,8 @@ restrict_shops(Shops, Restrictions) -> Shops ). -decode_shop(ShopID, Shop) -> +decode_shop(ShopID, Shop, PartyID, Context, IncludeLimits) -> + Limits = maybe_get_cash_limits(IncludeLimits, PartyID, ShopID, Context), genlib_map:compact(#{ <<"id">> => ShopID, <<"isBlocked">> => capi_handler_decoder_party:is_blocked(Shop#domain_ShopConfig.block), @@ -96,8 +97,19 @@ decode_shop(ShopID, Shop) -> <<"categoryID">> => capi_handler_decoder_utils:decode_category_ref(Shop#domain_ShopConfig.category), <<"details">> => capi_handler_decoder_party:decode_shop_details(Shop), <<"location">> => capi_handler_decoder_party:decode_shop_location(Shop#domain_ShopConfig.location), - <<"contractID">> => genlib:to_binary(Shop#domain_ShopConfig.terms#domain_TermSetHierarchyRef.id) + <<"contractID">> => genlib:to_binary(Shop#domain_ShopConfig.terms#domain_TermSetHierarchyRef.id), + <<"cashLimits">> => Limits }). +maybe_get_cash_limits(false, _PartyID, _ShopID, _Context) -> + undefined; +maybe_get_cash_limits(true, PartyID, ShopID, Context) -> + case capi_cash_limits:get_shop_limits(PartyID, ShopID, Context) of + {ok, Limits} when Limits =/= #{} -> + Limits; + _ -> + undefined + end. + get_shop_currency(#domain_ShopAccount{currency = Currency}) -> capi_handler_decoder_utils:decode_currency(Currency). diff --git a/apps/capi/test/capi_cash_limits_tests.erl b/apps/capi/test/capi_cash_limits_tests.erl new file mode 100644 index 0000000..141a1c5 --- /dev/null +++ b/apps/capi/test/capi_cash_limits_tests.erl @@ -0,0 +1,291 @@ +-module(capi_cash_limits_tests). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). + +-export([test/0]). + +-spec test() -> term(). +test() -> + eunit:test(?MODULE). + +-spec shop_only_limits_test_() -> term(). +shop_only_limits_test_() -> + {setup, fun setup_mocks/0, fun teardown_mocks/1, fun shop_only_limits_test/0}. + +-spec terminal_intersection_test_() -> term(). +terminal_intersection_test_() -> + {setup, fun setup_mocks/0, fun teardown_mocks/1, fun terminal_intersection_test/0}. + +-spec real_config_limits_test_() -> term(). +real_config_limits_test_() -> + {setup, fun setup_mocks/0, fun teardown_mocks/1, fun real_config_limits_test/0}. + +-spec setup_mocks() -> ok. +setup_mocks() -> + ok = meck:new(capi_party, [non_strict]), + ok = meck:new(capi_domain, [non_strict]), + ok. + +-spec teardown_mocks(term()) -> ok. +teardown_mocks(_) -> + meck:unload(capi_party), + meck:unload(capi_domain). + +-spec shop_only_limits_test() -> ok. +shop_only_limits_test() -> + Currency = #domain_CurrencyRef{symbolic_code = <<"RUB">>}, + ShopTerms = #domain_TermSet{ + payments = #domain_PaymentsServiceTerms{ + cash_limit = {value, mk_cash_range(100, 1000, Currency)}, + refunds = #domain_PaymentRefundsServiceTerms{ + partial_refunds = #domain_PartialRefundsServiceTerms{ + cash_limit = {value, mk_cash_range(200, 900, Currency)} + } + } + } + }, + Shop = mk_shop(Currency, #domain_TermSetHierarchyRef{id = 10}, #domain_PaymentInstitutionRef{id = 77}), + meck:expect(capi_party, get_shop, fun(_PartyID, _ShopID, _Context) -> {ok, Shop} end), + meck:expect(capi_domain, head, fun() -> 1 end), + meck:expect( + capi_domain, + get_ext, + fun({term_set_hierarchy, #domain_TermSetHierarchyRef{id = 10}}, 1, _Context) -> + {ok, #domain_TermSetHierarchy{term_set = ShopTerms}}; + (_, _, _) -> + {error, not_found} + end + ), + meck:expect( + capi_domain, + get_payment_institution, + fun(#domain_PaymentInstitutionRef{id = 77}, _Context) -> + {ok, #domain_PaymentInstitution{payment_routing_rules = undefined}} + end + ), + {ok, Result} = capi_cash_limits:get_shop_limits(<<"party">>, <<"shop">>, #{}), + Payment = maps:get(<<"payment">>, Result), + PartialRefund = maps:get(<<"partialRefund">>, Result), + ?assertEqual(<<"RUB">>, maps:get(<<"currency">>, Payment)), + ?assertEqual(#{<<"amount">> => 100, <<"inclusive">> => true}, maps:get(<<"lowerBound">>, Payment)), + ?assertEqual(#{<<"amount">> => 1000, <<"inclusive">> => true}, maps:get(<<"upperBound">>, Payment)), + ?assertEqual(<<"RUB">>, maps:get(<<"currency">>, PartialRefund)), + ?assertEqual(#{<<"amount">> => 200, <<"inclusive">> => true}, maps:get(<<"lowerBound">>, PartialRefund)), + ?assertEqual(#{<<"amount">> => 900, <<"inclusive">> => true}, maps:get(<<"upperBound">>, PartialRefund)). + +-spec terminal_intersection_test() -> ok. +terminal_intersection_test() -> + Currency = #domain_CurrencyRef{symbolic_code = <<"RUB">>}, + ShopTerms = #domain_TermSet{ + payments = #domain_PaymentsServiceTerms{ + cash_limit = {value, mk_cash_range(100, 1000, Currency)} + } + }, + Shop = mk_shop(Currency, #domain_TermSetHierarchyRef{id = 20}, #domain_PaymentInstitutionRef{id = 88}), + ProviderRef1 = #domain_ProviderRef{id = 1}, + ProviderRef2 = #domain_ProviderRef{id = 2}, + TerminalRef1 = #domain_TerminalRef{id = 11}, + TerminalRef2 = #domain_TerminalRef{id = 22}, + ProviderTerms1 = mk_provision_terms(50, 500, Currency), + ProviderTerms2 = mk_provision_terms(100, 700, Currency), + TerminalTerms1 = mk_provision_terms(200, 800, Currency), + TerminalTerms2 = mk_provision_terms(300, 900, Currency), + RulesetRef = #domain_RoutingRulesetRef{id = 999}, + Ruleset = #domain_RoutingRuleset{ + decisions = {candidates, [ + #domain_RoutingCandidate{allowed = {constant, true}, terminal = TerminalRef1}, + #domain_RoutingCandidate{allowed = {constant, true}, terminal = TerminalRef2} + ]} + }, + meck:expect(capi_party, get_shop, fun(_PartyID, _ShopID, _Context) -> {ok, Shop} end), + meck:expect(capi_domain, head, fun() -> 1 end), + meck:expect( + capi_domain, + get_ext, + fun({term_set_hierarchy, #domain_TermSetHierarchyRef{id = 20}}, 1, _Context) -> + {ok, #domain_TermSetHierarchy{term_set = ShopTerms}}; + (_, _, _) -> + {error, not_found} + end + ), + meck:expect( + capi_domain, + get_payment_institution, + fun(#domain_PaymentInstitutionRef{id = 88}, _Context) -> + {ok, #domain_PaymentInstitution{ + payment_routing_rules = #domain_RoutingRules{policies = RulesetRef} + }} + end + ), + meck:expect( + capi_domain, + get, + fun + ({routing_rules, #domain_RoutingRulesetRef{id = 999}}, _Context) -> + {ok, #domain_RoutingRulesObject{data = Ruleset}}; + ({terminal, #domain_TerminalRef{id = 11}}, _Context) -> + {ok, #domain_TerminalObject{data = #domain_Terminal{provider_ref = ProviderRef1, terms = TerminalTerms1}}}; + ({terminal, #domain_TerminalRef{id = 22}}, _Context) -> + {ok, #domain_TerminalObject{data = #domain_Terminal{provider_ref = ProviderRef2, terms = TerminalTerms2}}}; + ({provider, #domain_ProviderRef{id = 1}}, _Context) -> + {ok, #domain_ProviderObject{data = #domain_Provider{terms = ProviderTerms1}}}; + ({provider, #domain_ProviderRef{id = 2}}, _Context) -> + {ok, #domain_ProviderObject{data = #domain_Provider{terms = ProviderTerms2}}}; + (_, _Context) -> + {error, not_found} + end + ), + {ok, Result} = capi_cash_limits:get_shop_limits(<<"party">>, <<"shop">>, #{}), + Payment = maps:get(<<"payment">>, Result), + PartialRefund = maps:get(<<"partialRefund">>, Result), + ?assertEqual(Payment, PartialRefund), + ?assertEqual(<<"RUB">>, maps:get(<<"currency">>, Payment)), + ?assertEqual(#{<<"amount">> => 300, <<"inclusive">> => true}, maps:get(<<"lowerBound">>, Payment)), + ?assertEqual(#{<<"amount">> => 500, <<"inclusive">> => true}, maps:get(<<"upperBound">>, Payment)). + +-spec real_config_limits_test() -> ok. +real_config_limits_test() -> + Currency = #domain_CurrencyRef{symbolic_code = <<"KZT">>}, + Shop = mk_shop(Currency, #domain_TermSetHierarchyRef{id = 1000}, #domain_PaymentInstitutionRef{id = 100}), + RulesetRef = #domain_RoutingRulesetRef{id = 1059}, + Ruleset = #domain_RoutingRuleset{ + decisions = {candidates, [ + #domain_RoutingCandidate{allowed = {constant, true}, terminal = #domain_TerminalRef{id = 15}}, + #domain_RoutingCandidate{allowed = {constant, true}, terminal = #domain_TerminalRef{id = 16}} + ]} + }, + ProviderTerms8 = #domain_ProvisionTermSet{ + payments = #domain_PaymentsProvisionTerms{ + cash_limit = mk_cash_limit_decisions([{100, 1000000000, Currency}]), + refunds = #domain_PaymentRefundsProvisionTerms{ + partial_refunds = #domain_PartialRefundsProvisionTerms{ + cash_limit = mk_cash_limit_decisions([{100, 1000000000, Currency}]) + } + } + } + }, + ProviderTerms9 = #domain_ProvisionTermSet{ + payments = #domain_PaymentsProvisionTerms{ + cash_limit = mk_cash_limit_value(100, 1000000000, Currency), + refunds = #domain_PaymentRefundsProvisionTerms{ + partial_refunds = #domain_PartialRefundsProvisionTerms{ + cash_limit = mk_cash_limit_value(100, 100000000, Currency) + } + } + } + }, + TerminalTerms15 = #domain_ProvisionTermSet{ + payments = #domain_PaymentsProvisionTerms{ + cash_limit = mk_cash_limit_value(10000, 120000000, Currency) + } + }, + TerminalTerms16 = #domain_ProvisionTermSet{ + payments = #domain_PaymentsProvisionTerms{ + cash_limit = mk_cash_limit_decisions([ + {51300, 43609100, Currency}, + {51300, 128262000, Currency} + ]) + } + }, + meck:expect(capi_party, get_shop, fun(_PartyID, _ShopID, _Context) -> {ok, Shop} end), + meck:expect(capi_domain, head, fun() -> 1 end), + meck:expect( + capi_domain, + get_ext, + fun({term_set_hierarchy, #domain_TermSetHierarchyRef{id = 1000}}, 1, _Context) -> + {error, not_found}; + (_, _, _) -> + {error, not_found} + end + ), + meck:expect( + capi_domain, + get_payment_institution, + fun(#domain_PaymentInstitutionRef{id = 100}, _Context) -> + {ok, #domain_PaymentInstitution{ + payment_routing_rules = #domain_RoutingRules{policies = RulesetRef} + }} + end + ), + meck:expect( + capi_domain, + get, + fun + ({routing_rules, #domain_RoutingRulesetRef{id = 1059}}, _Context) -> + {ok, #domain_RoutingRulesObject{data = Ruleset}}; + ({terminal, #domain_TerminalRef{id = 15}}, _Context) -> + {ok, #domain_TerminalObject{ + data = #domain_Terminal{provider_ref = #domain_ProviderRef{id = 8}, terms = TerminalTerms15} + }}; + ({terminal, #domain_TerminalRef{id = 16}}, _Context) -> + {ok, #domain_TerminalObject{ + data = #domain_Terminal{provider_ref = #domain_ProviderRef{id = 9}, terms = TerminalTerms16} + }}; + ({provider, #domain_ProviderRef{id = 8}}, _Context) -> + {ok, #domain_ProviderObject{data = #domain_Provider{terms = ProviderTerms8}}}; + ({provider, #domain_ProviderRef{id = 9}}, _Context) -> + {ok, #domain_ProviderObject{data = #domain_Provider{terms = ProviderTerms9}}}; + (_, _Context) -> + {error, not_found} + end + ), + {ok, Result} = capi_cash_limits:get_shop_limits(<<"party">>, <<"shop">>, #{}), + io:format("real_config_limits_result=~p~n", [Result]), + Payment = maps:get(<<"payment">>, Result), + PartialRefund = maps:get(<<"partialRefund">>, Result), + ?assertEqual(<<"KZT">>, maps:get(<<"currency">>, Payment)), + ?assertEqual(#{<<"amount">> => 51300, <<"inclusive">> => true}, maps:get(<<"lowerBound">>, Payment)), + ?assertEqual(#{<<"amount">> => 43609100, <<"inclusive">> => true}, maps:get(<<"upperBound">>, Payment)), + ?assertEqual(Payment, PartialRefund). + +-spec mk_shop( + dmsl_domain_thrift:'CurrencyRef'(), + dmsl_domain_thrift:'TermSetHierarchyRef'(), + dmsl_domain_thrift:'PaymentInstitutionRef'() +) -> dmsl_domain_thrift:'ShopConfig'(). +mk_shop(Currency, TermsRef, PiRef) -> + #domain_ShopConfig{ + account = #domain_ShopAccount{currency = Currency}, + terms = TermsRef, + payment_institution = PiRef + }. + +-spec mk_provision_terms( + integer(), + integer(), + dmsl_domain_thrift:'CurrencyRef'() +) -> dmsl_domain_thrift:'ProvisionTermSet'(). +mk_provision_terms(Lower, Upper, Currency) -> + #domain_ProvisionTermSet{ + payments = #domain_PaymentsProvisionTerms{ + cash_limit = {value, mk_cash_range(Lower, Upper, Currency)} + } + }. + +-spec mk_cash_range( + integer(), + integer(), + dmsl_domain_thrift:'CurrencyRef'() +) -> dmsl_domain_thrift:'CashRange'(). +mk_cash_range(Lower, Upper, Currency) -> + #domain_CashRange{ + lower = {inclusive, #domain_Cash{amount = Lower, currency = Currency}}, + upper = {inclusive, #domain_Cash{amount = Upper, currency = Currency}} + }. + +-spec mk_cash_limit_value(integer(), integer(), dmsl_domain_thrift:'CurrencyRef'()) -> + dmsl_domain_thrift:'CashLimitSelector'(). +mk_cash_limit_value(Lower, Upper, Currency) -> + {value, mk_cash_range(Lower, Upper, Currency)}. + +-spec mk_cash_limit_decisions([{integer(), integer(), dmsl_domain_thrift:'CurrencyRef'()}]) -> + dmsl_domain_thrift:'CashLimitSelector'(). +mk_cash_limit_decisions(Ranges) -> + {decisions, [ + #domain_CashLimitDecision{ + if_ = {constant, true}, + then_ = mk_cash_limit_value(Lower, Upper, Currency) + } + || {Lower, Upper, Currency} <- Ranges + ]}. diff --git a/capi_domain.34746.coverdata b/capi_domain.34746.coverdata new file mode 100644 index 0000000000000000000000000000000000000000..7473a38c0644bee5c0b4768e1f202227135e7dba GIT binary patch literal 3010 zcmbVOOK8+U7@mFDqE#siI=4~F*D@)&3ym&$e-wyA%u`MdT;Q< z;Ic}7^?v9r4PQEHubA7jkZ+yu-}p1gjUE40=)1q+$8_o2Oy{-VPd`^%s_*(&zFu2> zUw{F1c3~plSUWViY3t<0@!XfbdH3AN`F&U4%r@8N*3;|Xw-2u~rqHLtmakova=P5$ zXBE$|{U#M`4Hqo~G}CV`5sH0{ma#*0pri;HNWld_F-JVpcfiJ{h~sLO86HW6&qqL= zT$bfzlsceNOaq%l@P~jpxbP-os#cL;VtPh2&jUc+T=Xg?!BS2TmAY67{&J*4mX{(U zHNdl2(f{=6tdNSEfO1?FnUEmTiev9&W2lyAQYQeW@vw--CGaHn@_O6Z{0_x5^UL&RWuE64!+C$AWza4^+cs&^4ymTU0My16*~mnOpNwc9j)?d6N}@%3O#I?ABArdc zBl$cAjPQb2d3 lZ89NG(j8Ay_l}zKBzqH+82>eu{j(fNyvtKilOMYa&>y`ZC;k8c literal 0 HcmV?d00001 diff --git a/capi_domain.97223.coverdata b/capi_domain.97223.coverdata new file mode 100644 index 0000000000000000000000000000000000000000..211e1322f4328f3a99c51d44e471749edfbb3a32 GIT binary patch literal 3010 zcmd044q#wl$gh~=d&ZZm*+9fK(J?)?JFbBDuG32cm1kb^KPy!`(|!lJ&z1XeF8$4A zy@`J}-dr!S{-hV*Vd?d!f)c%xZ@t`VztvYGP%1LZUkw()@#mS%3v#JNh-}PDCbU2EXa&c$a(+r`d}>~4E?D)fL|r;&PZf1#c8H6$i4YVS*gh-#qmj%kf;I$ z_+HWjya9g@=Hw?O<`gF~01e-o$Y6!jNfIC@VMzzYK#h!vR%H3+Ao&)0lN8vqiHg7^ z@||N$zE(T3f^HA#389fJi*^zn(4h1=Igx?*l7}U69VyD$5;qa0JSDLt5u7MX$&VBx z{Fw%++{q=%P0+HIHE}~CgEr1Q#+jR#of@BCPy)(SOo_+IcfT{i1P_X>Xrkj5RC3S6 zs~Ww84I|p4jEP6d_oyS@d{CNKUX)l6UzS=_46L$%H4vz*I!j87F(|#{qon*HgbV;|YDUm@9XU1X4NKH%u*I_E8o7_i!1bdK@ xl$a7%k#CbJ>Hg{=DZd7i?>1|q6Ko+-Nih+x!_gaO^YN-iFP4JHPP5vH3;+NOC;k8c literal 0 HcmV?d00001 diff --git a/capi_domain.97547.coverdata b/capi_domain.97547.coverdata new file mode 100644 index 0000000000000000000000000000000000000000..c6c24c66a7b12ae5ec3fa7b0a017576e34291ccb GIT binary patch literal 3010 zcmbVOO-L0{6u$2#rK}JP68c-@i$s){A(cU3KNyN&FK81so;P!QZ)*OWxzjki>Jeo{ zQ9<-e+lcU0Ad=J~;uS@?X;XsGN|4&LD6?9qDz z9|l%b@@o!2Pig4VF=y56-uZmXT;HZ&VQ%dB??UhWjX$SL-#>L+`}6cmwYmDPZ}sbS z)%OMHS7+uY^7XaDlbg3qUL4PT?Va<^jhx?q_03FUZFU2>{$t0`dUFbWE^PhQUMZ)` zbwRf1n@-T6f}<;%ZGvtE4JAwz)%GpohLDC76aeLz=nO?ehN1vaHxtzy^>iRKu|*W! z@`z4+qT7~73scYosDoL$5|0uWRHBg3!4#Z^M_ha`piXAf#)wa#ld|Gs0|?gQ6~@F% zuE=k_50HQfhs3#ENF0DI)z+|rE#Hh?zQK)-+`bKH7gOD}47cMsZHbHcR^Wo8oWib0 z<3W9i^6?Q+8*|ul6Ha2qcF01OY9d)$22T>%{5K_Mg@n4p%{>ioxq>|msFkTkeaWzk zV0eJ2ZWCPnih#B-`(?`*w76Ldpe`n?DN@QVp1>;clw!E>twy)wg;dom`LAlbkUO{J z2X)Dp9$eCwNV=o)13D8$h`d^S-v^~Unv$p2Eky5#{Gfi3V~~NzBJetR#s$}a0Ch73 z{*taohgFe2tgVTvEFirs z?wk({{wd!tq7kXpoWYdAR?d=Cnp;rLm7G|R8DEfCR8m>anVwn_U!0L&P@Kq=$e3uF z$e@W!DMxZfYI1gdX-Oh8P^ALVD&-Oxh_&9BXhUW2sf76n>`)D&RjLzmC^*Q#;Uq$| zp=v~{)J$YB#ufHLATNQfk1t9s%Ph{!&r4(g#)1SPCnKU(pLnHuM7vp)ctb6TS87hY IQo}?B01j6sJ^%m! literal 0 HcmV?d00001 diff --git a/capi_party.97223.coverdata b/capi_party.97223.coverdata new file mode 100644 index 0000000000000000000000000000000000000000..add60e685248ae8af70d038b0cfb911a294c6d73 GIT binary patch literal 839 zcmd054q#wl$gP;;+T+dDY#_nLy8Bh^7v}R1L~pHC`6yifX_Adp)$cHKz4U~%_qUUU zkAE|HHQ{N%%9`Bo$Fz-8|8&+pmJ)96ow0c3yoD)SX6T$t`r#8%BD#H%fw;xR>Firs z?wk({{wd!tq7kXpoWYdAR?d=Cnp;rLm7G|R8DEfCR8m>anVwn_U!0L&P@Kq=$e5^? z$e@W!DMxZfYI1gdX-Oh8P^Cg5gE20ZLLfuI=EoPMmSq-a=I13c0QE>DG7#$!ed3iG z6R*?|pZ8!P0rs9cAw$951BaOiA*G1mH6>oDCDBgRAX=p=@z$FYZKxVPl_*I@4xdVt O(AUJL62(lJLFirs z?wk({{wd!tq7kXtoWYdAR?d=Cnp;rLm7G|R8DEfCR8m>ak(`m5oSk1&IHN=`TKL Date: Thu, 15 Jan 2026 17:57:36 +0300 Subject: [PATCH 2/5] Refactors cash limit aggregation logic to use union instead of intersection; introduces new union and normalization functions for range handling. Updates tests to reflect new expected values for payment limits. --- apps/capi/src/capi_cash_limits.erl | 53 +++++++++++++++++++++-- apps/capi/test/capi_cash_limits_tests.erl | 8 ++-- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/apps/capi/src/capi_cash_limits.erl b/apps/capi/src/capi_cash_limits.erl index f1cbd8f..bb20247 100644 --- a/apps/capi/src/capi_cash_limits.erl +++ b/apps/capi/src/capi_cash_limits.erl @@ -41,15 +41,16 @@ compute_shop_terms(TermsRef, Revision, Context) -> end. aggregate_terminal_limits(TerminalRefs, Currency, Context) -> - lists:foldl( + {PaymentAcc, PartialRefundAcc} = lists:foldl( fun(TerminalRef, {PaymentAcc, PartialRefundAcc}) -> {Payment, PartialRefund} = get_terminal_limits(TerminalRef, Currency, Context), log_terminal_terms(TerminalRef, Payment, PartialRefund), - {intersect_optional(PaymentAcc, Payment), intersect_optional(PartialRefundAcc, PartialRefund)} + {union_optional(PaymentAcc, Payment), union_optional(PartialRefundAcc, PartialRefund)} end, - {undefined, undefined}, + {init, init}, TerminalRefs - ). + ), + {normalize_union(PaymentAcc), normalize_union(PartialRefundAcc)}. get_terminal_limits(TerminalRef, Currency, Context) -> case capi_domain:get({terminal, TerminalRef}, Context) of @@ -168,6 +169,19 @@ intersect_optional(#{currency := Currency} = R1, #{currency := Currency} = R2) - intersect_optional(_R1, _R2) -> undefined. +union_optional(undefined, _Range) -> + undefined; +union_optional(_Range, undefined) -> + undefined; +union_optional(init, Range) -> + Range; +union_optional(Range, init) -> + Range; +union_optional(#{currency := Currency} = R1, #{currency := Currency} = R2) -> + union_ranges(R1, R2); +union_optional(_R1, _R2) -> + undefined. + intersect_ranges(#{lower := Lower1, upper := Upper1} = R1, #{lower := Lower2, upper := Upper2}) -> Lower = max_lower(Lower1, Lower2), Upper = min_upper(Upper1, Upper2), @@ -178,6 +192,12 @@ intersect_ranges(#{lower := Lower1, upper := Upper1} = R1, #{lower := Lower2, up undefined end. +union_ranges(#{lower := Lower1, upper := Upper1} = R1, #{lower := Lower2, upper := Upper2}) -> + Lower = min_lower(Lower1, Lower2), + Upper = max_upper(Upper1, Upper2), + R1#{lower => Lower, upper => Upper}. + + max_lower({Type1, Amount1}, {_Type2, Amount2}) when Amount1 > Amount2 -> {Type1, Amount1}; max_lower({_Type1, Amount1}, {Type2, Amount2}) when Amount2 > Amount1 -> @@ -188,6 +208,16 @@ max_lower({Type1, Amount}, {Type2, Amount}) -> false -> {inclusive, Amount} end. +min_lower({Type1, Amount1}, {_Type2, Amount2}) when Amount1 < Amount2 -> + {Type1, Amount1}; +min_lower({_Type1, Amount1}, {Type2, Amount2}) when Amount2 < Amount1 -> + {Type2, Amount2}; +min_lower({Type1, Amount}, {Type2, Amount}) -> + case Type1 =:= inclusive orelse Type2 =:= inclusive of + true -> {inclusive, Amount}; + false -> {exclusive, Amount} + end. + min_upper({Type1, Amount1}, {_Type2, Amount2}) when Amount1 < Amount2 -> {Type1, Amount1}; min_upper({_Type1, Amount1}, {Type2, Amount2}) when Amount2 < Amount1 -> @@ -198,6 +228,16 @@ min_upper({Type1, Amount}, {Type2, Amount}) -> false -> {inclusive, Amount} end. +max_upper({Type1, Amount1}, {_Type2, Amount2}) when Amount1 > Amount2 -> + {Type1, Amount1}; +max_upper({_Type1, Amount1}, {Type2, Amount2}) when Amount2 > Amount1 -> + {Type2, Amount2}; +max_upper({Type1, Amount}, {Type2, Amount}) -> + case Type1 =:= inclusive orelse Type2 =:= inclusive of + true -> {inclusive, Amount}; + false -> {exclusive, Amount} + end. + valid_range({_, LowerAmount}, {_, UpperAmount}) when LowerAmount < UpperAmount -> true; valid_range({inclusive, Amount}, {inclusive, Amount}) -> @@ -205,6 +245,11 @@ valid_range({inclusive, Amount}, {inclusive, Amount}) -> valid_range(_, _) -> false. +normalize_union(init) -> + undefined; +normalize_union(Value) -> + Value. + pick_refund_limit(undefined, undefined) -> undefined; pick_refund_limit(Payment, undefined) -> diff --git a/apps/capi/test/capi_cash_limits_tests.erl b/apps/capi/test/capi_cash_limits_tests.erl index 141a1c5..c8c13fa 100644 --- a/apps/capi/test/capi_cash_limits_tests.erl +++ b/apps/capi/test/capi_cash_limits_tests.erl @@ -141,8 +141,8 @@ terminal_intersection_test() -> PartialRefund = maps:get(<<"partialRefund">>, Result), ?assertEqual(Payment, PartialRefund), ?assertEqual(<<"RUB">>, maps:get(<<"currency">>, Payment)), - ?assertEqual(#{<<"amount">> => 300, <<"inclusive">> => true}, maps:get(<<"lowerBound">>, Payment)), - ?assertEqual(#{<<"amount">> => 500, <<"inclusive">> => true}, maps:get(<<"upperBound">>, Payment)). + ?assertEqual(#{<<"amount">> => 200, <<"inclusive">> => true}, maps:get(<<"lowerBound">>, Payment)), + ?assertEqual(#{<<"amount">> => 700, <<"inclusive">> => true}, maps:get(<<"upperBound">>, Payment)). -spec real_config_limits_test() -> ok. real_config_limits_test() -> @@ -235,8 +235,8 @@ real_config_limits_test() -> Payment = maps:get(<<"payment">>, Result), PartialRefund = maps:get(<<"partialRefund">>, Result), ?assertEqual(<<"KZT">>, maps:get(<<"currency">>, Payment)), - ?assertEqual(#{<<"amount">> => 51300, <<"inclusive">> => true}, maps:get(<<"lowerBound">>, Payment)), - ?assertEqual(#{<<"amount">> => 43609100, <<"inclusive">> => true}, maps:get(<<"upperBound">>, Payment)), + ?assertEqual(#{<<"amount">> => 10000, <<"inclusive">> => true}, maps:get(<<"lowerBound">>, Payment)), + ?assertEqual(#{<<"amount">> => 120000000, <<"inclusive">> => true}, maps:get(<<"upperBound">>, Payment)), ?assertEqual(Payment, PartialRefund). -spec mk_shop( From ae06c084e4dd36cadf3ef36a415249611d015412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Mon, 19 Jan 2026 18:06:56 +0300 Subject: [PATCH 3/5] wip --- apps/capi/src/capi_cash_limits.erl | 231 ++++++++++++++++++++------- apps/capi/src/capi_handler_shops.erl | 36 +++-- rebar.config | 7 +- rebar.lock | 4 +- 4 files changed, 195 insertions(+), 83 deletions(-) diff --git a/apps/capi/src/capi_cash_limits.erl b/apps/capi/src/capi_cash_limits.erl index bb20247..72a8263 100644 --- a/apps/capi/src/capi_cash_limits.erl +++ b/apps/capi/src/capi_cash_limits.erl @@ -5,7 +5,7 @@ -type processing_context() :: capi_handler:processing_context(). --spec get_shop_limits(binary(), binary(), processing_context()) -> {ok, map()} | {error, not_found}. +-spec get_shop_limits(binary(), binary(), processing_context()) -> {ok, [map()]} | {error, not_found}. get_shop_limits(PartyID, ShopID, Context) -> logger:debug( "Cash limits computed from raw DMT selectors without varset for party=~p shop=~p", @@ -17,22 +17,33 @@ get_shop_limits(PartyID, ShopID, Context) -> {ok, Shop} -> Currency = Shop#domain_ShopConfig.account#domain_ShopAccount.currency, Revision = capi_domain:head(), - ShopTerms = compute_shop_terms(Shop#domain_ShopConfig.terms, Revision, Context), - ShopPayment = extract_payment_limit(ShopTerms, Currency), - ShopPartialRefund = extract_partial_refund_limit(ShopTerms, Currency), + ShopTerms = get_shop_terms(Shop#domain_ShopConfig.terms, Revision, Context), TerminalRefs = get_payment_terminal_refs(Shop#domain_ShopConfig.payment_institution, Context), - {TermPayment, TermPartialRefund} = aggregate_terminal_limits( - TerminalRefs, - Currency, - Context + ShopMethods = extract_payment_methods(ShopTerms), + TerminalMethods = extract_terminal_payment_methods(TerminalRefs, Context), + Methods = intersect_methods(ShopMethods, TerminalMethods), + Limits = lists:foldl( + fun(Method, Acc) -> + ShopPayment = extract_payment_limit(ShopTerms, Currency, Method), + ShopPartialRefund = extract_partial_refund_limit(ShopTerms, Currency, Method), + {TermPayment, TermPartialRefund} = aggregate_terminal_limits( + TerminalRefs, + Currency, + Method, + Context + ), + Payment = intersect_optional(ShopPayment, TermPayment), + PartialRefund = intersect_optional(ShopPartialRefund, TermPartialRefund), + EffectiveLimit = pick_refund_limit(Payment, PartialRefund), + Acc ++ encode_limits(Currency, Method, EffectiveLimit) + end, + [], + Methods ), - Payment = intersect_optional(ShopPayment, TermPayment), - PartialRefund = intersect_optional(ShopPartialRefund, TermPartialRefund), - EffectiveRefund = pick_refund_limit(Payment, PartialRefund), - {ok, encode_limits(Currency, Payment, EffectiveRefund)} + {ok, Limits} end. -compute_shop_terms(TermsRef, Revision, Context) -> +get_shop_terms(TermsRef, Revision, Context) -> case capi_domain:get_ext({term_set_hierarchy, TermsRef}, Revision, Context) of {ok, #domain_TermSetHierarchy{term_set = TermSet}} -> TermSet; @@ -40,10 +51,10 @@ compute_shop_terms(TermsRef, Revision, Context) -> undefined end. -aggregate_terminal_limits(TerminalRefs, Currency, Context) -> +aggregate_terminal_limits(TerminalRefs, Currency, Method, Context) -> {PaymentAcc, PartialRefundAcc} = lists:foldl( fun(TerminalRef, {PaymentAcc, PartialRefundAcc}) -> - {Payment, PartialRefund} = get_terminal_limits(TerminalRef, Currency, Context), + {Payment, PartialRefund} = get_terminal_limits(TerminalRef, Currency, Method, Context), log_terminal_terms(TerminalRef, Payment, PartialRefund), {union_optional(PaymentAcc, Payment), union_optional(PartialRefundAcc, PartialRefund)} end, @@ -52,17 +63,17 @@ aggregate_terminal_limits(TerminalRefs, Currency, Context) -> ), {normalize_union(PaymentAcc), normalize_union(PartialRefundAcc)}. -get_terminal_limits(TerminalRef, Currency, Context) -> +get_terminal_limits(TerminalRef, Currency, Method, Context) -> case capi_domain:get({terminal, TerminalRef}, Context) of {ok, #domain_TerminalObject{data = #domain_Terminal{provider_ref = ProviderRef, terms = TerminalTerms}}} -> ProviderTerms = get_provider_terms(ProviderRef, Context), Payment = intersect_optional( - extract_payment_limit(ProviderTerms, Currency), - extract_payment_limit(TerminalTerms, Currency) + extract_payment_limit(ProviderTerms, Currency, Method), + extract_payment_limit(TerminalTerms, Currency, Method) ), PartialRefund = intersect_optional( - extract_partial_refund_limit(ProviderTerms, Currency), - extract_partial_refund_limit(TerminalTerms, Currency) + extract_partial_refund_limit(ProviderTerms, Currency, Method), + extract_partial_refund_limit(TerminalTerms, Currency, Method) ), {Payment, PartialRefund}; _ -> @@ -77,71 +88,84 @@ get_provider_terms(ProviderRef, Context) -> undefined end. -extract_payment_limit(undefined, _Currency) -> +extract_payment_limit(undefined, _Currency, _Method) -> undefined; -extract_payment_limit(#domain_TermSet{payments = undefined}, _Currency) -> +extract_payment_limit(#domain_TermSet{payments = undefined}, _Currency, _Method) -> undefined; -extract_payment_limit(#domain_TermSet{payments = Payments}, Currency) -> +extract_payment_limit(#domain_TermSet{payments = Payments}, Currency, Method) -> Selector = Payments#domain_PaymentsServiceTerms.cash_limit, - range_from_selector(Selector, Currency); -extract_payment_limit(#domain_ProvisionTermSet{payments = undefined}, _Currency) -> + range_from_selector(Selector, Currency, Method); +extract_payment_limit(#domain_ProvisionTermSet{payments = undefined}, _Currency, _Method) -> undefined; -extract_payment_limit(#domain_ProvisionTermSet{payments = Payments}, Currency) -> +extract_payment_limit(#domain_ProvisionTermSet{payments = Payments}, Currency, Method) -> Selector = Payments#domain_PaymentsProvisionTerms.cash_limit, - range_from_selector(Selector, Currency). + range_from_selector(Selector, Currency, Method). -extract_partial_refund_limit(undefined, _Currency) -> +extract_partial_refund_limit(undefined, _Currency, _Method) -> undefined; -extract_partial_refund_limit(#domain_TermSet{payments = undefined}, _Currency) -> +extract_partial_refund_limit(#domain_TermSet{payments = undefined}, _Currency, _Method) -> undefined; -extract_partial_refund_limit(#domain_TermSet{payments = Payments}, Currency) -> +extract_partial_refund_limit(#domain_TermSet{payments = Payments}, Currency, Method) -> Refunds = Payments#domain_PaymentsServiceTerms.refunds, - partial_refund_limit_from_refunds(Refunds, Currency); -extract_partial_refund_limit(#domain_ProvisionTermSet{payments = undefined}, _Currency) -> + partial_refund_limit_from_refunds(Refunds, Currency, Method); +extract_partial_refund_limit(#domain_ProvisionTermSet{payments = undefined}, _Currency, _Method) -> undefined; -extract_partial_refund_limit(#domain_ProvisionTermSet{payments = Payments}, Currency) -> +extract_partial_refund_limit(#domain_ProvisionTermSet{payments = Payments}, Currency, Method) -> Refunds = Payments#domain_PaymentsProvisionTerms.refunds, - partial_refund_limit_from_refunds(Refunds, Currency). + partial_refund_limit_from_refunds(Refunds, Currency, Method). -partial_refund_limit_from_refunds(undefined, _Currency) -> +partial_refund_limit_from_refunds(undefined, _Currency, _Method) -> undefined; -partial_refund_limit_from_refunds(#domain_PaymentRefundsServiceTerms{partial_refunds = undefined}, _Currency) -> +partial_refund_limit_from_refunds(#domain_PaymentRefundsServiceTerms{partial_refunds = undefined}, _Currency, _Method) -> undefined; -partial_refund_limit_from_refunds(#domain_PaymentRefundsServiceTerms{partial_refunds = PartialRefunds}, Currency) -> +partial_refund_limit_from_refunds( + #domain_PaymentRefundsServiceTerms{partial_refunds = PartialRefunds}, Currency, Method +) -> Selector = PartialRefunds#domain_PartialRefundsServiceTerms.cash_limit, - range_from_selector(Selector, Currency); -partial_refund_limit_from_refunds(#domain_PaymentRefundsProvisionTerms{partial_refunds = undefined}, _Currency) -> + range_from_selector(Selector, Currency, Method); +partial_refund_limit_from_refunds( + #domain_PaymentRefundsProvisionTerms{partial_refunds = undefined}, _Currency, _Method +) -> undefined; -partial_refund_limit_from_refunds(#domain_PaymentRefundsProvisionTerms{partial_refunds = PartialRefunds}, Currency) -> +partial_refund_limit_from_refunds( + #domain_PaymentRefundsProvisionTerms{partial_refunds = PartialRefunds}, Currency, Method +) -> Selector = PartialRefunds#domain_PartialRefundsProvisionTerms.cash_limit, - range_from_selector(Selector, Currency). + range_from_selector(Selector, Currency, Method). -range_from_selector(undefined, _Currency) -> +range_from_selector(undefined, _Currency, _Method) -> undefined; -range_from_selector(Selector, Currency) -> - Ranges = ranges_from_selector(Selector, Currency), +range_from_selector(Selector, Currency, Method) -> + Ranges = ranges_from_selector(Selector, Currency, Method), intersect_all(Ranges). -ranges_from_selector({value, #domain_CashRange{} = Range}, Currency) -> +ranges_from_selector({value, #domain_CashRange{} = Range}, Currency, _Method) -> normalize_range(Range, Currency); -ranges_from_selector({decisions, Decisions}, Currency) when is_list(Decisions) -> +ranges_from_selector({decisions, Decisions}, Currency, Method) when is_list(Decisions) -> lists:flatmap( - fun(#domain_CashLimitDecision{then_ = Then}) -> - ranges_from_selector(Then, Currency) + fun(#domain_CashLimitDecision{if_ = Predicate, then_ = Then}) -> + case predicate_matches_method(Predicate, Method) of + true -> + ranges_from_selector(Then, Currency, Method); + false -> + [] + end end, Decisions ); -ranges_from_selector(_, _Currency) -> +ranges_from_selector(_, _Currency, _Method) -> []. normalize_range(#domain_CashRange{lower = Lower, upper = Upper}, #domain_CurrencyRef{symbolic_code = CurrencyCode}) -> case {extract_bound(Lower), extract_bound(Upper)} of {{ok, {LowerType, LowerAmount, CurrencyCode}}, {ok, {UpperType, UpperAmount, CurrencyCode}}} -> - [#{ - currency => CurrencyCode, - lower => {LowerType, LowerAmount}, - upper => {UpperType, UpperAmount} - }]; + [ + #{ + currency => CurrencyCode, + lower => {LowerType, LowerAmount}, + upper => {UpperType, UpperAmount} + } + ]; _ -> [] end; @@ -197,7 +221,6 @@ union_ranges(#{lower := Lower1, upper := Upper1} = R1, #{lower := Lower2, upper Upper = max_upper(Upper1, Upper2), R1#{lower => Lower, upper => Upper}. - max_lower({Type1, Amount1}, {_Type2, Amount2}) when Amount1 > Amount2 -> {Type1, Amount1}; max_lower({_Type1, Amount1}, {Type2, Amount2}) when Amount2 > Amount1 -> @@ -259,12 +282,14 @@ pick_refund_limit(undefined, PartialRefund) -> pick_refund_limit(Payment, PartialRefund) -> intersect_optional(Payment, PartialRefund). -encode_limits(Currency, Payment, PartialRefund) -> +encode_limits(Currency, Method, Range) -> CurrencyCode = capi_handler_decoder_utils:decode_currency(Currency), - genlib_map:compact(#{ - <<"payment">> => encode_range(CurrencyCode, Payment), - <<"partialRefund">> => encode_range(CurrencyCode, PartialRefund) - }). + case encode_range(CurrencyCode, Range) of + undefined -> + []; + Encoded -> + [Encoded#{<<"paymentMethod">> => encode_payment_method(Method)}] + end. encode_range(_CurrencyCode, undefined) -> undefined; @@ -331,3 +356,89 @@ log_terminal_terms(TerminalRef, Payment, PartialRefund) -> "Cash limits for terminal ~p: payment=~p partial_refund=~p", [TerminalRef, Payment, PartialRefund] ). + +extract_payment_methods(undefined) -> + undefined; +extract_payment_methods(#domain_TermSet{payments = undefined}) -> + undefined; +extract_payment_methods(#domain_TermSet{payments = Payments}) -> + payment_methods_from_selector(Payments#domain_PaymentsServiceTerms.payment_methods); +extract_payment_methods(#domain_ProvisionTermSet{payments = undefined}) -> + undefined; +extract_payment_methods(#domain_ProvisionTermSet{payments = Payments}) -> + payment_methods_from_selector(Payments#domain_PaymentsProvisionTerms.payment_methods). + +payment_methods_from_selector(undefined) -> + undefined; +payment_methods_from_selector({value, PaymentMethodRefs}) -> + lists:usort([payment_method_kind(Ref) || Ref <- PaymentMethodRefs]); +payment_methods_from_selector(_) -> + []. + +payment_method_kind(#domain_PaymentMethodRef{id = {bank_card, _}}) -> + bank_card; +payment_method_kind(#domain_PaymentMethodRef{id = {payment_terminal, _}}) -> + payment_terminal; +payment_method_kind(#domain_PaymentMethodRef{id = {digital_wallet, _}}) -> + digital_wallet; +payment_method_kind(#domain_PaymentMethodRef{id = {crypto_currency, _}}) -> + crypto_currency; +payment_method_kind(#domain_PaymentMethodRef{id = {mobile, _}}) -> + mobile. + +extract_terminal_payment_methods(TerminalRefs, Context) -> + Methods = lists:foldl( + fun(TerminalRef, Acc) -> + case capi_domain:get({terminal, TerminalRef}, Context) of + {ok, #domain_TerminalObject{data = #domain_Terminal{provider_ref = ProviderRef, terms = TerminalTerms}}} -> + ProviderTerms = get_provider_terms(ProviderRef, Context), + ProviderMethods = extract_payment_methods(ProviderTerms), + TerminalMethods = extract_payment_methods(TerminalTerms), + TerminalAllowed = intersect_methods(ProviderMethods, TerminalMethods), + lists:usort(Acc ++ TerminalAllowed); + _ -> + Acc + end + end, + [], + TerminalRefs + ), + Methods. + +intersect_methods(undefined, undefined) -> + []; +intersect_methods(undefined, Methods) -> + Methods; +intersect_methods(Methods, undefined) -> + Methods; +intersect_methods(Methods1, Methods2) -> + lists:usort([M || M <- Methods1, lists:member(M, Methods2)]). + +predicate_matches_method({constant, true}, _Method) -> + true; +predicate_matches_method({constant, false}, _Method) -> + false; +predicate_matches_method({condition, {payment_tool, {bank_card, _}}}, bank_card) -> + true; +predicate_matches_method({condition, {payment_terminal, _}}, payment_terminal) -> + true; +predicate_matches_method({condition, {digital_wallet, _}}, digital_wallet) -> + true; +predicate_matches_method({condition, {crypto_currency, _}}, crypto_currency) -> + true; +predicate_matches_method({condition, {mobile_commerce, _}}, mobile) -> + true; +predicate_matches_method(_Predicate, _Method) -> + %% TODO handle complex predicates (all_of/any_of/is_not) + false. + +encode_payment_method(bank_card) -> + #{<<"method">> => <<"BankCard">>}; +encode_payment_method(payment_terminal) -> + #{<<"method">> => <<"PaymentTerminal">>}; +encode_payment_method(digital_wallet) -> + #{<<"method">> => <<"DigitalWallet">>}; +encode_payment_method(crypto_currency) -> + #{<<"method">> => <<"CryptoWallet">>}; +encode_payment_method(mobile) -> + #{<<"method">> => <<"MobileCommerce">>}. diff --git a/apps/capi/src/capi_handler_shops.erl b/apps/capi/src/capi_handler_shops.erl index 02b4ab6..e307404 100644 --- a/apps/capi/src/capi_handler_shops.erl +++ b/apps/capi/src/capi_handler_shops.erl @@ -48,7 +48,23 @@ prepare('GetShopByIDForParty' = OperationID, Req, Context) -> Process = fun() -> case capi_party:get_shop(PartyID, ShopID, Context) of {ok, Shop} -> - {ok, {200, #{}, decode_shop(ShopID, Shop, PartyID, Context, true)}}; + {ok, {200, #{}, decode_shop(ShopID, Shop)}}; + {error, not_found} -> + {ok, general_error(404, <<"Shop not found">>)} + end + end, + {ok, #{authorize => Authorize, process => Process}}; +prepare('GetShopCashLimitsForParty' = OperationID, Req, Context) -> + PartyID = maps:get('partyID', Req), + ShopID = maps:get('shopID', Req), + Authorize = fun() -> + Prototypes = [{operation, #{id => OperationID, party => PartyID, shop => ShopID}}], + {ok, capi_auth:authorize_operation(Prototypes, Context)} + end, + Process = fun() -> + case capi_cash_limits:get_shop_limits(PartyID, ShopID, Context) of + {ok, Limits} -> + {ok, {200, #{}, Limits}}; {error, not_found} -> {ok, general_error(404, <<"Shop not found">>)} end @@ -70,7 +86,7 @@ get_shops_for_party(PartyID, Context) -> {ok, ShopsWithIDs} -> {ok, lists:map( - fun({ShopID, Shop}) -> decode_shop(ShopID, Shop, PartyID, Context, false) end, + fun({ShopID, Shop}) -> decode_shop(ShopID, Shop) end, ShopsWithIDs )}; {error, not_found} -> @@ -87,8 +103,7 @@ restrict_shops(Shops, Restrictions) -> Shops ). -decode_shop(ShopID, Shop, PartyID, Context, IncludeLimits) -> - Limits = maybe_get_cash_limits(IncludeLimits, PartyID, ShopID, Context), +decode_shop(ShopID, Shop) -> genlib_map:compact(#{ <<"id">> => ShopID, <<"isBlocked">> => capi_handler_decoder_party:is_blocked(Shop#domain_ShopConfig.block), @@ -97,19 +112,8 @@ decode_shop(ShopID, Shop, PartyID, Context, IncludeLimits) -> <<"categoryID">> => capi_handler_decoder_utils:decode_category_ref(Shop#domain_ShopConfig.category), <<"details">> => capi_handler_decoder_party:decode_shop_details(Shop), <<"location">> => capi_handler_decoder_party:decode_shop_location(Shop#domain_ShopConfig.location), - <<"contractID">> => genlib:to_binary(Shop#domain_ShopConfig.terms#domain_TermSetHierarchyRef.id), - <<"cashLimits">> => Limits + <<"contractID">> => genlib:to_binary(Shop#domain_ShopConfig.terms#domain_TermSetHierarchyRef.id) }). -maybe_get_cash_limits(false, _PartyID, _ShopID, _Context) -> - undefined; -maybe_get_cash_limits(true, PartyID, ShopID, Context) -> - case capi_cash_limits:get_shop_limits(PartyID, ShopID, Context) of - {ok, Limits} when Limits =/= #{} -> - Limits; - _ -> - undefined - end. - get_shop_currency(#domain_ShopAccount{currency = Currency}) -> capi_handler_decoder_utils:decode_currency(Currency). diff --git a/rebar.config b/rebar.config index 2f06869..6d1c3df 100644 --- a/rebar.config +++ b/rebar.config @@ -52,8 +52,8 @@ {party_client, {git, "https://github.com/valitydev/party-client-erlang.git", {tag, "v2.0.2"}}}, {feat, {git, "https://github.com/valitydev/feat.git", {branch, master}}}, %% Libraries generated with swagger-codegen-erlang from valitydev/swag-payments - {swag_server, {git, "https://github.com/valitydev/swag-payments", {branch, "release/erlang/server/epic-v3"}}}, - {swag_client, {git, "https://github.com/valitydev/swag-payments", {branch, "release/erlang/client/epic-v3"}}}, + {swag_server, {git, "https://github.com/valitydev/swag-payments", {branch, "release/erlang/server/v3"}}}, + {swag_client, {git, "https://github.com/valitydev/swag-payments", {branch, "release/erlang/client/v3"}}}, %% OpenTelemetry deps {opentelemetry_api, "1.4.0"}, {opentelemetry, "1.5.0"}, @@ -114,9 +114,6 @@ ]} ]}, {test, [ - {deps, [ - {meck, "1.1.0"} - ]}, {dialyzer, [{plt_extra_apps, [eunit, common_test, runtime_tools, bender_proto]}]} ]} ]}. diff --git a/rebar.lock b/rebar.lock index e42f5f8..7d1af3e 100644 --- a/rebar.lock +++ b/rebar.lock @@ -122,11 +122,11 @@ {<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.7">>},2}, {<<"swag_client">>, {git,"https://github.com/valitydev/swag-payments", - {ref,"7fe8ac4c0dfe40cb1341a56238157119a1603187"}}, + {ref,"24d442d4e14eb8ccb7a960fcebdf9a388c2d4700"}}, 0}, {<<"swag_server">>, {git,"https://github.com/valitydev/swag-payments", - {ref,"8f261e1e6371c4b6e15646e01ce10052b719bcad"}}, + {ref,"fddf13c86f824c75d9750cec90c9c742c7a377e7"}}, 0}, {<<"thrift">>, {git,"https://github.com/valitydev/thrift_erlang.git", From 0d411cd3f701d64a0950d1d40aec6a38594f8945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Tue, 20 Jan 2026 18:37:05 +0300 Subject: [PATCH 4/5] refactored --- apps/capi/src/capi_cash_limits.erl | 519 +++++++----------- .../test/capi_base_api_token_tests_SUITE.erl | 11 + apps/capi/test/capi_cash_limits_tests.erl | 291 ---------- apps/capi/test/capi_dummy_data.hrl | 363 ++++++++++++ 4 files changed, 568 insertions(+), 616 deletions(-) delete mode 100644 apps/capi/test/capi_cash_limits_tests.erl diff --git a/apps/capi/src/capi_cash_limits.erl b/apps/capi/src/capi_cash_limits.erl index 72a8263..1efa2e6 100644 --- a/apps/capi/src/capi_cash_limits.erl +++ b/apps/capi/src/capi_cash_limits.erl @@ -18,27 +18,16 @@ get_shop_limits(PartyID, ShopID, Context) -> Currency = Shop#domain_ShopConfig.account#domain_ShopAccount.currency, Revision = capi_domain:head(), ShopTerms = get_shop_terms(Shop#domain_ShopConfig.terms, Revision, Context), - TerminalRefs = get_payment_terminal_refs(Shop#domain_ShopConfig.payment_institution, Context), ShopMethods = extract_payment_methods(ShopTerms), - TerminalMethods = extract_terminal_payment_methods(TerminalRefs, Context), - Methods = intersect_methods(ShopMethods, TerminalMethods), - Limits = lists:foldl( - fun(Method, Acc) -> - ShopPayment = extract_payment_limit(ShopTerms, Currency, Method), - ShopPartialRefund = extract_partial_refund_limit(ShopTerms, Currency, Method), - {TermPayment, TermPartialRefund} = aggregate_terminal_limits( - TerminalRefs, - Currency, - Method, - Context - ), - Payment = intersect_optional(ShopPayment, TermPayment), - PartialRefund = intersect_optional(ShopPartialRefund, TermPartialRefund), - EffectiveLimit = pick_refund_limit(Payment, PartialRefund), - Acc ++ encode_limits(Currency, Method, EffectiveLimit) + ShopLimit = extract_shop_limit(ShopTerms, Currency), + TerminalRefs = get_payment_terminal_refs(Shop#domain_ShopConfig.payment_institution, Context), + TermLimit = aggregate_terminal_limits(TerminalRefs, Currency, Context), + EffectiveLimit = intersect_optional(ShopLimit, TermLimit), + Limits = lists:flatmap( + fun(ShopMethod) -> + encode_limits(Currency, ShopMethod, EffectiveLimit) end, - [], - Methods + ShopMethods ), {ok, Limits} end. @@ -51,33 +40,119 @@ get_shop_terms(TermsRef, Revision, Context) -> undefined end. -aggregate_terminal_limits(TerminalRefs, Currency, Method, Context) -> - {PaymentAcc, PartialRefundAcc} = lists:foldl( - fun(TerminalRef, {PaymentAcc, PartialRefundAcc}) -> - {Payment, PartialRefund} = get_terminal_limits(TerminalRef, Currency, Method, Context), - log_terminal_terms(TerminalRef, Payment, PartialRefund), - {union_optional(PaymentAcc, Payment), union_optional(PartialRefundAcc, PartialRefund)} +extract_payment_methods(#domain_TermSet{ + payments = #domain_PaymentsServiceTerms{payment_methods = {value, PaymentMethodRefs}} +}) -> + lists:usort([ID || #domain_PaymentMethodRef{id = {ID, _}} <- PaymentMethodRefs]); +extract_payment_methods(_) -> + []. + +extract_shop_limit(undefined, _Currency) -> + undefined; +extract_shop_limit(#domain_TermSet{payments = undefined}, _Currency) -> + undefined; +extract_shop_limit(#domain_TermSet{payments = Payments}, Currency) -> + PaymentSelector = Payments#domain_PaymentsServiceTerms.cash_limit, + PartialRefundSelector = + case Payments#domain_PaymentsServiceTerms.refunds of + undefined -> + undefined; + #domain_PaymentRefundsServiceTerms{partial_refunds = undefined} -> + undefined; + #domain_PaymentRefundsServiceTerms{partial_refunds = PartialRefunds} -> + PartialRefunds#domain_PartialRefundsServiceTerms.cash_limit end, - {init, init}, + Payment = range_from_selector(PaymentSelector, Currency), + PartialRefund = range_from_selector(PartialRefundSelector, Currency), + pick_refund_limit(Payment, PartialRefund). + +get_payment_terminal_refs(PiRef, Context) -> + case capi_domain:get_payment_institution(PiRef, Context) of + {ok, #domain_PaymentInstitution{payment_routing_rules = Rules}} -> + lists:usort(collect_ruleset_terminals(Rules, Context)); + _ -> + [] + end. + +collect_ruleset_terminals(undefined, _Context) -> + []; +collect_ruleset_terminals(#domain_RoutingRules{policies = PoliciesRef}, Context) -> + collect_ruleset_terminals(PoliciesRef, Context, sets:new()). + +collect_ruleset_terminals(#domain_RoutingRulesetRef{} = Ref, Context, Seen) -> + case sets:is_element(Ref, Seen) of + true -> + []; + false -> + Seen1 = sets:add_element(Ref, Seen), + case capi_domain:get({routing_rules, Ref}, Context) of + {ok, #domain_RoutingRulesObject{data = Ruleset}} -> + collect_ruleset_terminals(Ruleset, Context, Seen1); + _ -> + [] + end + end; +collect_ruleset_terminals(#domain_RoutingRuleset{decisions = Decisions}, Context, Seen) -> + collect_terminals_from_decisions(Decisions, Context, Seen). + +collect_terminals_from_decisions({candidates, Candidates}, _Context, _Seen) -> + [C#domain_RoutingCandidate.terminal || C <- Candidates]; +collect_terminals_from_decisions({delegates, Delegates}, Context, Seen) -> + lists:flatmap( + fun(#domain_RoutingDelegate{ruleset = Ref}) -> + collect_ruleset_terminals(Ref, Context, Seen) + end, + Delegates + ). + +aggregate_terminal_limits([], _Currency, _Context) -> + undefined; +aggregate_terminal_limits([TerminalRef | TerminalRefs], Currency, Context) -> + Limit0 = get_terminal_limit(TerminalRef, Currency, Context), + log_terminal_terms(TerminalRef, Limit0), + lists:foldl( + fun(TerminalRef1, LimitAcc) -> + Limit = get_terminal_limit(TerminalRef1, Currency, Context), + log_terminal_terms(TerminalRef1, Limit), + union_optional(LimitAcc, Limit) + end, + Limit0, TerminalRefs - ), - {normalize_union(PaymentAcc), normalize_union(PartialRefundAcc)}. + ). + +log_terminal_terms(TerminalRef, Limit) -> + logger:debug( + "Cash limits for terminal ~p: limit=~p", + [TerminalRef, Limit] + ). + +get_terminal_limit(TerminalRef, Currency, Context) -> + case get_and_check_terminal(TerminalRef, Context) of + {ok, #domain_Terminal{provider_ref = ProviderRef, terms = TerminalTerms}} -> + TerminalLimit = extract_provider_limit(TerminalTerms, Currency), + case TerminalLimit of + undefined -> + ProviderTerms = get_provider_terms(ProviderRef, Context), + extract_provider_limit(ProviderTerms, Currency); + _ -> + TerminalLimit + end; + _ -> + undefined + end. -get_terminal_limits(TerminalRef, Currency, Method, Context) -> +get_and_check_terminal(TerminalRef, Context) -> case capi_domain:get({terminal, TerminalRef}, Context) of - {ok, #domain_TerminalObject{data = #domain_Terminal{provider_ref = ProviderRef, terms = TerminalTerms}}} -> - ProviderTerms = get_provider_terms(ProviderRef, Context), - Payment = intersect_optional( - extract_payment_limit(ProviderTerms, Currency, Method), - extract_payment_limit(TerminalTerms, Currency, Method) - ), - PartialRefund = intersect_optional( - extract_partial_refund_limit(ProviderTerms, Currency, Method), - extract_partial_refund_limit(TerminalTerms, Currency, Method) - ), - {Payment, PartialRefund}; + {ok, #domain_TerminalObject{data = #domain_Terminal{terms = Terms} = Terminal}} -> + #domain_ProvisionTermSet{payments = #domain_PaymentsProvisionTerms{cash_limit = CashLimit}} = Terms, + case CashLimit of + {decisions, _} -> + undefined; + _ -> + {ok, Terminal} + end; _ -> - {undefined, undefined} + undefined end. get_provider_terms(ProviderRef, Context) -> @@ -88,123 +163,64 @@ get_provider_terms(ProviderRef, Context) -> undefined end. -extract_payment_limit(undefined, _Currency, _Method) -> - undefined; -extract_payment_limit(#domain_TermSet{payments = undefined}, _Currency, _Method) -> - undefined; -extract_payment_limit(#domain_TermSet{payments = Payments}, Currency, Method) -> - Selector = Payments#domain_PaymentsServiceTerms.cash_limit, - range_from_selector(Selector, Currency, Method); -extract_payment_limit(#domain_ProvisionTermSet{payments = undefined}, _Currency, _Method) -> - undefined; -extract_payment_limit(#domain_ProvisionTermSet{payments = Payments}, Currency, Method) -> - Selector = Payments#domain_PaymentsProvisionTerms.cash_limit, - range_from_selector(Selector, Currency, Method). - -extract_partial_refund_limit(undefined, _Currency, _Method) -> +extract_provider_limit(undefined, _Currency) -> undefined; -extract_partial_refund_limit(#domain_TermSet{payments = undefined}, _Currency, _Method) -> +extract_provider_limit(#domain_ProvisionTermSet{payments = undefined}, _Currency) -> undefined; -extract_partial_refund_limit(#domain_TermSet{payments = Payments}, Currency, Method) -> - Refunds = Payments#domain_PaymentsServiceTerms.refunds, - partial_refund_limit_from_refunds(Refunds, Currency, Method); -extract_partial_refund_limit(#domain_ProvisionTermSet{payments = undefined}, _Currency, _Method) -> - undefined; -extract_partial_refund_limit(#domain_ProvisionTermSet{payments = Payments}, Currency, Method) -> - Refunds = Payments#domain_PaymentsProvisionTerms.refunds, - partial_refund_limit_from_refunds(Refunds, Currency, Method). - -partial_refund_limit_from_refunds(undefined, _Currency, _Method) -> - undefined; -partial_refund_limit_from_refunds(#domain_PaymentRefundsServiceTerms{partial_refunds = undefined}, _Currency, _Method) -> - undefined; -partial_refund_limit_from_refunds( - #domain_PaymentRefundsServiceTerms{partial_refunds = PartialRefunds}, Currency, Method -) -> - Selector = PartialRefunds#domain_PartialRefundsServiceTerms.cash_limit, - range_from_selector(Selector, Currency, Method); -partial_refund_limit_from_refunds( - #domain_PaymentRefundsProvisionTerms{partial_refunds = undefined}, _Currency, _Method -) -> - undefined; -partial_refund_limit_from_refunds( - #domain_PaymentRefundsProvisionTerms{partial_refunds = PartialRefunds}, Currency, Method -) -> - Selector = PartialRefunds#domain_PartialRefundsProvisionTerms.cash_limit, - range_from_selector(Selector, Currency, Method). - -range_from_selector(undefined, _Currency, _Method) -> - undefined; -range_from_selector(Selector, Currency, Method) -> - Ranges = ranges_from_selector(Selector, Currency, Method), - intersect_all(Ranges). +extract_provider_limit(#domain_ProvisionTermSet{payments = Payments}, Currency) -> + PaymentSelector = Payments#domain_PaymentsProvisionTerms.cash_limit, + PartialRefundSelector = + case Payments#domain_PaymentsProvisionTerms.refunds of + undefined -> + undefined; + #domain_PaymentRefundsProvisionTerms{partial_refunds = undefined} -> + undefined; + #domain_PaymentRefundsProvisionTerms{partial_refunds = PartialRefunds} -> + PartialRefunds#domain_PartialRefundsProvisionTerms.cash_limit + end, + Payment = range_from_selector(PaymentSelector, Currency), + PartialRefund = range_from_selector(PartialRefundSelector, Currency), + pick_refund_limit(Payment, PartialRefund). -ranges_from_selector({value, #domain_CashRange{} = Range}, Currency, _Method) -> +range_from_selector({value, #domain_CashRange{} = Range}, Currency) -> normalize_range(Range, Currency); -ranges_from_selector({decisions, Decisions}, Currency, Method) when is_list(Decisions) -> - lists:flatmap( - fun(#domain_CashLimitDecision{if_ = Predicate, then_ = Then}) -> - case predicate_matches_method(Predicate, Method) of - true -> - ranges_from_selector(Then, Currency, Method); - false -> - [] - end - end, - Decisions - ); -ranges_from_selector(_, _Currency, _Method) -> - []. +range_from_selector(_, _Currency) -> + undefined. normalize_range(#domain_CashRange{lower = Lower, upper = Upper}, #domain_CurrencyRef{symbolic_code = CurrencyCode}) -> - case {extract_bound(Lower), extract_bound(Upper)} of - {{ok, {LowerType, LowerAmount, CurrencyCode}}, {ok, {UpperType, UpperAmount, CurrencyCode}}} -> - [ - #{ - currency => CurrencyCode, - lower => {LowerType, LowerAmount}, - upper => {UpperType, UpperAmount} - } - ]; + {LowerAmount, LowerCode} = extract_bound(Lower), + {UpperAmount, UpperCode} = extract_bound(Upper), + case {LowerCode, UpperCode} of + {CurrencyCode, CurrencyCode} -> + #{ + currency => CurrencyCode, + lower => LowerAmount, + upper => UpperAmount + }; _ -> - [] - end; -normalize_range(_Range, _Currency) -> - []. + undefined + end. extract_bound({inclusive, #domain_Cash{amount = Amount, currency = #domain_CurrencyRef{symbolic_code = Code}}}) -> - {ok, {inclusive, Amount, Code}}; + {Amount, Code}; extract_bound({exclusive, #domain_Cash{amount = Amount, currency = #domain_CurrencyRef{symbolic_code = Code}}}) -> - {ok, {exclusive, Amount, Code}}; -extract_bound(_) -> - error. + {Amount, Code}. -intersect_all([]) -> +pick_refund_limit(undefined, undefined) -> undefined; -intersect_all([Range | Rest]) -> - lists:foldl(fun intersect_optional/2, Range, Rest). +pick_refund_limit(Payment, undefined) -> + Payment; +pick_refund_limit(undefined, PartialRefund) -> + PartialRefund; +pick_refund_limit(Payment, PartialRefund) -> + intersect_optional(Payment, PartialRefund). intersect_optional(undefined, Range) -> Range; intersect_optional(Range, undefined) -> Range; intersect_optional(#{currency := Currency} = R1, #{currency := Currency} = R2) -> - intersect_ranges(R1, R2); -intersect_optional(_R1, _R2) -> - undefined. - -union_optional(undefined, _Range) -> - undefined; -union_optional(_Range, undefined) -> - undefined; -union_optional(init, Range) -> - Range; -union_optional(Range, init) -> - Range; -union_optional(#{currency := Currency} = R1, #{currency := Currency} = R2) -> - union_ranges(R1, R2); -union_optional(_R1, _R2) -> - undefined. + intersect_ranges(R1, R2). intersect_ranges(#{lower := Lower1, upper := Upper1} = R1, #{lower := Lower2, upper := Upper2}) -> Lower = max_lower(Lower1, Lower2), @@ -216,72 +232,51 @@ intersect_ranges(#{lower := Lower1, upper := Upper1} = R1, #{lower := Lower2, up undefined end. +union_optional(undefined, Range) -> + Range; +union_optional(Range, undefined) -> + Range; +union_optional(#{currency := Currency} = R1, #{currency := Currency} = R2) -> + union_ranges(R1, R2). + union_ranges(#{lower := Lower1, upper := Upper1} = R1, #{lower := Lower2, upper := Upper2}) -> Lower = min_lower(Lower1, Lower2), Upper = max_upper(Upper1, Upper2), R1#{lower => Lower, upper => Upper}. -max_lower({Type1, Amount1}, {_Type2, Amount2}) when Amount1 > Amount2 -> - {Type1, Amount1}; -max_lower({_Type1, Amount1}, {Type2, Amount2}) when Amount2 > Amount1 -> - {Type2, Amount2}; -max_lower({Type1, Amount}, {Type2, Amount}) -> - case Type1 =:= exclusive orelse Type2 =:= exclusive of - true -> {exclusive, Amount}; - false -> {inclusive, Amount} - end. - -min_lower({Type1, Amount1}, {_Type2, Amount2}) when Amount1 < Amount2 -> - {Type1, Amount1}; -min_lower({_Type1, Amount1}, {Type2, Amount2}) when Amount2 < Amount1 -> - {Type2, Amount2}; -min_lower({Type1, Amount}, {Type2, Amount}) -> - case Type1 =:= inclusive orelse Type2 =:= inclusive of - true -> {inclusive, Amount}; - false -> {exclusive, Amount} - end. - -min_upper({Type1, Amount1}, {_Type2, Amount2}) when Amount1 < Amount2 -> - {Type1, Amount1}; -min_upper({_Type1, Amount1}, {Type2, Amount2}) when Amount2 < Amount1 -> - {Type2, Amount2}; -min_upper({Type1, Amount}, {Type2, Amount}) -> - case Type1 =:= exclusive orelse Type2 =:= exclusive of - true -> {exclusive, Amount}; - false -> {inclusive, Amount} - end. - -max_upper({Type1, Amount1}, {_Type2, Amount2}) when Amount1 > Amount2 -> - {Type1, Amount1}; -max_upper({_Type1, Amount1}, {Type2, Amount2}) when Amount2 > Amount1 -> - {Type2, Amount2}; -max_upper({Type1, Amount}, {Type2, Amount}) -> - case Type1 =:= inclusive orelse Type2 =:= inclusive of - true -> {inclusive, Amount}; - false -> {exclusive, Amount} - end. - -valid_range({_, LowerAmount}, {_, UpperAmount}) when LowerAmount < UpperAmount -> - true; -valid_range({inclusive, Amount}, {inclusive, Amount}) -> +max_lower(Amount1, Amount2) when Amount1 > Amount2 -> + Amount1; +max_lower(Amount1, Amount2) when Amount2 > Amount1 -> + Amount2; +max_lower(Amount, Amount) -> + Amount. + +min_lower(Amount1, Amount2) when Amount1 < Amount2 -> + Amount1; +min_lower(Amount1, Amount2) when Amount2 < Amount1 -> + Amount2; +min_lower(Amount, Amount) -> + Amount. + +max_upper(Amount1, Amount2) when Amount1 > Amount2 -> + Amount1; +max_upper(Amount1, Amount2) when Amount2 > Amount1 -> + Amount2; +max_upper(Amount, Amount) -> + Amount. + +min_upper(Amount1, Amount2) when Amount1 < Amount2 -> + Amount1; +min_upper(Amount1, Amount2) when Amount2 < Amount1 -> + Amount2; +min_upper(Amount, Amount) -> + Amount. + +valid_range(LowerAmount, UpperAmount) when LowerAmount =< UpperAmount -> true; valid_range(_, _) -> false. -normalize_union(init) -> - undefined; -normalize_union(Value) -> - Value. - -pick_refund_limit(undefined, undefined) -> - undefined; -pick_refund_limit(Payment, undefined) -> - Payment; -pick_refund_limit(undefined, PartialRefund) -> - PartialRefund; -pick_refund_limit(Payment, PartialRefund) -> - intersect_optional(Payment, PartialRefund). - encode_limits(Currency, Method, Range) -> CurrencyCode = capi_handler_decoder_utils:decode_currency(Currency), case encode_range(CurrencyCode, Range) of @@ -300,138 +295,12 @@ encode_range(CurrencyCode, #{lower := Lower, upper := Upper}) -> <<"upperBound">> => encode_bound(Upper) }. -encode_bound({Type, Amount}) -> +encode_bound(Amount) -> #{ <<"amount">> => Amount, - <<"inclusive">> => Type =:= inclusive + <<"inclusive">> => true }. -get_payment_terminal_refs(PiRef, Context) -> - case capi_domain:get_payment_institution(PiRef, Context) of - {ok, #domain_PaymentInstitution{payment_routing_rules = Rules}} -> - lists:usort(collect_ruleset_terminals(Rules, Context)); - _ -> - [] - end. - -collect_ruleset_terminals(undefined, _Context) -> - []; -collect_ruleset_terminals(#domain_RoutingRules{policies = PoliciesRef}, Context) -> - collect_ruleset_terminals(PoliciesRef, Context, sets:new()); -collect_ruleset_terminals(_, _Context) -> - []. - -collect_ruleset_terminals(#domain_RoutingRulesetRef{} = Ref, Context, Seen) -> - case sets:is_element(Ref, Seen) of - true -> - []; - false -> - Seen1 = sets:add_element(Ref, Seen), - case capi_domain:get({routing_rules, Ref}, Context) of - {ok, #domain_RoutingRulesObject{data = Ruleset}} -> - collect_ruleset_terminals(Ruleset, Context, Seen1); - _ -> - [] - end - end; -collect_ruleset_terminals(#domain_RoutingRuleset{decisions = Decisions}, Context, Seen) -> - collect_terminals_from_decisions(Decisions, Context, Seen); -collect_ruleset_terminals(_, _Context, _Seen) -> - []. - -collect_terminals_from_decisions({candidates, Candidates}, _Context, _Seen) -> - [C#domain_RoutingCandidate.terminal || C <- Candidates]; -collect_terminals_from_decisions({delegates, Delegates}, Context, Seen) -> - lists:flatmap( - fun(#domain_RoutingDelegate{ruleset = Ref}) -> - collect_ruleset_terminals(Ref, Context, Seen) - end, - Delegates - ); -collect_terminals_from_decisions(_, _Context, _Seen) -> - []. - -log_terminal_terms(TerminalRef, Payment, PartialRefund) -> - logger:debug( - "Cash limits for terminal ~p: payment=~p partial_refund=~p", - [TerminalRef, Payment, PartialRefund] - ). - -extract_payment_methods(undefined) -> - undefined; -extract_payment_methods(#domain_TermSet{payments = undefined}) -> - undefined; -extract_payment_methods(#domain_TermSet{payments = Payments}) -> - payment_methods_from_selector(Payments#domain_PaymentsServiceTerms.payment_methods); -extract_payment_methods(#domain_ProvisionTermSet{payments = undefined}) -> - undefined; -extract_payment_methods(#domain_ProvisionTermSet{payments = Payments}) -> - payment_methods_from_selector(Payments#domain_PaymentsProvisionTerms.payment_methods). - -payment_methods_from_selector(undefined) -> - undefined; -payment_methods_from_selector({value, PaymentMethodRefs}) -> - lists:usort([payment_method_kind(Ref) || Ref <- PaymentMethodRefs]); -payment_methods_from_selector(_) -> - []. - -payment_method_kind(#domain_PaymentMethodRef{id = {bank_card, _}}) -> - bank_card; -payment_method_kind(#domain_PaymentMethodRef{id = {payment_terminal, _}}) -> - payment_terminal; -payment_method_kind(#domain_PaymentMethodRef{id = {digital_wallet, _}}) -> - digital_wallet; -payment_method_kind(#domain_PaymentMethodRef{id = {crypto_currency, _}}) -> - crypto_currency; -payment_method_kind(#domain_PaymentMethodRef{id = {mobile, _}}) -> - mobile. - -extract_terminal_payment_methods(TerminalRefs, Context) -> - Methods = lists:foldl( - fun(TerminalRef, Acc) -> - case capi_domain:get({terminal, TerminalRef}, Context) of - {ok, #domain_TerminalObject{data = #domain_Terminal{provider_ref = ProviderRef, terms = TerminalTerms}}} -> - ProviderTerms = get_provider_terms(ProviderRef, Context), - ProviderMethods = extract_payment_methods(ProviderTerms), - TerminalMethods = extract_payment_methods(TerminalTerms), - TerminalAllowed = intersect_methods(ProviderMethods, TerminalMethods), - lists:usort(Acc ++ TerminalAllowed); - _ -> - Acc - end - end, - [], - TerminalRefs - ), - Methods. - -intersect_methods(undefined, undefined) -> - []; -intersect_methods(undefined, Methods) -> - Methods; -intersect_methods(Methods, undefined) -> - Methods; -intersect_methods(Methods1, Methods2) -> - lists:usort([M || M <- Methods1, lists:member(M, Methods2)]). - -predicate_matches_method({constant, true}, _Method) -> - true; -predicate_matches_method({constant, false}, _Method) -> - false; -predicate_matches_method({condition, {payment_tool, {bank_card, _}}}, bank_card) -> - true; -predicate_matches_method({condition, {payment_terminal, _}}, payment_terminal) -> - true; -predicate_matches_method({condition, {digital_wallet, _}}, digital_wallet) -> - true; -predicate_matches_method({condition, {crypto_currency, _}}, crypto_currency) -> - true; -predicate_matches_method({condition, {mobile_commerce, _}}, mobile) -> - true; -predicate_matches_method(_Predicate, _Method) -> - %% TODO handle complex predicates (all_of/any_of/is_not) - false. - encode_payment_method(bank_card) -> #{<<"method">> => <<"BankCard">>}; encode_payment_method(payment_terminal) -> diff --git a/apps/capi/test/capi_base_api_token_tests_SUITE.erl b/apps/capi/test/capi_base_api_token_tests_SUITE.erl index 4dc791f..b2ebcb4 100644 --- a/apps/capi/test/capi_base_api_token_tests_SUITE.erl +++ b/apps/capi/test/capi_base_api_token_tests_SUITE.erl @@ -64,6 +64,7 @@ get_shops_for_party_restricted_ok_test/1, get_shop_by_id_for_party_error_test/1, get_shops_for_party_error_test/1, + real_config_limits_test/1, create_webhook_ok_test/1, create_webhook_limit_exceeded_test/1, get_webhooks/1, @@ -143,6 +144,7 @@ groups() -> get_shops_for_party_ok_test, get_shops_for_party_restricted_ok_test, get_shops_for_party_error_test, + real_config_limits_test, create_payment_ok_test, create_payment_with_changed_cost_ok_test, @@ -1229,6 +1231,15 @@ get_shops_for_party_error_test(Config) -> capi_client_shops:get_shops_for_party(?config(context, Config), <<"WrongPartyID">>) ). +-spec real_config_limits_test(config()) -> ok. +real_config_limits_test(Config) -> + Context = ?config(context, Config), + {ok, [Result]} = capi_cash_limits:get_shop_limits(?KZT_PARTY_ID, ?KZT_SHOP_ID, Context), + ?assertEqual(#{<<"method">> => <<"BankCard">>}, maps:get(<<"paymentMethod">>, Result)), + ?assertEqual(<<"KZT">>, maps:get(<<"currency">>, Result)), + ?assertEqual(#{<<"amount">> => 10000, <<"inclusive">> => true}, maps:get(<<"lowerBound">>, Result)), + ?assertEqual(#{<<"amount">> => 120000000, <<"inclusive">> => true}, maps:get(<<"upperBound">>, Result)). + -spec create_webhook_ok_test(config()) -> _. create_webhook_ok_test(Config) -> _ = capi_ct_helper:mock_services( diff --git a/apps/capi/test/capi_cash_limits_tests.erl b/apps/capi/test/capi_cash_limits_tests.erl deleted file mode 100644 index c8c13fa..0000000 --- a/apps/capi/test/capi_cash_limits_tests.erl +++ /dev/null @@ -1,291 +0,0 @@ --module(capi_cash_limits_tests). - --include_lib("eunit/include/eunit.hrl"). --include_lib("damsel/include/dmsl_domain_thrift.hrl"). - --export([test/0]). - --spec test() -> term(). -test() -> - eunit:test(?MODULE). - --spec shop_only_limits_test_() -> term(). -shop_only_limits_test_() -> - {setup, fun setup_mocks/0, fun teardown_mocks/1, fun shop_only_limits_test/0}. - --spec terminal_intersection_test_() -> term(). -terminal_intersection_test_() -> - {setup, fun setup_mocks/0, fun teardown_mocks/1, fun terminal_intersection_test/0}. - --spec real_config_limits_test_() -> term(). -real_config_limits_test_() -> - {setup, fun setup_mocks/0, fun teardown_mocks/1, fun real_config_limits_test/0}. - --spec setup_mocks() -> ok. -setup_mocks() -> - ok = meck:new(capi_party, [non_strict]), - ok = meck:new(capi_domain, [non_strict]), - ok. - --spec teardown_mocks(term()) -> ok. -teardown_mocks(_) -> - meck:unload(capi_party), - meck:unload(capi_domain). - --spec shop_only_limits_test() -> ok. -shop_only_limits_test() -> - Currency = #domain_CurrencyRef{symbolic_code = <<"RUB">>}, - ShopTerms = #domain_TermSet{ - payments = #domain_PaymentsServiceTerms{ - cash_limit = {value, mk_cash_range(100, 1000, Currency)}, - refunds = #domain_PaymentRefundsServiceTerms{ - partial_refunds = #domain_PartialRefundsServiceTerms{ - cash_limit = {value, mk_cash_range(200, 900, Currency)} - } - } - } - }, - Shop = mk_shop(Currency, #domain_TermSetHierarchyRef{id = 10}, #domain_PaymentInstitutionRef{id = 77}), - meck:expect(capi_party, get_shop, fun(_PartyID, _ShopID, _Context) -> {ok, Shop} end), - meck:expect(capi_domain, head, fun() -> 1 end), - meck:expect( - capi_domain, - get_ext, - fun({term_set_hierarchy, #domain_TermSetHierarchyRef{id = 10}}, 1, _Context) -> - {ok, #domain_TermSetHierarchy{term_set = ShopTerms}}; - (_, _, _) -> - {error, not_found} - end - ), - meck:expect( - capi_domain, - get_payment_institution, - fun(#domain_PaymentInstitutionRef{id = 77}, _Context) -> - {ok, #domain_PaymentInstitution{payment_routing_rules = undefined}} - end - ), - {ok, Result} = capi_cash_limits:get_shop_limits(<<"party">>, <<"shop">>, #{}), - Payment = maps:get(<<"payment">>, Result), - PartialRefund = maps:get(<<"partialRefund">>, Result), - ?assertEqual(<<"RUB">>, maps:get(<<"currency">>, Payment)), - ?assertEqual(#{<<"amount">> => 100, <<"inclusive">> => true}, maps:get(<<"lowerBound">>, Payment)), - ?assertEqual(#{<<"amount">> => 1000, <<"inclusive">> => true}, maps:get(<<"upperBound">>, Payment)), - ?assertEqual(<<"RUB">>, maps:get(<<"currency">>, PartialRefund)), - ?assertEqual(#{<<"amount">> => 200, <<"inclusive">> => true}, maps:get(<<"lowerBound">>, PartialRefund)), - ?assertEqual(#{<<"amount">> => 900, <<"inclusive">> => true}, maps:get(<<"upperBound">>, PartialRefund)). - --spec terminal_intersection_test() -> ok. -terminal_intersection_test() -> - Currency = #domain_CurrencyRef{symbolic_code = <<"RUB">>}, - ShopTerms = #domain_TermSet{ - payments = #domain_PaymentsServiceTerms{ - cash_limit = {value, mk_cash_range(100, 1000, Currency)} - } - }, - Shop = mk_shop(Currency, #domain_TermSetHierarchyRef{id = 20}, #domain_PaymentInstitutionRef{id = 88}), - ProviderRef1 = #domain_ProviderRef{id = 1}, - ProviderRef2 = #domain_ProviderRef{id = 2}, - TerminalRef1 = #domain_TerminalRef{id = 11}, - TerminalRef2 = #domain_TerminalRef{id = 22}, - ProviderTerms1 = mk_provision_terms(50, 500, Currency), - ProviderTerms2 = mk_provision_terms(100, 700, Currency), - TerminalTerms1 = mk_provision_terms(200, 800, Currency), - TerminalTerms2 = mk_provision_terms(300, 900, Currency), - RulesetRef = #domain_RoutingRulesetRef{id = 999}, - Ruleset = #domain_RoutingRuleset{ - decisions = {candidates, [ - #domain_RoutingCandidate{allowed = {constant, true}, terminal = TerminalRef1}, - #domain_RoutingCandidate{allowed = {constant, true}, terminal = TerminalRef2} - ]} - }, - meck:expect(capi_party, get_shop, fun(_PartyID, _ShopID, _Context) -> {ok, Shop} end), - meck:expect(capi_domain, head, fun() -> 1 end), - meck:expect( - capi_domain, - get_ext, - fun({term_set_hierarchy, #domain_TermSetHierarchyRef{id = 20}}, 1, _Context) -> - {ok, #domain_TermSetHierarchy{term_set = ShopTerms}}; - (_, _, _) -> - {error, not_found} - end - ), - meck:expect( - capi_domain, - get_payment_institution, - fun(#domain_PaymentInstitutionRef{id = 88}, _Context) -> - {ok, #domain_PaymentInstitution{ - payment_routing_rules = #domain_RoutingRules{policies = RulesetRef} - }} - end - ), - meck:expect( - capi_domain, - get, - fun - ({routing_rules, #domain_RoutingRulesetRef{id = 999}}, _Context) -> - {ok, #domain_RoutingRulesObject{data = Ruleset}}; - ({terminal, #domain_TerminalRef{id = 11}}, _Context) -> - {ok, #domain_TerminalObject{data = #domain_Terminal{provider_ref = ProviderRef1, terms = TerminalTerms1}}}; - ({terminal, #domain_TerminalRef{id = 22}}, _Context) -> - {ok, #domain_TerminalObject{data = #domain_Terminal{provider_ref = ProviderRef2, terms = TerminalTerms2}}}; - ({provider, #domain_ProviderRef{id = 1}}, _Context) -> - {ok, #domain_ProviderObject{data = #domain_Provider{terms = ProviderTerms1}}}; - ({provider, #domain_ProviderRef{id = 2}}, _Context) -> - {ok, #domain_ProviderObject{data = #domain_Provider{terms = ProviderTerms2}}}; - (_, _Context) -> - {error, not_found} - end - ), - {ok, Result} = capi_cash_limits:get_shop_limits(<<"party">>, <<"shop">>, #{}), - Payment = maps:get(<<"payment">>, Result), - PartialRefund = maps:get(<<"partialRefund">>, Result), - ?assertEqual(Payment, PartialRefund), - ?assertEqual(<<"RUB">>, maps:get(<<"currency">>, Payment)), - ?assertEqual(#{<<"amount">> => 200, <<"inclusive">> => true}, maps:get(<<"lowerBound">>, Payment)), - ?assertEqual(#{<<"amount">> => 700, <<"inclusive">> => true}, maps:get(<<"upperBound">>, Payment)). - --spec real_config_limits_test() -> ok. -real_config_limits_test() -> - Currency = #domain_CurrencyRef{symbolic_code = <<"KZT">>}, - Shop = mk_shop(Currency, #domain_TermSetHierarchyRef{id = 1000}, #domain_PaymentInstitutionRef{id = 100}), - RulesetRef = #domain_RoutingRulesetRef{id = 1059}, - Ruleset = #domain_RoutingRuleset{ - decisions = {candidates, [ - #domain_RoutingCandidate{allowed = {constant, true}, terminal = #domain_TerminalRef{id = 15}}, - #domain_RoutingCandidate{allowed = {constant, true}, terminal = #domain_TerminalRef{id = 16}} - ]} - }, - ProviderTerms8 = #domain_ProvisionTermSet{ - payments = #domain_PaymentsProvisionTerms{ - cash_limit = mk_cash_limit_decisions([{100, 1000000000, Currency}]), - refunds = #domain_PaymentRefundsProvisionTerms{ - partial_refunds = #domain_PartialRefundsProvisionTerms{ - cash_limit = mk_cash_limit_decisions([{100, 1000000000, Currency}]) - } - } - } - }, - ProviderTerms9 = #domain_ProvisionTermSet{ - payments = #domain_PaymentsProvisionTerms{ - cash_limit = mk_cash_limit_value(100, 1000000000, Currency), - refunds = #domain_PaymentRefundsProvisionTerms{ - partial_refunds = #domain_PartialRefundsProvisionTerms{ - cash_limit = mk_cash_limit_value(100, 100000000, Currency) - } - } - } - }, - TerminalTerms15 = #domain_ProvisionTermSet{ - payments = #domain_PaymentsProvisionTerms{ - cash_limit = mk_cash_limit_value(10000, 120000000, Currency) - } - }, - TerminalTerms16 = #domain_ProvisionTermSet{ - payments = #domain_PaymentsProvisionTerms{ - cash_limit = mk_cash_limit_decisions([ - {51300, 43609100, Currency}, - {51300, 128262000, Currency} - ]) - } - }, - meck:expect(capi_party, get_shop, fun(_PartyID, _ShopID, _Context) -> {ok, Shop} end), - meck:expect(capi_domain, head, fun() -> 1 end), - meck:expect( - capi_domain, - get_ext, - fun({term_set_hierarchy, #domain_TermSetHierarchyRef{id = 1000}}, 1, _Context) -> - {error, not_found}; - (_, _, _) -> - {error, not_found} - end - ), - meck:expect( - capi_domain, - get_payment_institution, - fun(#domain_PaymentInstitutionRef{id = 100}, _Context) -> - {ok, #domain_PaymentInstitution{ - payment_routing_rules = #domain_RoutingRules{policies = RulesetRef} - }} - end - ), - meck:expect( - capi_domain, - get, - fun - ({routing_rules, #domain_RoutingRulesetRef{id = 1059}}, _Context) -> - {ok, #domain_RoutingRulesObject{data = Ruleset}}; - ({terminal, #domain_TerminalRef{id = 15}}, _Context) -> - {ok, #domain_TerminalObject{ - data = #domain_Terminal{provider_ref = #domain_ProviderRef{id = 8}, terms = TerminalTerms15} - }}; - ({terminal, #domain_TerminalRef{id = 16}}, _Context) -> - {ok, #domain_TerminalObject{ - data = #domain_Terminal{provider_ref = #domain_ProviderRef{id = 9}, terms = TerminalTerms16} - }}; - ({provider, #domain_ProviderRef{id = 8}}, _Context) -> - {ok, #domain_ProviderObject{data = #domain_Provider{terms = ProviderTerms8}}}; - ({provider, #domain_ProviderRef{id = 9}}, _Context) -> - {ok, #domain_ProviderObject{data = #domain_Provider{terms = ProviderTerms9}}}; - (_, _Context) -> - {error, not_found} - end - ), - {ok, Result} = capi_cash_limits:get_shop_limits(<<"party">>, <<"shop">>, #{}), - io:format("real_config_limits_result=~p~n", [Result]), - Payment = maps:get(<<"payment">>, Result), - PartialRefund = maps:get(<<"partialRefund">>, Result), - ?assertEqual(<<"KZT">>, maps:get(<<"currency">>, Payment)), - ?assertEqual(#{<<"amount">> => 10000, <<"inclusive">> => true}, maps:get(<<"lowerBound">>, Payment)), - ?assertEqual(#{<<"amount">> => 120000000, <<"inclusive">> => true}, maps:get(<<"upperBound">>, Payment)), - ?assertEqual(Payment, PartialRefund). - --spec mk_shop( - dmsl_domain_thrift:'CurrencyRef'(), - dmsl_domain_thrift:'TermSetHierarchyRef'(), - dmsl_domain_thrift:'PaymentInstitutionRef'() -) -> dmsl_domain_thrift:'ShopConfig'(). -mk_shop(Currency, TermsRef, PiRef) -> - #domain_ShopConfig{ - account = #domain_ShopAccount{currency = Currency}, - terms = TermsRef, - payment_institution = PiRef - }. - --spec mk_provision_terms( - integer(), - integer(), - dmsl_domain_thrift:'CurrencyRef'() -) -> dmsl_domain_thrift:'ProvisionTermSet'(). -mk_provision_terms(Lower, Upper, Currency) -> - #domain_ProvisionTermSet{ - payments = #domain_PaymentsProvisionTerms{ - cash_limit = {value, mk_cash_range(Lower, Upper, Currency)} - } - }. - --spec mk_cash_range( - integer(), - integer(), - dmsl_domain_thrift:'CurrencyRef'() -) -> dmsl_domain_thrift:'CashRange'(). -mk_cash_range(Lower, Upper, Currency) -> - #domain_CashRange{ - lower = {inclusive, #domain_Cash{amount = Lower, currency = Currency}}, - upper = {inclusive, #domain_Cash{amount = Upper, currency = Currency}} - }. - --spec mk_cash_limit_value(integer(), integer(), dmsl_domain_thrift:'CurrencyRef'()) -> - dmsl_domain_thrift:'CashLimitSelector'(). -mk_cash_limit_value(Lower, Upper, Currency) -> - {value, mk_cash_range(Lower, Upper, Currency)}. - --spec mk_cash_limit_decisions([{integer(), integer(), dmsl_domain_thrift:'CurrencyRef'()}]) -> - dmsl_domain_thrift:'CashLimitSelector'(). -mk_cash_limit_decisions(Ranges) -> - {decisions, [ - #domain_CashLimitDecision{ - if_ = {constant, true}, - then_ = mk_cash_limit_value(Lower, Upper, Currency) - } - || {Lower, Upper, Currency} <- Ranges - ]}. diff --git a/apps/capi/test/capi_dummy_data.hrl b/apps/capi/test/capi_dummy_data.hrl index a4e4cf1..8009d72 100644 --- a/apps/capi/test/capi_dummy_data.hrl +++ b/apps/capi/test/capi_dummy_data.hrl @@ -12,6 +12,7 @@ -define(STRING, <<"TEST">>). -define(RUB, <<"RUB">>). -define(USD, <<"USD">>). +-define(KZT, <<"KZT">>). -define(BANKID_RU, <<"PUTIN">>). -define(BANKID_US, <<"TRAMP">>). -define(WALLET_TOOL, <<"TOOL">>). @@ -30,6 +31,17 @@ -define(API_TOKEN, <<"letmein">>). -define(EMAIL, <<"test@test.ru">>). +-define(KZT_PARTY_ID, <<"95a158c2-343a-40c9-b690-247dbee3fa40">>). +-define(KZT_SHOP_ID, <<"bbe49f63-0ff8-4cc4-99e8-00892a683cec">>). +-define(KZT_PI_ID, 100). +-define(KZT_TERMS_ID, 1000). +-define(KZT_RULESET_ID, 1059). +-define(KZT_PROHIBITIONS_ID, 1060). +-define(KZT_PROVIDER_8_ID, 8). +-define(KZT_PROVIDER_9_ID, 9). +-define(KZT_TERMINAL_15_ID, 15). +-define(KZT_TERMINAL_16_ID, 16). + -define(RATIONAL, #base_Rational{p = ?INTEGER, q = ?INTEGER}). -define(DETAILS, #domain_InvoiceDetails{ @@ -749,6 +761,357 @@ }} } } + }}, + + {payment_institution, #domain_PaymentInstitutionRef{id = ?KZT_PI_ID}} => + {payment_institution, #domain_PaymentInstitutionObject{ + ref = #domain_PaymentInstitutionRef{id = ?KZT_PI_ID}, + data = #domain_PaymentInstitution{ + name = ?STRING, + description = ?STRING, + system_account_set = {value, #domain_SystemAccountSetRef{id = ?INTEGER}}, + inspector = {value, #domain_InspectorRef{id = ?INTEGER}}, + realm = test, + residences = [rus], + payment_routing_rules = #domain_RoutingRules{ + policies = #domain_RoutingRulesetRef{id = ?KZT_RULESET_ID}, + prohibitions = #domain_RoutingRulesetRef{id = ?KZT_PROHIBITIONS_ID} + } + } + }}, + + {routing_rules, #domain_RoutingRulesetRef{id = ?KZT_RULESET_ID}} => + {routing_rules, #domain_RoutingRulesObject{ + ref = #domain_RoutingRulesetRef{id = ?KZT_RULESET_ID}, + data = #domain_RoutingRuleset{ + name = ?STRING, + decisions = + {candidates, [ + #domain_RoutingCandidate{ + allowed = {constant, true}, + terminal = #domain_TerminalRef{id = ?KZT_TERMINAL_15_ID} + }, + #domain_RoutingCandidate{ + allowed = {constant, true}, + terminal = #domain_TerminalRef{id = ?KZT_TERMINAL_16_ID} + } + ]} + } + }}, + + {routing_rules, #domain_RoutingRulesetRef{id = ?KZT_PROHIBITIONS_ID}} => + {routing_rules, #domain_RoutingRulesObject{ + ref = #domain_RoutingRulesetRef{id = ?KZT_PROHIBITIONS_ID}, + data = #domain_RoutingRuleset{ + name = ?STRING, + decisions = {candidates, []} + } + }}, + + {provider, #domain_ProviderRef{id = ?KZT_PROVIDER_8_ID}} => + {provider, #domain_ProviderObject{ + ref = #domain_ProviderRef{id = ?KZT_PROVIDER_8_ID}, + data = #domain_Provider{ + name = ?STRING, + description = ?STRING, + proxy = #domain_Proxy{ref = #domain_ProxyRef{id = ?INTEGER}, additional = #{}}, + realm = test, + terms = + #domain_ProvisionTermSet{ + payments = + #domain_PaymentsProvisionTerms{ + payment_methods = + {value, [ + #domain_PaymentMethodRef{ + id = + {bank_card, #domain_BankCardPaymentMethod{ + payment_system = #domain_PaymentSystemRef{id = <<"MASTERCARD">>}, + is_cvv_empty = false + }} + }, + #domain_PaymentMethodRef{ + id = + {bank_card, #domain_BankCardPaymentMethod{ + payment_system = #domain_PaymentSystemRef{id = <<"VISA">>}, + is_cvv_empty = false + }} + } + ]}, + cash_limit = + {decisions, [ + #domain_CashLimitDecision{ + if_ = {constant, true}, + then_ = + {value, #domain_CashRange{ + lower = + {inclusive, #domain_Cash{ + amount = 100, + currency = + #domain_CurrencyRef{symbolic_code = ?KZT} + }}, + upper = + {inclusive, #domain_Cash{ + amount = 1000000000, + currency = + #domain_CurrencyRef{symbolic_code = ?KZT} + }} + }} + } + ]}, + refunds = + #domain_PaymentRefundsProvisionTerms{ + cash_flow = {value, []}, + partial_refunds = + #domain_PartialRefundsProvisionTerms{ + cash_limit = + {decisions, [ + #domain_CashLimitDecision{ + if_ = {constant, true}, + then_ = + {value, #domain_CashRange{ + lower = + {inclusive, #domain_Cash{ + amount = 100, + currency = + #domain_CurrencyRef{ + symbolic_code = ?KZT + } + }}, + upper = + {inclusive, #domain_Cash{ + amount = 1000000000, + currency = + #domain_CurrencyRef{ + symbolic_code = ?KZT + } + }} + }} + } + ]} + } + } + } + } + } + }}, + + {provider, #domain_ProviderRef{id = ?KZT_PROVIDER_9_ID}} => + {provider, #domain_ProviderObject{ + ref = #domain_ProviderRef{id = ?KZT_PROVIDER_9_ID}, + data = #domain_Provider{ + name = ?STRING, + description = ?STRING, + proxy = #domain_Proxy{ref = #domain_ProxyRef{id = ?INTEGER}, additional = #{}}, + realm = test, + terms = + #domain_ProvisionTermSet{ + payments = + #domain_PaymentsProvisionTerms{ + payment_methods = + {value, [ + #domain_PaymentMethodRef{ + id = + {bank_card, #domain_BankCardPaymentMethod{ + payment_system = #domain_PaymentSystemRef{id = <<"MASTERCARD">>}, + is_cvv_empty = false + }} + }, + #domain_PaymentMethodRef{ + id = + {bank_card, #domain_BankCardPaymentMethod{ + payment_system = #domain_PaymentSystemRef{id = <<"VISA">>}, + is_cvv_empty = false + }} + } + ]}, + cash_limit = + {value, #domain_CashRange{ + lower = + {inclusive, #domain_Cash{ + amount = 100, + currency = #domain_CurrencyRef{symbolic_code = ?KZT} + }}, + upper = + {inclusive, #domain_Cash{ + amount = 1000000000, + currency = #domain_CurrencyRef{symbolic_code = ?KZT} + }} + }}, + refunds = + #domain_PaymentRefundsProvisionTerms{ + cash_flow = {value, []}, + partial_refunds = + #domain_PartialRefundsProvisionTerms{ + cash_limit = + {value, #domain_CashRange{ + lower = + {inclusive, #domain_Cash{ + amount = 100, + currency = + #domain_CurrencyRef{ + symbolic_code = ?KZT + } + }}, + upper = + {inclusive, #domain_Cash{ + amount = 100000000, + currency = + #domain_CurrencyRef{ + symbolic_code = ?KZT + } + }} + }} + } + } + } + } + } + }}, + + {terminal, #domain_TerminalRef{id = ?KZT_TERMINAL_15_ID}} => + {terminal, #domain_TerminalObject{ + ref = #domain_TerminalRef{id = ?KZT_TERMINAL_15_ID}, + data = #domain_Terminal{ + name = ?STRING, + description = ?STRING, + provider_ref = #domain_ProviderRef{id = ?KZT_PROVIDER_8_ID}, + terms = + #domain_ProvisionTermSet{ + payments = + #domain_PaymentsProvisionTerms{ + payment_methods = + {value, [ + #domain_PaymentMethodRef{ + id = + {bank_card, #domain_BankCardPaymentMethod{ + payment_system = #domain_PaymentSystemRef{id = <<"MASTERCARD">>}, + is_cvv_empty = false + }} + }, + #domain_PaymentMethodRef{ + id = + {bank_card, #domain_BankCardPaymentMethod{ + payment_system = #domain_PaymentSystemRef{id = <<"VISA">>}, + is_cvv_empty = false + }} + } + ]}, + cash_limit = + {value, #domain_CashRange{ + lower = + {inclusive, #domain_Cash{ + amount = 10000, + currency = #domain_CurrencyRef{symbolic_code = ?KZT} + }}, + upper = + {inclusive, #domain_Cash{ + amount = 120000000, + currency = #domain_CurrencyRef{symbolic_code = ?KZT} + }} + }} + } + } + } + }}, + + {terminal, #domain_TerminalRef{id = ?KZT_TERMINAL_16_ID}} => + {terminal, #domain_TerminalObject{ + ref = #domain_TerminalRef{id = ?KZT_TERMINAL_16_ID}, + data = #domain_Terminal{ + name = ?STRING, + description = ?STRING, + provider_ref = #domain_ProviderRef{id = ?KZT_PROVIDER_9_ID}, + terms = + #domain_ProvisionTermSet{ + payments = + #domain_PaymentsProvisionTerms{ + cash_limit = + {decisions, [ + #domain_CashLimitDecision{ + if_ = {constant, true}, + then_ = + {value, #domain_CashRange{ + lower = + {inclusive, #domain_Cash{ + amount = 51300, + currency = + #domain_CurrencyRef{symbolic_code = ?KZT} + }}, + upper = + {inclusive, #domain_Cash{ + amount = 43609100, + currency = + #domain_CurrencyRef{symbolic_code = ?KZT} + }} + }} + }, + #domain_CashLimitDecision{ + if_ = {constant, true}, + then_ = + {value, #domain_CashRange{ + lower = + {inclusive, #domain_Cash{ + amount = 51300, + currency = + #domain_CurrencyRef{symbolic_code = ?KZT} + }}, + upper = + {inclusive, #domain_Cash{ + amount = 128262000, + currency = + #domain_CurrencyRef{symbolic_code = ?KZT} + }} + }} + } + ]} + } + } + } + }}, + + {term_set_hierarchy, #domain_TermSetHierarchyRef{id = ?KZT_TERMS_ID}} => + {term_set_hierarchy, #domain_TermSetHierarchyObject{ + ref = #domain_TermSetHierarchyRef{id = ?KZT_TERMS_ID}, + data = #domain_TermSetHierarchy{ + term_set = + #domain_TermSet{ + payments = + #domain_PaymentsServiceTerms{ + payment_methods = + {value, + ordsets:from_list([ + #domain_PaymentMethodRef{ + id = + {bank_card, #domain_BankCardPaymentMethod{ + payment_system = #domain_PaymentSystemRef{ + id = <<"VISA">> + } + }} + } + ])} + } + } + } + }}, + + {shop_config, #domain_ShopConfigRef{id = ?KZT_SHOP_ID}} => + {shop_config, #domain_ShopConfigObject{ + ref = #domain_ShopConfigRef{id = ?KZT_SHOP_ID}, + data = #domain_ShopConfig{ + name = ?STRING, + block = ?BLOCKING, + suspension = ?SUSPENTION, + payment_institution = #domain_PaymentInstitutionRef{id = ?KZT_PI_ID}, + terms = #domain_TermSetHierarchyRef{id = ?KZT_TERMS_ID}, + account = #domain_ShopAccount{ + currency = #domain_CurrencyRef{symbolic_code = ?KZT}, + settlement = ?INTEGER, + guarantee = ?INTEGER + }, + party_ref = #domain_PartyConfigRef{id = ?KZT_PARTY_ID}, + location = ?SHOP_LOCATION, + category = #domain_CategoryRef{id = ?INTEGER} + } }} }). From 58c228b105b3d6ece38d53b2cd1f05305b66ec54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Tue, 20 Jan 2026 18:38:47 +0300 Subject: [PATCH 5/5] clean up --- .cursorignore | 1 - capi_domain.34746.coverdata | Bin 3010 -> 0 bytes capi_domain.97223.coverdata | Bin 3010 -> 0 bytes capi_domain.97547.coverdata | Bin 3010 -> 0 bytes capi_party.34746.coverdata | Bin 839 -> 0 bytes capi_party.97223.coverdata | Bin 839 -> 0 bytes capi_party.97547.coverdata | Bin 839 -> 0 bytes mock data/provider_8 | 337 ------------------ mock data/provider_9 | 271 -------------- mock data/routing_rules_1059 | 43 --- ...ig_bbe49f63-0ff8-4cc4-99e8-00892a683cec(1) | 43 --- mock data/terminal_15 | 141 -------- mock data/terminal_16 | 141 -------- 13 files changed, 977 deletions(-) delete mode 100644 .cursorignore delete mode 100644 capi_domain.34746.coverdata delete mode 100644 capi_domain.97223.coverdata delete mode 100644 capi_domain.97547.coverdata delete mode 100644 capi_party.34746.coverdata delete mode 100644 capi_party.97223.coverdata delete mode 100644 capi_party.97547.coverdata delete mode 100644 mock data/provider_8 delete mode 100644 mock data/provider_9 delete mode 100644 mock data/routing_rules_1059 delete mode 100644 mock data/shop_config_bbe49f63-0ff8-4cc4-99e8-00892a683cec(1) delete mode 100644 mock data/terminal_15 delete mode 100644 mock data/terminal_16 diff --git a/.cursorignore b/.cursorignore deleted file mode 100644 index 8dc1bb0..0000000 --- a/.cursorignore +++ /dev/null @@ -1 +0,0 @@ -!_build/** \ No newline at end of file diff --git a/capi_domain.34746.coverdata b/capi_domain.34746.coverdata deleted file mode 100644 index 7473a38c0644bee5c0b4768e1f202227135e7dba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3010 zcmbVOOK8+U7@mFDqE#siI=4~F*D@)&3ym&$e-wyA%u`MdT;Q< z;Ic}7^?v9r4PQEHubA7jkZ+yu-}p1gjUE40=)1q+$8_o2Oy{-VPd`^%s_*(&zFu2> zUw{F1c3~plSUWViY3t<0@!XfbdH3AN`F&U4%r@8N*3;|Xw-2u~rqHLtmakova=P5$ zXBE$|{U#M`4Hqo~G}CV`5sH0{ma#*0pri;HNWld_F-JVpcfiJ{h~sLO86HW6&qqL= zT$bfzlsceNOaq%l@P~jpxbP-os#cL;VtPh2&jUc+T=Xg?!BS2TmAY67{&J*4mX{(U zHNdl2(f{=6tdNSEfO1?FnUEmTiev9&W2lyAQYQeW@vw--CGaHn@_O6Z{0_x5^UL&RWuE64!+C$AWza4^+cs&^4ymTU0My16*~mnOpNwc9j)?d6N}@%3O#I?ABArdc zBl$cAjPQb2d3 lZ89NG(j8Ay_l}zKBzqH+82>eu{j(fNyvtKilOMYa&>y`ZC;k8c diff --git a/capi_domain.97223.coverdata b/capi_domain.97223.coverdata deleted file mode 100644 index 211e1322f4328f3a99c51d44e471749edfbb3a32..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3010 zcmd044q#wl$gh~=d&ZZm*+9fK(J?)?JFbBDuG32cm1kb^KPy!`(|!lJ&z1XeF8$4A zy@`J}-dr!S{-hV*Vd?d!f)c%xZ@t`VztvYGP%1LZUkw()@#mS%3v#JNh-}PDCbU2EXa&c$a(+r`d}>~4E?D)fL|r;&PZf1#c8H6$i4YVS*gh-#qmj%kf;I$ z_+HWjya9g@=Hw?O<`gF~01e-o$Y6!jNfIC@VMzzYK#h!vR%H3+Ao&)0lN8vqiHg7^ z@||N$zE(T3f^HA#389fJi*^zn(4h1=Igx?*l7}U69VyD$5;qa0JSDLt5u7MX$&VBx z{Fw%++{q=%P0+HIHE}~CgEr1Q#+jR#of@BCPy)(SOo_+IcfT{i1P_X>Xrkj5RC3S6 zs~Ww84I|p4jEP6d_oyS@d{CNKUX)l6UzS=_46L$%H4vz*I!j87F(|#{qon*HgbV;|YDUm@9XU1X4NKH%u*I_E8o7_i!1bdK@ xl$a7%k#CbJ>Hg{=DZd7i?>1|q6Ko+-Nih+x!_gaO^YN-iFP4JHPP5vH3;+NOC;k8c diff --git a/capi_domain.97547.coverdata b/capi_domain.97547.coverdata deleted file mode 100644 index c6c24c66a7b12ae5ec3fa7b0a017576e34291ccb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3010 zcmbVOO-L0{6u$2#rK}JP68c-@i$s){A(cU3KNyN&FK81so;P!QZ)*OWxzjki>Jeo{ zQ9<-e+lcU0Ad=J~;uS@?X;XsGN|4&LD6?9qDz z9|l%b@@o!2Pig4VF=y56-uZmXT;HZ&VQ%dB??UhWjX$SL-#>L+`}6cmwYmDPZ}sbS z)%OMHS7+uY^7XaDlbg3qUL4PT?Va<^jhx?q_03FUZFU2>{$t0`dUFbWE^PhQUMZ)` zbwRf1n@-T6f}<;%ZGvtE4JAwz)%GpohLDC76aeLz=nO?ehN1vaHxtzy^>iRKu|*W! z@`z4+qT7~73scYosDoL$5|0uWRHBg3!4#Z^M_ha`piXAf#)wa#ld|Gs0|?gQ6~@F% zuE=k_50HQfhs3#ENF0DI)z+|rE#Hh?zQK)-+`bKH7gOD}47cMsZHbHcR^Wo8oWib0 z<3W9i^6?Q+8*|ul6Ha2qcF01OY9d)$22T>%{5K_Mg@n4p%{>ioxq>|msFkTkeaWzk zV0eJ2ZWCPnih#B-`(?`*w76Ldpe`n?DN@QVp1>;clw!E>twy)wg;dom`LAlbkUO{J z2X)Dp9$eCwNV=o)13D8$h`d^S-v^~Unv$p2Eky5#{Gfi3V~~NzBJetR#s$}a0Ch73 z{*taohgFe2tgVTvEFirs z?wk({{wd!tq7kXpoWYdAR?d=Cnp;rLm7G|R8DEfCR8m>anVwn_U!0L&P@Kq=$e3uF z$e@W!DMxZfYI1gdX-Oh8P^ALVD&-Oxh_&9BXhUW2sf76n>`)D&RjLzmC^*Q#;Uq$| zp=v~{)J$YB#ufHLATNQfk1t9s%Ph{!&r4(g#)1SPCnKU(pLnHuM7vp)ctb6TS87hY IQo}?B01j6sJ^%m! diff --git a/capi_party.97223.coverdata b/capi_party.97223.coverdata deleted file mode 100644 index add60e685248ae8af70d038b0cfb911a294c6d73..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 839 zcmd054q#wl$gP;;+T+dDY#_nLy8Bh^7v}R1L~pHC`6yifX_Adp)$cHKz4U~%_qUUU zkAE|HHQ{N%%9`Bo$Fz-8|8&+pmJ)96ow0c3yoD)SX6T$t`r#8%BD#H%fw;xR>Firs z?wk({{wd!tq7kXpoWYdAR?d=Cnp;rLm7G|R8DEfCR8m>anVwn_U!0L&P@Kq=$e5^? z$e@W!DMxZfYI1gdX-Oh8P^Cg5gE20ZLLfuI=EoPMmSq-a=I13c0QE>DG7#$!ed3iG z6R*?|pZ8!P0rs9cAw$951BaOiA*G1mH6>oDCDBgRAX=p=@z$FYZKxVPl_*I@4xdVt O(AUJL62(lJLFirs z?wk({{wd!tq7kXtoWYdAR?d=Cnp;rLm7G|R8DEfCR8m>ak(`m5oSk1&IHN=`TKL