From dcf72452de2ef96c35d13c090d484211370fe5b7 Mon Sep 17 00:00:00 2001 From: Aleksey Kashapov Date: Mon, 17 Jun 2024 15:37:15 +0300 Subject: [PATCH 1/9] OPS-474: Adds support for amount randomization invoice mutation --- apps/hellgate/src/hg_invoice.erl | 44 +++++++++++++++++++++-- apps/hellgate/src/hg_invoice_handler.erl | 14 ++++++-- apps/hellgate/src/hg_invoice_template.erl | 15 ++++---- apps/hellgate/src/hg_invoice_utils.erl | 23 ++++++++++++ apps/hellgate/test/hg_ct_helper.erl | 2 +- rebar.config | 2 +- rebar.lock | 2 +- 7 files changed, 87 insertions(+), 15 deletions(-) diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index fedb9379..e7e46dbf 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -132,7 +132,7 @@ create(ID, InvoiceTplID, PartyRevision, V = #payproc_InvoiceParams{}, Allocation OwnerID = V#payproc_InvoiceParams.party_id, ShopID = V#payproc_InvoiceParams.shop_id, Cost = V#payproc_InvoiceParams.cost, - #domain_Invoice{ + apply_mutations(V#payproc_InvoiceParams.mutations, #domain_Invoice{ id = ID, shop_id = ShopID, owner_id = OwnerID, @@ -147,7 +147,47 @@ create(ID, InvoiceTplID, PartyRevision, V = #payproc_InvoiceParams{}, Allocation external_id = V#payproc_InvoiceParams.external_id, client_info = V#payproc_InvoiceParams.client_info, allocation = Allocation - }. + }). + +apply_mutations(MutationsParams, Invoice) -> + lists:foldl(fun apply_mutation/2, Invoice, MutationsParams). + +-define(SATISFY_RANDOMIZATION_CONDITION(P, Amount), + %% Multiplicity check + (P#domain_RandomizationMutationParams.amount_multiplicity_condition =:= undefined orelse + Amount rem P#domain_RandomizationMutationParams.amount_multiplicity_condition =:= 0) andalso + %% Min amount + (P#domain_RandomizationMutationParams.min_amount_condition =:= undefined orelse + P#domain_RandomizationMutationParams.min_amount_condition =< Amount) andalso + %% Max amount + (P#domain_RandomizationMutationParams.max_amount_condition =:= undefined orelse + P#domain_RandomizationMutationParams.max_amount_condition >= Amount) +). + +apply_mutation( + {amount, + {randomization, + Params = #domain_RandomizationMutationParams{ + deviation = MaxDeviation, + precision = Precision, + rounding = Rounding + }}}, + Invoice = #domain_Invoice{cost = Cost = #domain_Cash{amount = Amount}} +) when ?SATISFY_RANDOMIZATION_CONDITION(Params, Amount) -> + RoundingFun = + case Rounding of + round_half_towards_zero -> fun round/1; + round_half_away_from_zero -> fun round/1; + round_down -> fun floor/1; + round_up -> fun ceil/1 + end, + PrecisionFactor = trunc(math:pow(10, Precision)), + Deviation0 = rand:uniform(MaxDeviation + 1) - 1, + Deviation1 = RoundingFun(Deviation0 / PrecisionFactor) * PrecisionFactor, + Sign = trunc(math:pow(-1, rand:uniform(2))), + Invoice#domain_Invoice{cost = Cost#domain_Cash{amount = Amount + Sign * Deviation1}}; +apply_mutation(_, Invoice) -> + Invoice. %%----------------- invoice asserts assert_invoice(Checks, #st{} = St) when is_list(Checks) -> diff --git a/apps/hellgate/src/hg_invoice_handler.erl b/apps/hellgate/src/hg_invoice_handler.erl index bc75807c..a346e6ad 100644 --- a/apps/hellgate/src/hg_invoice_handler.erl +++ b/apps/hellgate/src/hg_invoice_handler.erl @@ -40,6 +40,8 @@ handle_function_('Create', {InvoiceParams}, _Opts) -> Party = hg_party:get_party(PartyID), Shop = assert_shop_exists(hg_party:get_shop(ShopID, Party)), _ = assert_party_shop_operable(Shop, Party), + %% FIXME Should cost amount to be mutated before calculating + %% merchant terms and allocation? VS = #{ cost => InvoiceParams#payproc_InvoiceParams.cost, shop_id => Shop#domain_Shop.id @@ -337,7 +339,8 @@ make_invoice_params(Params) -> product = Product, description = Description, details = TplDetails, - context = TplContext + context = TplContext, + mutations = MutationsParams } = hg_invoice_template:get(TplID), Party = hg_party:get_party(PartyID), Shop = assert_shop_exists(hg_party:get_shop(ShopID, Party)), @@ -359,14 +362,19 @@ make_invoice_params(Params) -> due = InvoiceDue, cost = InvoiceCost, context = InvoiceContext, - external_id = ExternalID + external_id = ExternalID, + mutations = MutationsParams }, {Party, Shop, InvoiceParams}. -validate_invoice_params(#payproc_InvoiceParams{cost = Cost}, Shop, MerchantTerms) -> +validate_invoice_params(#payproc_InvoiceParams{cost = Cost} = InvoiceParams, Shop, MerchantTerms) -> ok = validate_invoice_cost(Cost, Shop, MerchantTerms), + ok = validate_mutations(InvoiceParams), ok. +validate_mutations(#payproc_InvoiceParams{mutations = Mutations, details = Details}) -> + hg_invoice_utils:validate_mutations(Mutations, Details). + validate_invoice_cost(Cost, Shop, #domain_TermSet{payments = PaymentTerms}) -> _ = hg_invoice_utils:validate_cost(Cost, Shop), _ = hg_invoice_utils:assert_cost_payable(Cost, PaymentTerms), diff --git a/apps/hellgate/src/hg_invoice_template.erl b/apps/hellgate/src/hg_invoice_template.erl index b2bc83a0..fb002076 100644 --- a/apps/hellgate/src/hg_invoice_template.erl +++ b/apps/hellgate/src/hg_invoice_template.erl @@ -2,6 +2,7 @@ -module(hg_invoice_template). +-include_lib("damsel/include/dmsl_base_thrift.hrl"). -include_lib("damsel/include/dmsl_domain_thrift.hrl"). -include_lib("damsel/include/dmsl_payproc_thrift.hrl"). @@ -115,17 +116,17 @@ get_shop(ShopID, Party) -> set_meta(ID) -> scoper:add_meta(#{invoice_template_id => ID}). -validate_create_params(#payproc_InvoiceTemplateCreateParams{details = Details}, Shop) -> - ok = validate_details(Details, Shop). +validate_create_params(#payproc_InvoiceTemplateCreateParams{details = Details, mutations = Mutations}, Shop) -> + ok = validate_details(Details, Mutations, Shop). validate_update_params(#payproc_InvoiceTemplateUpdateParams{details = undefined}, _) -> ok; -validate_update_params(#payproc_InvoiceTemplateUpdateParams{details = Details}, Shop) -> - ok = validate_details(Details, Shop). +validate_update_params(#payproc_InvoiceTemplateUpdateParams{details = Details, mutations = Mutations}, Shop) -> + ok = validate_details(Details, Mutations, Shop). -validate_details({cart, #domain_InvoiceCart{}}, _) -> - ok; -validate_details({product, #domain_InvoiceTemplateProduct{price = Price}}, Shop) -> +validate_details({cart, #domain_InvoiceCart{}} = Details, Mutations, _) -> + hg_invoice_utils:validate_mutations(Mutations, Details); +validate_details({product, #domain_InvoiceTemplateProduct{price = Price}}, _, Shop) -> validate_price(Price, Shop). validate_price({fixed, Cash}, Shop) -> diff --git a/apps/hellgate/src/hg_invoice_utils.erl b/apps/hellgate/src/hg_invoice_utils.erl index 9b37ffe0..ab243179 100644 --- a/apps/hellgate/src/hg_invoice_utils.erl +++ b/apps/hellgate/src/hg_invoice_utils.erl @@ -10,6 +10,7 @@ -export([validate_cost/2]). -export([validate_currency/2]). -export([validate_cash_range/1]). +-export([validate_mutations/2]). -export([assert_party_operable/1]). -export([assert_shop_exists/1]). -export([assert_shop_operable/1]). @@ -36,6 +37,9 @@ -type timestamp() :: dmsl_base_thrift:'Timestamp'(). -type party_revision_param() :: dmsl_payproc_thrift:'PartyRevisionParam'(). -type varset() :: dmsl_payproc_thrift:'ComputeShopTermsVarset'(). +-type invoice_details() :: dmsl_domain_thrift:'InvoiceDetails'(). +-type invoice_template_details() :: dmsl_domain_thrift:'InvoiceTemplateDetails'(). +-type invoice_mutation() :: dmsl_payproc_thrift:'InvoiceMutationParams'(). -spec validate_cost(cash(), shop()) -> ok. validate_cost(#domain_Cash{currency = Currency, amount = Amount}, Shop) -> @@ -66,6 +70,25 @@ validate_cash_range(#domain_CashRange{ validate_cash_range(_) -> throw(#base_InvalidRequest{errors = [<<"Invalid cost range">>]}). +-spec validate_mutations([invoice_mutation()], invoice_details() | invoice_template_details()) -> ok. +validate_mutations(Mutations, #domain_InvoiceDetails{cart = Cart}) -> + validate_mutations_w_cart(Mutations, Cart); +validate_mutations(Mutations, {cart, #domain_InvoiceCart{} = Cart}) -> + validate_mutations_w_cart(Mutations, Cart); +validate_mutations(_Mutations, _Details) -> + ok. + +validate_mutations_w_cart(Mutations, #domain_InvoiceCart{lines = Lines}) -> + lists:any( + fun + ({amount, _}) -> true; + (_) -> false + end, + Mutations + ) andalso length(Lines) > 1 andalso + throw(#base_InvalidRequest{errors = [<<"Amount mutation with multiline cart is not allowed">>]}), + ok. + -spec assert_party_operable(party()) -> party(). assert_party_operable(V) -> _ = assert_party_unblocked(V), diff --git a/apps/hellgate/test/hg_ct_helper.erl b/apps/hellgate/test/hg_ct_helper.erl index fc7266a8..89a70d68 100644 --- a/apps/hellgate/test/hg_ct_helper.erl +++ b/apps/hellgate/test/hg_ct_helper.erl @@ -480,7 +480,7 @@ make_shop_params(Category, ContractID, PayoutToolID) -> make_party_params() -> #payproc_PartyParams{ contact_info = #domain_PartyContactInfo{ - email = <> + registration_email = <> } }. diff --git a/rebar.config b/rebar.config index edeeb8c4..4af1e43e 100644 --- a/rebar.config +++ b/rebar.config @@ -30,7 +30,7 @@ {gproc, "0.9.0"}, {genlib, {git, "https://github.com/valitydev/genlib.git", {branch, "master"}}}, {woody, {git, "https://github.com/valitydev/woody_erlang.git", {branch, "master"}}}, - {damsel, {git, "https://github.com/valitydev/damsel.git", {branch, "master"}}}, + {damsel, {git, "https://github.com/valitydev/damsel.git", {branch, "OPS-474/ft/invoice-randomization"}}}, {payproc_errors, {git, "https://github.com/valitydev/payproc-errors-erlang.git", {branch, "master"}}}, {mg_proto, {git, "https://github.com/valitydev/machinegun-proto.git", {branch, "master"}}}, {dmt_client, {git, "https://github.com/valitydev/dmt-client.git", {branch, "master"}}}, diff --git a/rebar.lock b/rebar.lock index aa5cc9b5..b37ce384 100644 --- a/rebar.lock +++ b/rebar.lock @@ -21,7 +21,7 @@ {<<"ctx">>,{pkg,<<"ctx">>,<<"0.6.0">>},2}, {<<"damsel">>, {git,"https://github.com/valitydev/damsel.git", - {ref,"b04aba83100a4d0adc19b5797372970fd632f911"}}, + {ref,"73b65efbe017ba541802344afef8c9c184185b81"}}, 0}, {<<"dmt_client">>, {git,"https://github.com/valitydev/dmt-client.git", From 87ac0280d9b024c1ebd11a52971a053df087e187 Mon Sep 17 00:00:00 2001 From: Aleksey Kashapov Date: Mon, 17 Jun 2024 18:14:40 +0300 Subject: [PATCH 2/9] Bumps dmt_client and party_client --- rebar.config | 3 ++- rebar.lock | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/rebar.config b/rebar.config index 4af1e43e..c0d184ad 100644 --- a/rebar.config +++ b/rebar.config @@ -35,7 +35,8 @@ {mg_proto, {git, "https://github.com/valitydev/machinegun-proto.git", {branch, "master"}}}, {dmt_client, {git, "https://github.com/valitydev/dmt-client.git", {branch, "master"}}}, {scoper, {git, "https://github.com/valitydev/scoper.git", {branch, "master"}}}, - {party_client, {git, "https://github.com/valitydev/party-client-erlang.git", {branch, "master"}}}, + {party_client, + {git, "https://github.com/valitydev/party-client-erlang.git", {branch, "OPS-474/fx/fix-damsel-compat"}}}, {bender_client, {git, "https://github.com/valitydev/bender-client-erlang.git", {branch, "master"}}}, {erl_health, {git, "https://github.com/valitydev/erlang-health.git", {branch, "master"}}}, {fault_detector_proto, {git, "https://github.com/valitydev/fault-detector-proto.git", {branch, "master"}}}, diff --git a/rebar.lock b/rebar.lock index b37ce384..131520b4 100644 --- a/rebar.lock +++ b/rebar.lock @@ -25,11 +25,11 @@ 0}, {<<"dmt_client">>, {git,"https://github.com/valitydev/dmt-client.git", - {ref,"b8bc0281dbf1e55a1a67ef6da861e0353ff14913"}}, + {ref,"d8a4f490d49c038d96f1cbc2a279164c6f4039f9"}}, 0}, {<<"dmt_core">>, {git,"https://github.com/valitydev/dmt-core.git", - {ref,"75841332fe0b40a77da0c12ea8d5dbb994da8e82"}}, + {ref,"19d8f57198f2cbe5b64aa4a923ba32774e505503"}}, 1}, {<<"erl_health">>, {git,"https://github.com/valitydev/erlang-health.git", @@ -51,7 +51,7 @@ {<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},1}, {<<"limiter_proto">>, {git,"https://github.com/valitydev/limiter-proto.git", - {ref,"e045813d32e67432e5592d582e59e45df05da647"}}, + {ref,"10328404f1cea68586962ed7fce0405b18d62b28"}}, 0}, {<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},2}, {<<"mg_proto">>, @@ -74,7 +74,7 @@ {<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.3.1">>},2}, {<<"party_client">>, {git,"https://github.com/valitydev/party-client-erlang.git", - {ref,"38c7782286877a63087c19de49f26ab175a37de7"}}, + {ref,"108cae6434753b626a46f5a5aa549a2a605ff9da"}}, 0}, {<<"payproc_errors">>, {git,"https://github.com/valitydev/payproc-errors-erlang.git", From ec5126ecbb48af7eeb7bdb4f92ecfa19bbccb400 Mon Sep 17 00:00:00 2001 From: Aleksey Kashapov Date: Tue, 18 Jun 2024 11:01:45 +0300 Subject: [PATCH 3/9] Completes amount mutation with cart's line price and adds unit tests --- apps/hellgate/src/hg_invoice.erl | 121 ++++++++++++++++++++++++- apps/hellgate/src/hg_invoice_utils.erl | 14 ++- rebar.config | 3 +- rebar.lock | 2 +- 4 files changed, 132 insertions(+), 8 deletions(-) diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index e7e46dbf..bd1ad1f8 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -164,6 +164,7 @@ apply_mutations(MutationsParams, Invoice) -> P#domain_RandomizationMutationParams.max_amount_condition >= Amount) ). +%% FIXME Split into smaller and cleaner funcs apply_mutation( {amount, {randomization, @@ -172,7 +173,11 @@ apply_mutation( precision = Precision, rounding = Rounding }}}, - Invoice = #domain_Invoice{cost = Cost = #domain_Cash{amount = Amount}} + Invoice = #domain_Invoice{ + cost = Cost = #domain_Cash{amount = Amount}, + mutations = Mutations, + details = Details = #domain_InvoiceDetails{cart = Cart = #domain_InvoiceCart{lines = [Line]}} + } ) when ?SATISFY_RANDOMIZATION_CONDITION(Params, Amount) -> RoundingFun = case Rounding of @@ -185,7 +190,15 @@ apply_mutation( Deviation0 = rand:uniform(MaxDeviation + 1) - 1, Deviation1 = RoundingFun(Deviation0 / PrecisionFactor) * PrecisionFactor, Sign = trunc(math:pow(-1, rand:uniform(2))), - Invoice#domain_Invoice{cost = Cost#domain_Cash{amount = Amount + Sign * Deviation1}}; + NewAmount = Amount + Sign * Deviation1, + NewMutation = {amount, #domain_InvoiceAmountMutation{original = Amount, mutated = NewAmount}}, + NewLines = [Line#domain_InvoiceLine{price = (Line#domain_InvoiceLine.price)#domain_Cash{amount = NewAmount}}], + NewCart = Cart#domain_InvoiceCart{lines = NewLines}, + Invoice#domain_Invoice{ + cost = Cost#domain_Cash{amount = NewAmount}, + mutations = [NewMutation | genlib:define(Mutations, [])], + details = Details#domain_InvoiceDetails{cart = NewCart} + }; apply_mutation(_, Invoice) -> Invoice. @@ -1041,4 +1054,108 @@ construct_refund_id_test() -> Refunds = lists:map(fun create_dummy_refund_with_id/1, IDs), ?assert(<<"11">> =:= construct_refund_id(Refunds)). +-define(mutations(Deviation, Precision, Rounding, Min, Max, Multiplicity), [ + {amount, + {randomization, #domain_RandomizationMutationParams{ + deviation = Deviation, + precision = Precision, + rounding = Rounding, + min_amount_condition = Min, + max_amount_condition = Max, + amount_multiplicity_condition = Multiplicity + }}} +]). + +-define(currency(), #domain_Currency{ + name = <<"RUB">>, + numeric_code = 666, + symbolic_code = <<"RUB">>, + exponent = 2 +}). + +-define(invoice(Amount, Lines, Mutations), #domain_Invoice{ + id = <<"invoice">>, + shop_id = <<"shop_id">>, + owner_id = <<"owner_id">>, + created_at = <<"1970-01-01T00:00:00Z">>, + status = ?invoice_unpaid(), + cost = #domain_Cash{ + amount = Amount, + currency = ?currency() + }, + due = <<"1970-01-01T00:00:00Z">>, + details = #domain_InvoiceDetails{cart = #domain_InvoiceCart{lines = Lines}}, + mutations = Mutations +}). + +-define(mutated_invoice(OriginalAmount, MutatedAmount, Lines), + ?invoice(MutatedAmount, Lines, [ + {amount, #domain_InvoiceAmountMutation{original = OriginalAmount, mutated = MutatedAmount}} + ]) +). + +-define(not_mutated_invoice(Amount, Lines), ?invoice(Amount, Lines, undefined)). + +-define(cart_line(Price), #domain_InvoiceLine{ + product = <<"product">>, + quantity = 1, + price = #domain_Cash{amount = Price, currency = ?currency()}, + metadata = #{} +}). + +-spec apply_mutations_test_() -> [_TestGen]. +apply_mutations_test_() -> + lists:flatten([ + %% Didn't mutate because of conditions + ?_assertEqual( + ?not_mutated_invoice(1000_00, [?cart_line(1000_00)]), + apply_mutations( + ?mutations(100_00, 2, round_half_away_from_zero, 0, 100_00, 1_00), + ?not_mutated_invoice(1000_00, [?cart_line(1000_00)]) + ) + ), + ?_assertEqual( + ?not_mutated_invoice(1234_00, [?cart_line(1234_00)]), + apply_mutations( + ?mutations(100_00, 2, round_half_away_from_zero, 0, 1000_00, 7_00), + ?not_mutated_invoice(1234_00, [?cart_line(1234_00)]) + ) + ), + + %% No deviation, stil did mutate, but amount is same + ?_assertEqual( + ?mutated_invoice(100_00, 100_00, [?cart_line(100_00)]), + apply_mutations( + ?mutations(0, 2, round_half_away_from_zero, 0, 1000_00, 1_00), + ?not_mutated_invoice(100_00, [?cart_line(100_00)]) + ) + ), + + %% Deviate only with 2 other possible values + [ + ?_assertMatch( + ?mutated_invoice(100_00, A, [?cart_line(A)]) when + A =:= 0 orelse A =:= 100_00 orelse A =:= 200_00, + apply_mutations( + Mutations, + ?not_mutated_invoice(100_00, [?cart_line(100_00)]) + ) + ) + || Mutations <- lists:duplicate(10, ?mutations(100_00, 4, round_half_away_from_zero, 0, 1000_00, 1_00)) + ], + + %% Deviate in segment [900_00, 1100_00] without minor units + [ + ?_assertMatch( + ?mutated_invoice(1000_00, A, [?cart_line(A)]) when + A >= 900_00 andalso A =< 1100_00 andalso A rem 100 =:= 0, + apply_mutations( + Mutations, + ?not_mutated_invoice(1000_00, [?cart_line(1000_00)]) + ) + ) + || Mutations <- lists:duplicate(10, ?mutations(100_00, 2, round_half_away_from_zero, 0, 1000_00, 1_00)) + ] + ]). + -endif. diff --git a/apps/hellgate/src/hg_invoice_utils.erl b/apps/hellgate/src/hg_invoice_utils.erl index ab243179..e90e755f 100644 --- a/apps/hellgate/src/hg_invoice_utils.erl +++ b/apps/hellgate/src/hg_invoice_utils.erl @@ -79,15 +79,23 @@ validate_mutations(_Mutations, _Details) -> ok. validate_mutations_w_cart(Mutations, #domain_InvoiceCart{lines = Lines}) -> + amount_mutation_is_present(Mutations) andalso cart_is_valid_for_mutation(Lines) andalso + throw(#base_InvalidRequest{ + errors = [<<"Amount mutation with multiline cart or multiple items in a line is not allowed">>] + }), + ok. + +amount_mutation_is_present(Mutations) -> lists:any( fun ({amount, _}) -> true; (_) -> false end, Mutations - ) andalso length(Lines) > 1 andalso - throw(#base_InvalidRequest{errors = [<<"Amount mutation with multiline cart is not allowed">>]}), - ok. + ). + +cart_is_valid_for_mutation(Lines) -> + length(Lines) > 1 orelse (hd(Lines))#domain_InvoiceLine.quantity =/= 1. -spec assert_party_operable(party()) -> party(). assert_party_operable(V) -> diff --git a/rebar.config b/rebar.config index c0d184ad..4af1e43e 100644 --- a/rebar.config +++ b/rebar.config @@ -35,8 +35,7 @@ {mg_proto, {git, "https://github.com/valitydev/machinegun-proto.git", {branch, "master"}}}, {dmt_client, {git, "https://github.com/valitydev/dmt-client.git", {branch, "master"}}}, {scoper, {git, "https://github.com/valitydev/scoper.git", {branch, "master"}}}, - {party_client, - {git, "https://github.com/valitydev/party-client-erlang.git", {branch, "OPS-474/fx/fix-damsel-compat"}}}, + {party_client, {git, "https://github.com/valitydev/party-client-erlang.git", {branch, "master"}}}, {bender_client, {git, "https://github.com/valitydev/bender-client-erlang.git", {branch, "master"}}}, {erl_health, {git, "https://github.com/valitydev/erlang-health.git", {branch, "master"}}}, {fault_detector_proto, {git, "https://github.com/valitydev/fault-detector-proto.git", {branch, "master"}}}, diff --git a/rebar.lock b/rebar.lock index 131520b4..12fe11a5 100644 --- a/rebar.lock +++ b/rebar.lock @@ -74,7 +74,7 @@ {<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.3.1">>},2}, {<<"party_client">>, {git,"https://github.com/valitydev/party-client-erlang.git", - {ref,"108cae6434753b626a46f5a5aa549a2a605ff9da"}}, + {ref,"a82682b6f55f41ff4962b2666bbd12cb5f1ece25"}}, 0}, {<<"payproc_errors">>, {git,"https://github.com/valitydev/payproc-errors-erlang.git", From 979beb1eb6d3cf17fe0fdea9f80f66d514c01995 Mon Sep 17 00:00:00 2001 From: Aleksey Kashapov Date: Tue, 18 Jun 2024 12:01:00 +0300 Subject: [PATCH 4/9] Refactors amount mutation func --- apps/hellgate/src/hg_invoice.erl | 91 ++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 40 deletions(-) diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index bd1ad1f8..1cdab897 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -164,44 +164,57 @@ apply_mutations(MutationsParams, Invoice) -> P#domain_RandomizationMutationParams.max_amount_condition >= Amount) ). -%% FIXME Split into smaller and cleaner funcs apply_mutation( - {amount, - {randomization, - Params = #domain_RandomizationMutationParams{ - deviation = MaxDeviation, - precision = Precision, - rounding = Rounding - }}}, - Invoice = #domain_Invoice{ - cost = Cost = #domain_Cash{amount = Amount}, - mutations = Mutations, - details = Details = #domain_InvoiceDetails{cart = Cart = #domain_InvoiceCart{lines = [Line]}} - } + {amount, {randomization, Params = #domain_RandomizationMutationParams{}}}, + Invoice = #domain_Invoice{cost = #domain_Cash{amount = Amount}} ) when ?SATISFY_RANDOMIZATION_CONDITION(Params, Amount) -> - RoundingFun = - case Rounding of - round_half_towards_zero -> fun round/1; - round_half_away_from_zero -> fun round/1; - round_down -> fun floor/1; - round_up -> fun ceil/1 - end, - PrecisionFactor = trunc(math:pow(10, Precision)), - Deviation0 = rand:uniform(MaxDeviation + 1) - 1, - Deviation1 = RoundingFun(Deviation0 / PrecisionFactor) * PrecisionFactor, - Sign = trunc(math:pow(-1, rand:uniform(2))), - NewAmount = Amount + Sign * Deviation1, - NewMutation = {amount, #domain_InvoiceAmountMutation{original = Amount, mutated = NewAmount}}, - NewLines = [Line#domain_InvoiceLine{price = (Line#domain_InvoiceLine.price)#domain_Cash{amount = NewAmount}}], - NewCart = Cart#domain_InvoiceCart{lines = NewLines}, - Invoice#domain_Invoice{ - cost = Cost#domain_Cash{amount = NewAmount}, - mutations = [NewMutation | genlib:define(Mutations, [])], - details = Details#domain_InvoiceDetails{cart = NewCart} - }; + update_invoice_cost(Amount, calc_new_amount(Amount, Params), Invoice); apply_mutation(_, Invoice) -> Invoice. +update_invoice_cost(OldAmount, NewAmount, Invoice) -> + #domain_Invoice{cost = Cost, mutations = Mutations} = Invoice, + update_invoice_details_price(NewAmount, Invoice#domain_Invoice{ + cost = Cost#domain_Cash{amount = NewAmount}, + mutations = [new_amount_mutation(OldAmount, NewAmount) | genlib:define(Mutations, [])] + }). + +update_invoice_details_price(NewAmount, Invoice) -> + #domain_Invoice{details = Details} = Invoice, + #domain_InvoiceDetails{cart = Cart} = Details, + #domain_InvoiceCart{lines = [Line]} = Cart, + NewLines = [update_invoice_line_price(NewAmount, Line)], + NewCart = Cart#domain_InvoiceCart{lines = NewLines}, + Invoice#domain_Invoice{details = Details#domain_InvoiceDetails{cart = NewCart}}. + +update_invoice_line_price(NewAmount, Line = #domain_InvoiceLine{price = Price}) -> + Line#domain_InvoiceLine{price = Price#domain_Cash{amount = NewAmount}}. + +new_amount_mutation(OldAmount, NewAmount) -> + {amount, #domain_InvoiceAmountMutation{original = OldAmount, mutated = NewAmount}}. + +calc_new_amount(Amount, #domain_RandomizationMutationParams{ + deviation = MaxDeviation, + precision = Precision, + rounding = RoundingMethod +}) -> + Deviation = calc_deviation(RoundingMethod, MaxDeviation, trunc(math:pow(10, Precision))), + Sign = trunc(math:pow(-1, rand:uniform(2))), + Amount + Sign * Deviation. + +calc_deviation(RoundingMethod, MaxDeviation, PrecisionFactor) -> + RoundingFun = rounding_fun(RoundingMethod), + Deviation0 = rand:uniform(MaxDeviation + 1) - 1, + RoundingFun(Deviation0 / PrecisionFactor) * PrecisionFactor. + +rounding_fun(RoundingMethod) -> + case RoundingMethod of + round_half_towards_zero -> fun round/1; + round_half_away_from_zero -> fun round/1; + round_down -> fun floor/1; + round_up -> fun ceil/1 + end. + %%----------------- invoice asserts assert_invoice(Checks, #st{} = St) when is_list(Checks) -> lists:foldl(fun assert_invoice/2, St, Checks); @@ -1066,12 +1079,7 @@ construct_refund_id_test() -> }}} ]). --define(currency(), #domain_Currency{ - name = <<"RUB">>, - numeric_code = 666, - symbolic_code = <<"RUB">>, - exponent = 2 -}). +-define(currency(), #domain_CurrencyRef{symbolic_code = <<"RUB">>}). -define(invoice(Amount, Lines, Mutations), #domain_Invoice{ id = <<"invoice">>, @@ -1084,7 +1092,10 @@ construct_refund_id_test() -> currency = ?currency() }, due = <<"1970-01-01T00:00:00Z">>, - details = #domain_InvoiceDetails{cart = #domain_InvoiceCart{lines = Lines}}, + details = #domain_InvoiceDetails{ + product = <<"rubberduck">>, + cart = #domain_InvoiceCart{lines = Lines} + }, mutations = Mutations }). From 422e3985bc37cb32ffe52eb358da83804c8e39b3 Mon Sep 17 00:00:00 2001 From: Aleksey Kashapov Date: Tue, 18 Jun 2024 12:37:04 +0300 Subject: [PATCH 5/9] Moves mutations into separate module --- apps/hellgate/src/hg_invoice.erl | 170 +--------------- apps/hellgate/src/hg_invoice_handler.erl | 2 +- apps/hellgate/src/hg_invoice_mutation.erl | 226 ++++++++++++++++++++++ apps/hellgate/src/hg_invoice_template.erl | 2 +- apps/hellgate/src/hg_invoice_utils.erl | 31 --- 5 files changed, 229 insertions(+), 202 deletions(-) create mode 100644 apps/hellgate/src/hg_invoice_mutation.erl diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index 1cdab897..7a3b99f3 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -132,7 +132,7 @@ create(ID, InvoiceTplID, PartyRevision, V = #payproc_InvoiceParams{}, Allocation OwnerID = V#payproc_InvoiceParams.party_id, ShopID = V#payproc_InvoiceParams.shop_id, Cost = V#payproc_InvoiceParams.cost, - apply_mutations(V#payproc_InvoiceParams.mutations, #domain_Invoice{ + hg_invoice_mutation:apply_mutations(V#payproc_InvoiceParams.mutations, #domain_Invoice{ id = ID, shop_id = ShopID, owner_id = OwnerID, @@ -149,72 +149,6 @@ create(ID, InvoiceTplID, PartyRevision, V = #payproc_InvoiceParams{}, Allocation allocation = Allocation }). -apply_mutations(MutationsParams, Invoice) -> - lists:foldl(fun apply_mutation/2, Invoice, MutationsParams). - --define(SATISFY_RANDOMIZATION_CONDITION(P, Amount), - %% Multiplicity check - (P#domain_RandomizationMutationParams.amount_multiplicity_condition =:= undefined orelse - Amount rem P#domain_RandomizationMutationParams.amount_multiplicity_condition =:= 0) andalso - %% Min amount - (P#domain_RandomizationMutationParams.min_amount_condition =:= undefined orelse - P#domain_RandomizationMutationParams.min_amount_condition =< Amount) andalso - %% Max amount - (P#domain_RandomizationMutationParams.max_amount_condition =:= undefined orelse - P#domain_RandomizationMutationParams.max_amount_condition >= Amount) -). - -apply_mutation( - {amount, {randomization, Params = #domain_RandomizationMutationParams{}}}, - Invoice = #domain_Invoice{cost = #domain_Cash{amount = Amount}} -) when ?SATISFY_RANDOMIZATION_CONDITION(Params, Amount) -> - update_invoice_cost(Amount, calc_new_amount(Amount, Params), Invoice); -apply_mutation(_, Invoice) -> - Invoice. - -update_invoice_cost(OldAmount, NewAmount, Invoice) -> - #domain_Invoice{cost = Cost, mutations = Mutations} = Invoice, - update_invoice_details_price(NewAmount, Invoice#domain_Invoice{ - cost = Cost#domain_Cash{amount = NewAmount}, - mutations = [new_amount_mutation(OldAmount, NewAmount) | genlib:define(Mutations, [])] - }). - -update_invoice_details_price(NewAmount, Invoice) -> - #domain_Invoice{details = Details} = Invoice, - #domain_InvoiceDetails{cart = Cart} = Details, - #domain_InvoiceCart{lines = [Line]} = Cart, - NewLines = [update_invoice_line_price(NewAmount, Line)], - NewCart = Cart#domain_InvoiceCart{lines = NewLines}, - Invoice#domain_Invoice{details = Details#domain_InvoiceDetails{cart = NewCart}}. - -update_invoice_line_price(NewAmount, Line = #domain_InvoiceLine{price = Price}) -> - Line#domain_InvoiceLine{price = Price#domain_Cash{amount = NewAmount}}. - -new_amount_mutation(OldAmount, NewAmount) -> - {amount, #domain_InvoiceAmountMutation{original = OldAmount, mutated = NewAmount}}. - -calc_new_amount(Amount, #domain_RandomizationMutationParams{ - deviation = MaxDeviation, - precision = Precision, - rounding = RoundingMethod -}) -> - Deviation = calc_deviation(RoundingMethod, MaxDeviation, trunc(math:pow(10, Precision))), - Sign = trunc(math:pow(-1, rand:uniform(2))), - Amount + Sign * Deviation. - -calc_deviation(RoundingMethod, MaxDeviation, PrecisionFactor) -> - RoundingFun = rounding_fun(RoundingMethod), - Deviation0 = rand:uniform(MaxDeviation + 1) - 1, - RoundingFun(Deviation0 / PrecisionFactor) * PrecisionFactor. - -rounding_fun(RoundingMethod) -> - case RoundingMethod of - round_half_towards_zero -> fun round/1; - round_half_away_from_zero -> fun round/1; - round_down -> fun floor/1; - round_up -> fun ceil/1 - end. - %%----------------- invoice asserts assert_invoice(Checks, #st{} = St) when is_list(Checks) -> lists:foldl(fun assert_invoice/2, St, Checks); @@ -1067,106 +1001,4 @@ construct_refund_id_test() -> Refunds = lists:map(fun create_dummy_refund_with_id/1, IDs), ?assert(<<"11">> =:= construct_refund_id(Refunds)). --define(mutations(Deviation, Precision, Rounding, Min, Max, Multiplicity), [ - {amount, - {randomization, #domain_RandomizationMutationParams{ - deviation = Deviation, - precision = Precision, - rounding = Rounding, - min_amount_condition = Min, - max_amount_condition = Max, - amount_multiplicity_condition = Multiplicity - }}} -]). - --define(currency(), #domain_CurrencyRef{symbolic_code = <<"RUB">>}). - --define(invoice(Amount, Lines, Mutations), #domain_Invoice{ - id = <<"invoice">>, - shop_id = <<"shop_id">>, - owner_id = <<"owner_id">>, - created_at = <<"1970-01-01T00:00:00Z">>, - status = ?invoice_unpaid(), - cost = #domain_Cash{ - amount = Amount, - currency = ?currency() - }, - due = <<"1970-01-01T00:00:00Z">>, - details = #domain_InvoiceDetails{ - product = <<"rubberduck">>, - cart = #domain_InvoiceCart{lines = Lines} - }, - mutations = Mutations -}). - --define(mutated_invoice(OriginalAmount, MutatedAmount, Lines), - ?invoice(MutatedAmount, Lines, [ - {amount, #domain_InvoiceAmountMutation{original = OriginalAmount, mutated = MutatedAmount}} - ]) -). - --define(not_mutated_invoice(Amount, Lines), ?invoice(Amount, Lines, undefined)). - --define(cart_line(Price), #domain_InvoiceLine{ - product = <<"product">>, - quantity = 1, - price = #domain_Cash{amount = Price, currency = ?currency()}, - metadata = #{} -}). - --spec apply_mutations_test_() -> [_TestGen]. -apply_mutations_test_() -> - lists:flatten([ - %% Didn't mutate because of conditions - ?_assertEqual( - ?not_mutated_invoice(1000_00, [?cart_line(1000_00)]), - apply_mutations( - ?mutations(100_00, 2, round_half_away_from_zero, 0, 100_00, 1_00), - ?not_mutated_invoice(1000_00, [?cart_line(1000_00)]) - ) - ), - ?_assertEqual( - ?not_mutated_invoice(1234_00, [?cart_line(1234_00)]), - apply_mutations( - ?mutations(100_00, 2, round_half_away_from_zero, 0, 1000_00, 7_00), - ?not_mutated_invoice(1234_00, [?cart_line(1234_00)]) - ) - ), - - %% No deviation, stil did mutate, but amount is same - ?_assertEqual( - ?mutated_invoice(100_00, 100_00, [?cart_line(100_00)]), - apply_mutations( - ?mutations(0, 2, round_half_away_from_zero, 0, 1000_00, 1_00), - ?not_mutated_invoice(100_00, [?cart_line(100_00)]) - ) - ), - - %% Deviate only with 2 other possible values - [ - ?_assertMatch( - ?mutated_invoice(100_00, A, [?cart_line(A)]) when - A =:= 0 orelse A =:= 100_00 orelse A =:= 200_00, - apply_mutations( - Mutations, - ?not_mutated_invoice(100_00, [?cart_line(100_00)]) - ) - ) - || Mutations <- lists:duplicate(10, ?mutations(100_00, 4, round_half_away_from_zero, 0, 1000_00, 1_00)) - ], - - %% Deviate in segment [900_00, 1100_00] without minor units - [ - ?_assertMatch( - ?mutated_invoice(1000_00, A, [?cart_line(A)]) when - A >= 900_00 andalso A =< 1100_00 andalso A rem 100 =:= 0, - apply_mutations( - Mutations, - ?not_mutated_invoice(1000_00, [?cart_line(1000_00)]) - ) - ) - || Mutations <- lists:duplicate(10, ?mutations(100_00, 2, round_half_away_from_zero, 0, 1000_00, 1_00)) - ] - ]). - -endif. diff --git a/apps/hellgate/src/hg_invoice_handler.erl b/apps/hellgate/src/hg_invoice_handler.erl index a346e6ad..63dcd0ff 100644 --- a/apps/hellgate/src/hg_invoice_handler.erl +++ b/apps/hellgate/src/hg_invoice_handler.erl @@ -373,7 +373,7 @@ validate_invoice_params(#payproc_InvoiceParams{cost = Cost} = InvoiceParams, Sho ok. validate_mutations(#payproc_InvoiceParams{mutations = Mutations, details = Details}) -> - hg_invoice_utils:validate_mutations(Mutations, Details). + hg_invoice_mutation:validate_mutations(Mutations, Details). validate_invoice_cost(Cost, Shop, #domain_TermSet{payments = PaymentTerms}) -> _ = hg_invoice_utils:validate_cost(Cost, Shop), diff --git a/apps/hellgate/src/hg_invoice_mutation.erl b/apps/hellgate/src/hg_invoice_mutation.erl new file mode 100644 index 00000000..fdcbc9db --- /dev/null +++ b/apps/hellgate/src/hg_invoice_mutation.erl @@ -0,0 +1,226 @@ +-module(hg_invoice_mutation). + +-include_lib("damsel/include/dmsl_base_thrift.hrl"). +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). + +-export([validate_mutations/2]). +-export([apply_mutations/2]). + +-type mutation_params() :: dmsl_domain_thrift:'InvoiceMutationParams'(). + +-export_type([mutation_params/0]). + +%% + +-type invoice_details() :: dmsl_domain_thrift:'InvoiceDetails'(). +-type invoice_template_details() :: dmsl_domain_thrift:'InvoiceTemplateDetails'(). + +-spec validate_mutations([mutation_params()], invoice_details() | invoice_template_details()) -> ok. +validate_mutations(Mutations, #domain_InvoiceDetails{cart = Cart}) -> + validate_mutations_w_cart(Mutations, Cart); +validate_mutations(Mutations, {cart, #domain_InvoiceCart{} = Cart}) -> + validate_mutations_w_cart(Mutations, Cart); +validate_mutations(_Mutations, _Details) -> + ok. + +validate_mutations_w_cart(Mutations, #domain_InvoiceCart{lines = Lines}) -> + amount_mutation_is_present(Mutations) andalso cart_is_valid_for_mutation(Lines) andalso + throw(#base_InvalidRequest{ + errors = [<<"Amount mutation with multiline cart or multiple items in a line is not allowed">>] + }), + ok. + +amount_mutation_is_present(Mutations) -> + lists:any( + fun + ({amount, _}) -> true; + (_) -> false + end, + Mutations + ). + +cart_is_valid_for_mutation(Lines) -> + length(Lines) > 1 orelse (hd(Lines))#domain_InvoiceLine.quantity =/= 1. + +%% + +-spec apply_mutations([mutation_params()] | undefined, Invoice) -> Invoice when Invoice :: hg_invoice:invoice(). +apply_mutations(MutationsParams, Invoice) -> + lists:foldl(fun apply_mutation/2, Invoice, genlib:define(MutationsParams, [])). + +%% + +-define(SATISFY_RANDOMIZATION_CONDITION(P, Amount), + %% Multiplicity check + (P#domain_RandomizationMutationParams.amount_multiplicity_condition =:= undefined orelse + Amount rem P#domain_RandomizationMutationParams.amount_multiplicity_condition =:= 0) andalso + %% Min amount + (P#domain_RandomizationMutationParams.min_amount_condition =:= undefined orelse + P#domain_RandomizationMutationParams.min_amount_condition =< Amount) andalso + %% Max amount + (P#domain_RandomizationMutationParams.max_amount_condition =:= undefined orelse + P#domain_RandomizationMutationParams.max_amount_condition >= Amount) +). + +apply_mutation( + {amount, {randomization, Params = #domain_RandomizationMutationParams{}}}, + Invoice = #domain_Invoice{cost = #domain_Cash{amount = Amount}} +) when ?SATISFY_RANDOMIZATION_CONDITION(Params, Amount) -> + update_invoice_cost(Amount, calc_new_amount(Amount, Params), Invoice); +apply_mutation(_, Invoice) -> + Invoice. + +update_invoice_cost(OldAmount, NewAmount, Invoice) -> + #domain_Invoice{cost = Cost, mutations = Mutations} = Invoice, + update_invoice_details_price(NewAmount, Invoice#domain_Invoice{ + cost = Cost#domain_Cash{amount = NewAmount}, + mutations = [new_amount_mutation(OldAmount, NewAmount) | genlib:define(Mutations, [])] + }). + +update_invoice_details_price(NewAmount, Invoice) -> + #domain_Invoice{details = Details} = Invoice, + #domain_InvoiceDetails{cart = Cart} = Details, + #domain_InvoiceCart{lines = [Line]} = Cart, + NewLines = [update_invoice_line_price(NewAmount, Line)], + NewCart = Cart#domain_InvoiceCart{lines = NewLines}, + Invoice#domain_Invoice{details = Details#domain_InvoiceDetails{cart = NewCart}}. + +update_invoice_line_price(NewAmount, Line = #domain_InvoiceLine{price = Price}) -> + Line#domain_InvoiceLine{price = Price#domain_Cash{amount = NewAmount}}. + +new_amount_mutation(OldAmount, NewAmount) -> + {amount, #domain_InvoiceAmountMutation{original = OldAmount, mutated = NewAmount}}. + +calc_new_amount(Amount, #domain_RandomizationMutationParams{ + deviation = MaxDeviation, + precision = Precision, + rounding = RoundingMethod +}) -> + Deviation = calc_deviation(RoundingMethod, MaxDeviation, trunc(math:pow(10, Precision))), + Sign = trunc(math:pow(-1, rand:uniform(2))), + Amount + Sign * Deviation. + +calc_deviation(RoundingMethod, MaxDeviation, PrecisionFactor) -> + RoundingFun = rounding_fun(RoundingMethod), + Deviation0 = rand:uniform(MaxDeviation + 1) - 1, + RoundingFun(Deviation0 / PrecisionFactor) * PrecisionFactor. + +rounding_fun(RoundingMethod) -> + case RoundingMethod of + round_half_towards_zero -> fun round/1; + round_half_away_from_zero -> fun round/1; + round_down -> fun floor/1; + round_up -> fun ceil/1 + end. + +%% + + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +-spec test() -> _. + +-define(mutations(Deviation, Precision, Rounding, Min, Max, Multiplicity), [ + {amount, + {randomization, #domain_RandomizationMutationParams{ + deviation = Deviation, + precision = Precision, + rounding = Rounding, + min_amount_condition = Min, + max_amount_condition = Max, + amount_multiplicity_condition = Multiplicity + }}} +]). + +-define(currency(), #domain_CurrencyRef{symbolic_code = <<"RUB">>}). + +-define(invoice(Amount, Lines, Mutations), #domain_Invoice{ + id = <<"invoice">>, + shop_id = <<"shop_id">>, + owner_id = <<"owner_id">>, + created_at = <<"1970-01-01T00:00:00Z">>, + status = {unpaid, #domain_InvoiceUnpaid{}}, + cost = #domain_Cash{ + amount = Amount, + currency = ?currency() + }, + due = <<"1970-01-01T00:00:00Z">>, + details = #domain_InvoiceDetails{ + product = <<"rubberduck">>, + cart = #domain_InvoiceCart{lines = Lines} + }, + mutations = Mutations +}). + +-define(mutated_invoice(OriginalAmount, MutatedAmount, Lines), + ?invoice(MutatedAmount, Lines, [ + {amount, #domain_InvoiceAmountMutation{original = OriginalAmount, mutated = MutatedAmount}} + ]) +). + +-define(not_mutated_invoice(Amount, Lines), ?invoice(Amount, Lines, undefined)). + +-define(cart_line(Price), #domain_InvoiceLine{ + product = <<"product">>, + quantity = 1, + price = #domain_Cash{amount = Price, currency = ?currency()}, + metadata = #{} +}). + +-spec apply_mutations_test_() -> [_TestGen]. +apply_mutations_test_() -> + lists:flatten([ + %% Didn't mutate because of conditions + ?_assertEqual( + ?not_mutated_invoice(1000_00, [?cart_line(1000_00)]), + apply_mutations( + ?mutations(100_00, 2, round_half_away_from_zero, 0, 100_00, 1_00), + ?not_mutated_invoice(1000_00, [?cart_line(1000_00)]) + ) + ), + ?_assertEqual( + ?not_mutated_invoice(1234_00, [?cart_line(1234_00)]), + apply_mutations( + ?mutations(100_00, 2, round_half_away_from_zero, 0, 1000_00, 7_00), + ?not_mutated_invoice(1234_00, [?cart_line(1234_00)]) + ) + ), + + %% No deviation, stil did mutate, but amount is same + ?_assertEqual( + ?mutated_invoice(100_00, 100_00, [?cart_line(100_00)]), + apply_mutations( + ?mutations(0, 2, round_half_away_from_zero, 0, 1000_00, 1_00), + ?not_mutated_invoice(100_00, [?cart_line(100_00)]) + ) + ), + + %% Deviate only with 2 other possible values + [ + ?_assertMatch( + ?mutated_invoice(100_00, A, [?cart_line(A)]) when + A =:= 0 orelse A =:= 100_00 orelse A =:= 200_00, + apply_mutations( + Mutations, + ?not_mutated_invoice(100_00, [?cart_line(100_00)]) + ) + ) + || Mutations <- lists:duplicate(10, ?mutations(100_00, 4, round_half_away_from_zero, 0, 1000_00, 1_00)) + ], + + %% Deviate in segment [900_00, 1100_00] without minor units + [ + ?_assertMatch( + ?mutated_invoice(1000_00, A, [?cart_line(A)]) when + A >= 900_00 andalso A =< 1100_00 andalso A rem 100 =:= 0, + apply_mutations( + Mutations, + ?not_mutated_invoice(1000_00, [?cart_line(1000_00)]) + ) + ) + || Mutations <- lists:duplicate(10, ?mutations(100_00, 2, round_half_away_from_zero, 0, 1000_00, 1_00)) + ] + ]). + +-endif. diff --git a/apps/hellgate/src/hg_invoice_template.erl b/apps/hellgate/src/hg_invoice_template.erl index fb002076..4df3dc7f 100644 --- a/apps/hellgate/src/hg_invoice_template.erl +++ b/apps/hellgate/src/hg_invoice_template.erl @@ -125,7 +125,7 @@ validate_update_params(#payproc_InvoiceTemplateUpdateParams{details = Details, m ok = validate_details(Details, Mutations, Shop). validate_details({cart, #domain_InvoiceCart{}} = Details, Mutations, _) -> - hg_invoice_utils:validate_mutations(Mutations, Details); + hg_invoice_mutation:validate_mutations(Mutations, Details); validate_details({product, #domain_InvoiceTemplateProduct{price = Price}}, _, Shop) -> validate_price(Price, Shop). diff --git a/apps/hellgate/src/hg_invoice_utils.erl b/apps/hellgate/src/hg_invoice_utils.erl index e90e755f..9b37ffe0 100644 --- a/apps/hellgate/src/hg_invoice_utils.erl +++ b/apps/hellgate/src/hg_invoice_utils.erl @@ -10,7 +10,6 @@ -export([validate_cost/2]). -export([validate_currency/2]). -export([validate_cash_range/1]). --export([validate_mutations/2]). -export([assert_party_operable/1]). -export([assert_shop_exists/1]). -export([assert_shop_operable/1]). @@ -37,9 +36,6 @@ -type timestamp() :: dmsl_base_thrift:'Timestamp'(). -type party_revision_param() :: dmsl_payproc_thrift:'PartyRevisionParam'(). -type varset() :: dmsl_payproc_thrift:'ComputeShopTermsVarset'(). --type invoice_details() :: dmsl_domain_thrift:'InvoiceDetails'(). --type invoice_template_details() :: dmsl_domain_thrift:'InvoiceTemplateDetails'(). --type invoice_mutation() :: dmsl_payproc_thrift:'InvoiceMutationParams'(). -spec validate_cost(cash(), shop()) -> ok. validate_cost(#domain_Cash{currency = Currency, amount = Amount}, Shop) -> @@ -70,33 +66,6 @@ validate_cash_range(#domain_CashRange{ validate_cash_range(_) -> throw(#base_InvalidRequest{errors = [<<"Invalid cost range">>]}). --spec validate_mutations([invoice_mutation()], invoice_details() | invoice_template_details()) -> ok. -validate_mutations(Mutations, #domain_InvoiceDetails{cart = Cart}) -> - validate_mutations_w_cart(Mutations, Cart); -validate_mutations(Mutations, {cart, #domain_InvoiceCart{} = Cart}) -> - validate_mutations_w_cart(Mutations, Cart); -validate_mutations(_Mutations, _Details) -> - ok. - -validate_mutations_w_cart(Mutations, #domain_InvoiceCart{lines = Lines}) -> - amount_mutation_is_present(Mutations) andalso cart_is_valid_for_mutation(Lines) andalso - throw(#base_InvalidRequest{ - errors = [<<"Amount mutation with multiline cart or multiple items in a line is not allowed">>] - }), - ok. - -amount_mutation_is_present(Mutations) -> - lists:any( - fun - ({amount, _}) -> true; - (_) -> false - end, - Mutations - ). - -cart_is_valid_for_mutation(Lines) -> - length(Lines) > 1 orelse (hd(Lines))#domain_InvoiceLine.quantity =/= 1. - -spec assert_party_operable(party()) -> party(). assert_party_operable(V) -> _ = assert_party_unblocked(V), From 84467854a238d233e292f12261feeec3bb202262 Mon Sep 17 00:00:00 2001 From: Aleksey Kashapov Date: Tue, 18 Jun 2024 13:51:24 +0300 Subject: [PATCH 6/9] Refactors into separate make and apply mutation functions --- apps/hellgate/src/hg_invoice.erl | 15 ++- apps/hellgate/src/hg_invoice_handler.erl | 31 +++--- apps/hellgate/src/hg_invoice_mutation.erl | 109 ++++++++++++++-------- 3 files changed, 98 insertions(+), 57 deletions(-) diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index 7a3b99f3..da82a9c6 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -36,7 +36,7 @@ -export([get/1]). -export([get_payment/2]). -export([get_payment_opts/1]). --export([create/5]). +-export([create/6]). -export([marshal_invoice/1]). -export([unmarshal_history/1]). -export([collapse_history/1]). @@ -126,13 +126,20 @@ get_payment_opts(Revision, _, St = #st{invoice = Invoice}) -> timestamp => hg_datetime:format_now() }. --spec create(hg_machine:id(), undefined | hg_machine:id(), hg_party:party_revision(), invoice_params(), allocation()) -> +-spec create( + hg_machine:id(), + undefined | hg_machine:id(), + hg_party:party_revision(), + invoice_params(), + allocation(), + [hg_invoice_mutation:mutation()] +) -> invoice(). -create(ID, InvoiceTplID, PartyRevision, V = #payproc_InvoiceParams{}, Allocation) -> +create(ID, InvoiceTplID, PartyRevision, V = #payproc_InvoiceParams{}, Allocation, Mutations) -> OwnerID = V#payproc_InvoiceParams.party_id, ShopID = V#payproc_InvoiceParams.shop_id, Cost = V#payproc_InvoiceParams.cost, - hg_invoice_mutation:apply_mutations(V#payproc_InvoiceParams.mutations, #domain_Invoice{ + hg_invoice_mutation:apply_mutations(Mutations, #domain_Invoice{ id = ID, shop_id = ShopID, owner_id = OwnerID, diff --git a/apps/hellgate/src/hg_invoice_handler.erl b/apps/hellgate/src/hg_invoice_handler.erl index 63dcd0ff..27581c8c 100644 --- a/apps/hellgate/src/hg_invoice_handler.erl +++ b/apps/hellgate/src/hg_invoice_handler.erl @@ -40,18 +40,17 @@ handle_function_('Create', {InvoiceParams}, _Opts) -> Party = hg_party:get_party(PartyID), Shop = assert_shop_exists(hg_party:get_shop(ShopID, Party)), _ = assert_party_shop_operable(Shop, Party), - %% FIXME Should cost amount to be mutated before calculating - %% merchant terms and allocation? + ok = validate_invoice_mutations(InvoiceParams), + {Cost, Mutations} = maybe_make_mutations(InvoiceParams), VS = #{ - cost => InvoiceParams#payproc_InvoiceParams.cost, + cost => Cost, shop_id => Shop#domain_Shop.id }, MerchantTerms = hg_invoice_utils:get_merchant_terms(Party, Shop, DomainRevision, hg_datetime:format_now(), VS), ok = validate_invoice_params(InvoiceParams, Shop, MerchantTerms), AllocationPrototype = InvoiceParams#payproc_InvoiceParams.allocation, - Cost = InvoiceParams#payproc_InvoiceParams.cost, Allocation = maybe_allocation(AllocationPrototype, Cost, MerchantTerms, Party, Shop), - ok = ensure_started(InvoiceID, undefined, Party#domain_Party.revision, InvoiceParams, Allocation), + ok = ensure_started(InvoiceID, undefined, Party#domain_Party.revision, InvoiceParams, Allocation, Mutations), get_invoice_state(get_state(InvoiceID)); handle_function_('CreateWithTemplate', {Params}, _Opts) -> DomainRevision = hg_domain:head(), @@ -59,16 +58,17 @@ handle_function_('CreateWithTemplate', {Params}, _Opts) -> _ = set_invoicing_meta(InvoiceID), TplID = Params#payproc_InvoiceWithTemplateParams.template_id, {Party, Shop, InvoiceParams} = make_invoice_params(Params), + ok = validate_invoice_mutations(InvoiceParams), + {Cost, Mutations} = maybe_make_mutations(InvoiceParams), VS = #{ - cost => InvoiceParams#payproc_InvoiceParams.cost, + cost => Cost, shop_id => Shop#domain_Shop.id }, MerchantTerms = hg_invoice_utils:get_merchant_terms(Party, Shop, DomainRevision, hg_datetime:format_now(), VS), ok = validate_invoice_params(InvoiceParams, Shop, MerchantTerms), AllocationPrototype = InvoiceParams#payproc_InvoiceParams.allocation, - Cost = InvoiceParams#payproc_InvoiceParams.cost, Allocation = maybe_allocation(AllocationPrototype, Cost, MerchantTerms, Party, Shop), - ok = ensure_started(InvoiceID, TplID, Party#domain_Party.revision, InvoiceParams, Allocation), + ok = ensure_started(InvoiceID, TplID, Party#domain_Party.revision, InvoiceParams, Allocation, Mutations), get_invoice_state(get_state(InvoiceID)); handle_function_('CapturePaymentNew', Args, Opts) -> handle_function_('CapturePayment', Args, Opts); @@ -148,8 +148,8 @@ handle_function_('ExplainRoute', {InvoiceID, PaymentID}, _Opts) -> St = get_state(InvoiceID), hg_routing_explanation:get_explanation(get_payment_session(PaymentID, St), hg_invoice:get_payment_opts(St)). -ensure_started(ID, TemplateID, PartyRevision, Params, Allocation) -> - Invoice = hg_invoice:create(ID, TemplateID, PartyRevision, Params, Allocation), +ensure_started(ID, TemplateID, PartyRevision, Params, Allocation, Mutations) -> + Invoice = hg_invoice:create(ID, TemplateID, PartyRevision, Params, Allocation, Mutations), case hg_machine:start(hg_invoice:namespace(), ID, hg_invoice:marshal_invoice(Invoice)) of {ok, _} -> ok; {error, exists} -> ok; @@ -367,12 +367,11 @@ make_invoice_params(Params) -> }, {Party, Shop, InvoiceParams}. -validate_invoice_params(#payproc_InvoiceParams{cost = Cost} = InvoiceParams, Shop, MerchantTerms) -> +validate_invoice_params(#payproc_InvoiceParams{cost = Cost}, Shop, MerchantTerms) -> ok = validate_invoice_cost(Cost, Shop, MerchantTerms), - ok = validate_mutations(InvoiceParams), ok. -validate_mutations(#payproc_InvoiceParams{mutations = Mutations, details = Details}) -> +validate_invoice_mutations(#payproc_InvoiceParams{mutations = Mutations, details = Details}) -> hg_invoice_mutation:validate_mutations(Mutations, Details). validate_invoice_cost(Cost, Shop, #domain_TermSet{payments = PaymentTerms}) -> @@ -380,6 +379,12 @@ validate_invoice_cost(Cost, Shop, #domain_TermSet{payments = PaymentTerms}) -> _ = hg_invoice_utils:assert_cost_payable(Cost, PaymentTerms), ok. +maybe_make_mutations(InvoiceParams) -> + Cost = InvoiceParams#payproc_InvoiceParams.cost, + Mutations = hg_invoice_mutation:make_mutations(InvoiceParams#payproc_InvoiceParams.mutations, #{cost => Cost}), + NewCost = hg_invoice_mutation:get_mutated_cost(Mutations, Cost), + {NewCost, Mutations}. + make_invoice_cart(_, {cart, Cart}, _Shop) -> Cart; make_invoice_cart(Cost, {product, TplProduct}, Shop) -> diff --git a/apps/hellgate/src/hg_invoice_mutation.erl b/apps/hellgate/src/hg_invoice_mutation.erl index fdcbc9db..a106b857 100644 --- a/apps/hellgate/src/hg_invoice_mutation.erl +++ b/apps/hellgate/src/hg_invoice_mutation.erl @@ -3,15 +3,35 @@ -include_lib("damsel/include/dmsl_base_thrift.hrl"). -include_lib("damsel/include/dmsl_domain_thrift.hrl"). +-export([make_mutations/2]). +-export([get_mutated_cost/2]). -export([validate_mutations/2]). -export([apply_mutations/2]). -type mutation_params() :: dmsl_domain_thrift:'InvoiceMutationParams'(). +-type mutation() :: dmsl_domain_thrift:'InvoiceMutation'(). +-type mutation_context() :: #{ + cost := hg_cash:cash() +}. -export_type([mutation_params/0]). +-export_type([mutation/0]). %% +-spec get_mutated_cost([mutation()], Cost) -> Cost when Cost :: hg_cash:cash(). +get_mutated_cost(Mutations, Cost) -> + lists:foldl( + fun + ({amount, #domain_InvoiceAmountMutation{mutated = MutatedAmount}}, C) -> + C#domain_Cash{amount = MutatedAmount}; + (_, C) -> + C + end, + Cost, + Mutations + ). + -type invoice_details() :: dmsl_domain_thrift:'InvoiceDetails'(). -type invoice_template_details() :: dmsl_domain_thrift:'InvoiceTemplateDetails'(). @@ -42,40 +62,18 @@ amount_mutation_is_present(Mutations) -> cart_is_valid_for_mutation(Lines) -> length(Lines) > 1 orelse (hd(Lines))#domain_InvoiceLine.quantity =/= 1. -%% - -spec apply_mutations([mutation_params()] | undefined, Invoice) -> Invoice when Invoice :: hg_invoice:invoice(). apply_mutations(MutationsParams, Invoice) -> lists:foldl(fun apply_mutation/2, Invoice, genlib:define(MutationsParams, [])). -%% - --define(SATISFY_RANDOMIZATION_CONDITION(P, Amount), - %% Multiplicity check - (P#domain_RandomizationMutationParams.amount_multiplicity_condition =:= undefined orelse - Amount rem P#domain_RandomizationMutationParams.amount_multiplicity_condition =:= 0) andalso - %% Min amount - (P#domain_RandomizationMutationParams.min_amount_condition =:= undefined orelse - P#domain_RandomizationMutationParams.min_amount_condition =< Amount) andalso - %% Max amount - (P#domain_RandomizationMutationParams.max_amount_condition =:= undefined orelse - P#domain_RandomizationMutationParams.max_amount_condition >= Amount) -). - -apply_mutation( - {amount, {randomization, Params = #domain_RandomizationMutationParams{}}}, - Invoice = #domain_Invoice{cost = #domain_Cash{amount = Amount}} -) when ?SATISFY_RANDOMIZATION_CONDITION(Params, Amount) -> - update_invoice_cost(Amount, calc_new_amount(Amount, Params), Invoice); -apply_mutation(_, Invoice) -> - Invoice. - -update_invoice_cost(OldAmount, NewAmount, Invoice) -> +apply_mutation(Mutation = {amount, #domain_InvoiceAmountMutation{mutated = NewAmount}}, Invoice) -> #domain_Invoice{cost = Cost, mutations = Mutations} = Invoice, update_invoice_details_price(NewAmount, Invoice#domain_Invoice{ cost = Cost#domain_Cash{amount = NewAmount}, - mutations = [new_amount_mutation(OldAmount, NewAmount) | genlib:define(Mutations, [])] - }). + mutations = genlib:define(Mutations, []) ++ [Mutation] + }); +apply_mutation(_, Invoice) -> + Invoice. update_invoice_details_price(NewAmount, Invoice) -> #domain_Invoice{details = Details} = Invoice, @@ -88,8 +86,35 @@ update_invoice_details_price(NewAmount, Invoice) -> update_invoice_line_price(NewAmount, Line = #domain_InvoiceLine{price = Price}) -> Line#domain_InvoiceLine{price = Price#domain_Cash{amount = NewAmount}}. -new_amount_mutation(OldAmount, NewAmount) -> - {amount, #domain_InvoiceAmountMutation{original = OldAmount, mutated = NewAmount}}. +-spec make_mutations([mutation_params()], mutation_context()) -> [mutation()]. +make_mutations(MutationsParams, Context) -> + lists:reverse(lists:foldl(fun make_mutation/2, {[], Context}, genlib:define(MutationsParams, []))). + +-define(SATISFY_RANDOMIZATION_CONDITION(P, Amount), + %% Multiplicity check + (P#domain_RandomizationMutationParams.amount_multiplicity_condition =:= undefined orelse + Amount rem P#domain_RandomizationMutationParams.amount_multiplicity_condition =:= 0) andalso + %% Min amount + (P#domain_RandomizationMutationParams.min_amount_condition =:= undefined orelse + P#domain_RandomizationMutationParams.min_amount_condition =< Amount) andalso + %% Max amount + (P#domain_RandomizationMutationParams.max_amount_condition =:= undefined orelse + P#domain_RandomizationMutationParams.max_amount_condition >= Amount) +). + +make_mutation( + {amount, {randomization, Params = #domain_RandomizationMutationParams{}}}, + {Mutations, #{cost := #domain_Cash{amount = Amount}}} +) when ?SATISFY_RANDOMIZATION_CONDITION(Params, Amount) -> + [ + {amount, #domain_InvoiceAmountMutation{ + original = Amount, + mutated = calc_new_amount(Amount, Params) + }} + | Mutations + ]; +make_mutation(_, {Mutations, _Context}) -> + Mutations. calc_new_amount(Amount, #domain_RandomizationMutationParams{ deviation = MaxDeviation, @@ -115,7 +140,6 @@ rounding_fun(RoundingMethod) -> %% - -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). @@ -133,6 +157,8 @@ rounding_fun(RoundingMethod) -> }}} ]). +-define(cash(Amount), #domain_Cash{amount = Amount, currency = ?currency()}). + -define(currency(), #domain_CurrencyRef{symbolic_code = <<"RUB">>}). -define(invoice(Amount, Lines, Mutations), #domain_Invoice{ @@ -141,10 +167,7 @@ rounding_fun(RoundingMethod) -> owner_id = <<"owner_id">>, created_at = <<"1970-01-01T00:00:00Z">>, status = {unpaid, #domain_InvoiceUnpaid{}}, - cost = #domain_Cash{ - amount = Amount, - currency = ?currency() - }, + cost = ?cash(Amount), due = <<"1970-01-01T00:00:00Z">>, details = #domain_InvoiceDetails{ product = <<"rubberduck">>, @@ -164,7 +187,7 @@ rounding_fun(RoundingMethod) -> -define(cart_line(Price), #domain_InvoiceLine{ product = <<"product">>, quantity = 1, - price = #domain_Cash{amount = Price, currency = ?currency()}, + price = ?cash(Price), metadata = #{} }). @@ -175,14 +198,18 @@ apply_mutations_test_() -> ?_assertEqual( ?not_mutated_invoice(1000_00, [?cart_line(1000_00)]), apply_mutations( - ?mutations(100_00, 2, round_half_away_from_zero, 0, 100_00, 1_00), + make_mutations(?mutations(100_00, 2, round_half_away_from_zero, 0, 100_00, 1_00), #{ + cost => ?cash(1000_00) + }), ?not_mutated_invoice(1000_00, [?cart_line(1000_00)]) ) ), ?_assertEqual( ?not_mutated_invoice(1234_00, [?cart_line(1234_00)]), apply_mutations( - ?mutations(100_00, 2, round_half_away_from_zero, 0, 1000_00, 7_00), + make_mutations(?mutations(100_00, 2, round_half_away_from_zero, 0, 1000_00, 7_00), #{ + cost => ?cash(1234_00) + }), ?not_mutated_invoice(1234_00, [?cart_line(1234_00)]) ) ), @@ -191,7 +218,9 @@ apply_mutations_test_() -> ?_assertEqual( ?mutated_invoice(100_00, 100_00, [?cart_line(100_00)]), apply_mutations( - ?mutations(0, 2, round_half_away_from_zero, 0, 1000_00, 1_00), + make_mutations(?mutations(0, 2, round_half_away_from_zero, 0, 1000_00, 1_00), #{ + cost => ?cash(100_00) + }), ?not_mutated_invoice(100_00, [?cart_line(100_00)]) ) ), @@ -202,7 +231,7 @@ apply_mutations_test_() -> ?mutated_invoice(100_00, A, [?cart_line(A)]) when A =:= 0 orelse A =:= 100_00 orelse A =:= 200_00, apply_mutations( - Mutations, + make_mutations(Mutations, #{cost => ?cash(100_00)}), ?not_mutated_invoice(100_00, [?cart_line(100_00)]) ) ) @@ -215,7 +244,7 @@ apply_mutations_test_() -> ?mutated_invoice(1000_00, A, [?cart_line(A)]) when A >= 900_00 andalso A =< 1100_00 andalso A rem 100 =:= 0, apply_mutations( - Mutations, + make_mutations(Mutations, #{cost => ?cash(1000_00)}), ?not_mutated_invoice(1000_00, [?cart_line(1000_00)]) ) ) From 1fadcf80bd7f1fc2e64054049fd5cce9c9c2252c Mon Sep 17 00:00:00 2001 From: Aleksey Kashapov Date: Tue, 18 Jun 2024 13:59:33 +0300 Subject: [PATCH 7/9] Fixes make_mutations foldl --- apps/hellgate/src/hg_invoice_mutation.erl | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/apps/hellgate/src/hg_invoice_mutation.erl b/apps/hellgate/src/hg_invoice_mutation.erl index a106b857..c0b9a98e 100644 --- a/apps/hellgate/src/hg_invoice_mutation.erl +++ b/apps/hellgate/src/hg_invoice_mutation.erl @@ -88,7 +88,8 @@ update_invoice_line_price(NewAmount, Line = #domain_InvoiceLine{price = Price}) -spec make_mutations([mutation_params()], mutation_context()) -> [mutation()]. make_mutations(MutationsParams, Context) -> - lists:reverse(lists:foldl(fun make_mutation/2, {[], Context}, genlib:define(MutationsParams, []))). + {Mutations, _} = lists:foldl(fun make_mutation/2, {[], Context}, genlib:define(MutationsParams, [])), + lists:reverse(Mutations). -define(SATISFY_RANDOMIZATION_CONDITION(P, Amount), %% Multiplicity check @@ -104,17 +105,13 @@ make_mutations(MutationsParams, Context) -> make_mutation( {amount, {randomization, Params = #domain_RandomizationMutationParams{}}}, - {Mutations, #{cost := #domain_Cash{amount = Amount}}} + {Mutations, Context = #{cost := #domain_Cash{amount = Amount}}} ) when ?SATISFY_RANDOMIZATION_CONDITION(Params, Amount) -> - [ - {amount, #domain_InvoiceAmountMutation{ - original = Amount, - mutated = calc_new_amount(Amount, Params) - }} - | Mutations - ]; -make_mutation(_, {Mutations, _Context}) -> - Mutations. + NewMutation = + {amount, #domain_InvoiceAmountMutation{original = Amount, mutated = calc_new_amount(Amount, Params)}}, + {[NewMutation | Mutations], Context}; +make_mutation(_, {Mutations, Context}) -> + {Mutations, Context}. calc_new_amount(Amount, #domain_RandomizationMutationParams{ deviation = MaxDeviation, From cd8567fad72bb701a49e5de6a9965cddedc95618 Mon Sep 17 00:00:00 2001 From: Aleksey Kashapov Date: Tue, 18 Jun 2024 14:23:25 +0300 Subject: [PATCH 8/9] Fixes cart validation clause --- apps/hellgate/src/hg_invoice_mutation.erl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/hellgate/src/hg_invoice_mutation.erl b/apps/hellgate/src/hg_invoice_mutation.erl index c0b9a98e..1bdd3fc7 100644 --- a/apps/hellgate/src/hg_invoice_mutation.erl +++ b/apps/hellgate/src/hg_invoice_mutation.erl @@ -36,7 +36,7 @@ get_mutated_cost(Mutations, Cost) -> -type invoice_template_details() :: dmsl_domain_thrift:'InvoiceTemplateDetails'(). -spec validate_mutations([mutation_params()], invoice_details() | invoice_template_details()) -> ok. -validate_mutations(Mutations, #domain_InvoiceDetails{cart = Cart}) -> +validate_mutations(Mutations, #domain_InvoiceDetails{cart = #domain_InvoiceCart{} = Cart}) -> validate_mutations_w_cart(Mutations, Cart); validate_mutations(Mutations, {cart, #domain_InvoiceCart{} = Cart}) -> validate_mutations_w_cart(Mutations, Cart); @@ -44,7 +44,8 @@ validate_mutations(_Mutations, _Details) -> ok. validate_mutations_w_cart(Mutations, #domain_InvoiceCart{lines = Lines}) -> - amount_mutation_is_present(Mutations) andalso cart_is_valid_for_mutation(Lines) andalso + Mutations1 = genlib:define(Mutations, []), + amount_mutation_is_present(Mutations1) andalso cart_is_valid_for_mutation(Lines) andalso throw(#base_InvalidRequest{ errors = [<<"Amount mutation with multiline cart or multiple items in a line is not allowed">>] }), From 87c31dada02a9390db9d1ff8a7ef71dce11f676c Mon Sep 17 00:00:00 2001 From: Aleksey Kashapov Date: Wed, 19 Jun 2024 17:21:09 +0300 Subject: [PATCH 9/9] Retires 'rounding' option in amount randomization --- apps/hellgate/src/hg_invoice_mutation.erl | 34 +++++++---------------- rebar.config | 2 +- rebar.lock | 2 +- 3 files changed, 12 insertions(+), 26 deletions(-) diff --git a/apps/hellgate/src/hg_invoice_mutation.erl b/apps/hellgate/src/hg_invoice_mutation.erl index 1bdd3fc7..e828b9fc 100644 --- a/apps/hellgate/src/hg_invoice_mutation.erl +++ b/apps/hellgate/src/hg_invoice_mutation.erl @@ -114,27 +114,14 @@ make_mutation( make_mutation(_, {Mutations, Context}) -> {Mutations, Context}. -calc_new_amount(Amount, #domain_RandomizationMutationParams{ - deviation = MaxDeviation, - precision = Precision, - rounding = RoundingMethod -}) -> - Deviation = calc_deviation(RoundingMethod, MaxDeviation, trunc(math:pow(10, Precision))), +calc_new_amount(Amount, #domain_RandomizationMutationParams{deviation = MaxDeviation, precision = Precision}) -> + Deviation = calc_deviation(MaxDeviation, trunc(math:pow(10, Precision))), Sign = trunc(math:pow(-1, rand:uniform(2))), Amount + Sign * Deviation. -calc_deviation(RoundingMethod, MaxDeviation, PrecisionFactor) -> - RoundingFun = rounding_fun(RoundingMethod), +calc_deviation(MaxDeviation, PrecisionFactor) -> Deviation0 = rand:uniform(MaxDeviation + 1) - 1, - RoundingFun(Deviation0 / PrecisionFactor) * PrecisionFactor. - -rounding_fun(RoundingMethod) -> - case RoundingMethod of - round_half_towards_zero -> fun round/1; - round_half_away_from_zero -> fun round/1; - round_down -> fun floor/1; - round_up -> fun ceil/1 - end. + erlang:round(Deviation0 / PrecisionFactor) * PrecisionFactor. %% @@ -143,12 +130,11 @@ rounding_fun(RoundingMethod) -> -spec test() -> _. --define(mutations(Deviation, Precision, Rounding, Min, Max, Multiplicity), [ +-define(mutations(Deviation, Precision, Min, Max, Multiplicity), [ {amount, {randomization, #domain_RandomizationMutationParams{ deviation = Deviation, precision = Precision, - rounding = Rounding, min_amount_condition = Min, max_amount_condition = Max, amount_multiplicity_condition = Multiplicity @@ -196,7 +182,7 @@ apply_mutations_test_() -> ?_assertEqual( ?not_mutated_invoice(1000_00, [?cart_line(1000_00)]), apply_mutations( - make_mutations(?mutations(100_00, 2, round_half_away_from_zero, 0, 100_00, 1_00), #{ + make_mutations(?mutations(100_00, 2, 0, 100_00, 1_00), #{ cost => ?cash(1000_00) }), ?not_mutated_invoice(1000_00, [?cart_line(1000_00)]) @@ -205,7 +191,7 @@ apply_mutations_test_() -> ?_assertEqual( ?not_mutated_invoice(1234_00, [?cart_line(1234_00)]), apply_mutations( - make_mutations(?mutations(100_00, 2, round_half_away_from_zero, 0, 1000_00, 7_00), #{ + make_mutations(?mutations(100_00, 2, 0, 1000_00, 7_00), #{ cost => ?cash(1234_00) }), ?not_mutated_invoice(1234_00, [?cart_line(1234_00)]) @@ -216,7 +202,7 @@ apply_mutations_test_() -> ?_assertEqual( ?mutated_invoice(100_00, 100_00, [?cart_line(100_00)]), apply_mutations( - make_mutations(?mutations(0, 2, round_half_away_from_zero, 0, 1000_00, 1_00), #{ + make_mutations(?mutations(0, 2, 0, 1000_00, 1_00), #{ cost => ?cash(100_00) }), ?not_mutated_invoice(100_00, [?cart_line(100_00)]) @@ -233,7 +219,7 @@ apply_mutations_test_() -> ?not_mutated_invoice(100_00, [?cart_line(100_00)]) ) ) - || Mutations <- lists:duplicate(10, ?mutations(100_00, 4, round_half_away_from_zero, 0, 1000_00, 1_00)) + || Mutations <- lists:duplicate(10, ?mutations(100_00, 4, 0, 1000_00, 1_00)) ], %% Deviate in segment [900_00, 1100_00] without minor units @@ -246,7 +232,7 @@ apply_mutations_test_() -> ?not_mutated_invoice(1000_00, [?cart_line(1000_00)]) ) ) - || Mutations <- lists:duplicate(10, ?mutations(100_00, 2, round_half_away_from_zero, 0, 1000_00, 1_00)) + || Mutations <- lists:duplicate(10, ?mutations(100_00, 2, 0, 1000_00, 1_00)) ] ]). diff --git a/rebar.config b/rebar.config index 4af1e43e..edeeb8c4 100644 --- a/rebar.config +++ b/rebar.config @@ -30,7 +30,7 @@ {gproc, "0.9.0"}, {genlib, {git, "https://github.com/valitydev/genlib.git", {branch, "master"}}}, {woody, {git, "https://github.com/valitydev/woody_erlang.git", {branch, "master"}}}, - {damsel, {git, "https://github.com/valitydev/damsel.git", {branch, "OPS-474/ft/invoice-randomization"}}}, + {damsel, {git, "https://github.com/valitydev/damsel.git", {branch, "master"}}}, {payproc_errors, {git, "https://github.com/valitydev/payproc-errors-erlang.git", {branch, "master"}}}, {mg_proto, {git, "https://github.com/valitydev/machinegun-proto.git", {branch, "master"}}}, {dmt_client, {git, "https://github.com/valitydev/dmt-client.git", {branch, "master"}}}, diff --git a/rebar.lock b/rebar.lock index 12fe11a5..06c36220 100644 --- a/rebar.lock +++ b/rebar.lock @@ -21,7 +21,7 @@ {<<"ctx">>,{pkg,<<"ctx">>,<<"0.6.0">>},2}, {<<"damsel">>, {git,"https://github.com/valitydev/damsel.git", - {ref,"73b65efbe017ba541802344afef8c9c184185b81"}}, + {ref,"c170117e5fde4ebdc6878e75dcd37ca2779dfb82"}}, 0}, {<<"dmt_client">>, {git,"https://github.com/valitydev/dmt-client.git",