|
| 1 | +-module(hg_invoice_mutation). |
| 2 | + |
| 3 | +-include_lib("damsel/include/dmsl_base_thrift.hrl"). |
| 4 | +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). |
| 5 | + |
| 6 | +-export([make_mutations/2]). |
| 7 | +-export([get_mutated_cost/2]). |
| 8 | +-export([validate_mutations/2]). |
| 9 | +-export([apply_mutations/2]). |
| 10 | + |
| 11 | +-type mutation_params() :: dmsl_domain_thrift:'InvoiceMutationParams'(). |
| 12 | +-type mutation() :: dmsl_domain_thrift:'InvoiceMutation'(). |
| 13 | +-type mutation_context() :: #{ |
| 14 | + cost := hg_cash:cash() |
| 15 | +}. |
| 16 | + |
| 17 | +-export_type([mutation_params/0]). |
| 18 | +-export_type([mutation/0]). |
| 19 | + |
| 20 | +%% |
| 21 | + |
| 22 | +-spec get_mutated_cost([mutation()], Cost) -> Cost when Cost :: hg_cash:cash(). |
| 23 | +get_mutated_cost(Mutations, Cost) -> |
| 24 | + lists:foldl( |
| 25 | + fun |
| 26 | + ({amount, #domain_InvoiceAmountMutation{mutated = MutatedAmount}}, C) -> |
| 27 | + C#domain_Cash{amount = MutatedAmount}; |
| 28 | + (_, C) -> |
| 29 | + C |
| 30 | + end, |
| 31 | + Cost, |
| 32 | + Mutations |
| 33 | + ). |
| 34 | + |
| 35 | +-type invoice_details() :: dmsl_domain_thrift:'InvoiceDetails'(). |
| 36 | +-type invoice_template_details() :: dmsl_domain_thrift:'InvoiceTemplateDetails'(). |
| 37 | + |
| 38 | +-spec validate_mutations([mutation_params()], invoice_details() | invoice_template_details()) -> ok. |
| 39 | +validate_mutations(Mutations, #domain_InvoiceDetails{cart = #domain_InvoiceCart{} = Cart}) -> |
| 40 | + validate_mutations_w_cart(Mutations, Cart); |
| 41 | +validate_mutations(Mutations, {cart, #domain_InvoiceCart{} = Cart}) -> |
| 42 | + validate_mutations_w_cart(Mutations, Cart); |
| 43 | +validate_mutations(_Mutations, _Details) -> |
| 44 | + ok. |
| 45 | + |
| 46 | +validate_mutations_w_cart(Mutations, #domain_InvoiceCart{lines = Lines}) -> |
| 47 | + Mutations1 = genlib:define(Mutations, []), |
| 48 | + amount_mutation_is_present(Mutations1) andalso cart_is_valid_for_mutation(Lines) andalso |
| 49 | + throw(#base_InvalidRequest{ |
| 50 | + errors = [<<"Amount mutation with multiline cart or multiple items in a line is not allowed">>] |
| 51 | + }), |
| 52 | + ok. |
| 53 | + |
| 54 | +amount_mutation_is_present(Mutations) -> |
| 55 | + lists:any( |
| 56 | + fun |
| 57 | + ({amount, _}) -> true; |
| 58 | + (_) -> false |
| 59 | + end, |
| 60 | + Mutations |
| 61 | + ). |
| 62 | + |
| 63 | +cart_is_valid_for_mutation(Lines) -> |
| 64 | + length(Lines) > 1 orelse (hd(Lines))#domain_InvoiceLine.quantity =/= 1. |
| 65 | + |
| 66 | +-spec apply_mutations([mutation_params()] | undefined, Invoice) -> Invoice when Invoice :: hg_invoice:invoice(). |
| 67 | +apply_mutations(MutationsParams, Invoice) -> |
| 68 | + lists:foldl(fun apply_mutation/2, Invoice, genlib:define(MutationsParams, [])). |
| 69 | + |
| 70 | +apply_mutation(Mutation = {amount, #domain_InvoiceAmountMutation{mutated = NewAmount}}, Invoice) -> |
| 71 | + #domain_Invoice{cost = Cost, mutations = Mutations} = Invoice, |
| 72 | + update_invoice_details_price(NewAmount, Invoice#domain_Invoice{ |
| 73 | + cost = Cost#domain_Cash{amount = NewAmount}, |
| 74 | + mutations = genlib:define(Mutations, []) ++ [Mutation] |
| 75 | + }); |
| 76 | +apply_mutation(_, Invoice) -> |
| 77 | + Invoice. |
| 78 | + |
| 79 | +update_invoice_details_price(NewAmount, Invoice) -> |
| 80 | + #domain_Invoice{details = Details} = Invoice, |
| 81 | + #domain_InvoiceDetails{cart = Cart} = Details, |
| 82 | + #domain_InvoiceCart{lines = [Line]} = Cart, |
| 83 | + NewLines = [update_invoice_line_price(NewAmount, Line)], |
| 84 | + NewCart = Cart#domain_InvoiceCart{lines = NewLines}, |
| 85 | + Invoice#domain_Invoice{details = Details#domain_InvoiceDetails{cart = NewCart}}. |
| 86 | + |
| 87 | +update_invoice_line_price(NewAmount, Line = #domain_InvoiceLine{price = Price}) -> |
| 88 | + Line#domain_InvoiceLine{price = Price#domain_Cash{amount = NewAmount}}. |
| 89 | + |
| 90 | +-spec make_mutations([mutation_params()], mutation_context()) -> [mutation()]. |
| 91 | +make_mutations(MutationsParams, Context) -> |
| 92 | + {Mutations, _} = lists:foldl(fun make_mutation/2, {[], Context}, genlib:define(MutationsParams, [])), |
| 93 | + lists:reverse(Mutations). |
| 94 | + |
| 95 | +-define(SATISFY_RANDOMIZATION_CONDITION(P, Amount), |
| 96 | + %% Multiplicity check |
| 97 | + (P#domain_RandomizationMutationParams.amount_multiplicity_condition =:= undefined orelse |
| 98 | + Amount rem P#domain_RandomizationMutationParams.amount_multiplicity_condition =:= 0) andalso |
| 99 | + %% Min amount |
| 100 | + (P#domain_RandomizationMutationParams.min_amount_condition =:= undefined orelse |
| 101 | + P#domain_RandomizationMutationParams.min_amount_condition =< Amount) andalso |
| 102 | + %% Max amount |
| 103 | + (P#domain_RandomizationMutationParams.max_amount_condition =:= undefined orelse |
| 104 | + P#domain_RandomizationMutationParams.max_amount_condition >= Amount) |
| 105 | +). |
| 106 | + |
| 107 | +make_mutation( |
| 108 | + {amount, {randomization, Params = #domain_RandomizationMutationParams{}}}, |
| 109 | + {Mutations, Context = #{cost := #domain_Cash{amount = Amount}}} |
| 110 | +) when ?SATISFY_RANDOMIZATION_CONDITION(Params, Amount) -> |
| 111 | + NewMutation = |
| 112 | + {amount, #domain_InvoiceAmountMutation{original = Amount, mutated = calc_new_amount(Amount, Params)}}, |
| 113 | + {[NewMutation | Mutations], Context}; |
| 114 | +make_mutation(_, {Mutations, Context}) -> |
| 115 | + {Mutations, Context}. |
| 116 | + |
| 117 | +calc_new_amount(Amount, #domain_RandomizationMutationParams{deviation = MaxDeviation, precision = Precision}) -> |
| 118 | + Deviation = calc_deviation(MaxDeviation, trunc(math:pow(10, Precision))), |
| 119 | + Sign = trunc(math:pow(-1, rand:uniform(2))), |
| 120 | + Amount + Sign * Deviation. |
| 121 | + |
| 122 | +calc_deviation(MaxDeviation, PrecisionFactor) -> |
| 123 | + Deviation0 = rand:uniform(MaxDeviation + 1) - 1, |
| 124 | + erlang:round(Deviation0 / PrecisionFactor) * PrecisionFactor. |
| 125 | + |
| 126 | +%% |
| 127 | + |
| 128 | +-ifdef(TEST). |
| 129 | +-include_lib("eunit/include/eunit.hrl"). |
| 130 | + |
| 131 | +-spec test() -> _. |
| 132 | + |
| 133 | +-define(mutations(Deviation, Precision, Min, Max, Multiplicity), [ |
| 134 | + {amount, |
| 135 | + {randomization, #domain_RandomizationMutationParams{ |
| 136 | + deviation = Deviation, |
| 137 | + precision = Precision, |
| 138 | + min_amount_condition = Min, |
| 139 | + max_amount_condition = Max, |
| 140 | + amount_multiplicity_condition = Multiplicity |
| 141 | + }}} |
| 142 | +]). |
| 143 | + |
| 144 | +-define(cash(Amount), #domain_Cash{amount = Amount, currency = ?currency()}). |
| 145 | + |
| 146 | +-define(currency(), #domain_CurrencyRef{symbolic_code = <<"RUB">>}). |
| 147 | + |
| 148 | +-define(invoice(Amount, Lines, Mutations), #domain_Invoice{ |
| 149 | + id = <<"invoice">>, |
| 150 | + shop_id = <<"shop_id">>, |
| 151 | + owner_id = <<"owner_id">>, |
| 152 | + created_at = <<"1970-01-01T00:00:00Z">>, |
| 153 | + status = {unpaid, #domain_InvoiceUnpaid{}}, |
| 154 | + cost = ?cash(Amount), |
| 155 | + due = <<"1970-01-01T00:00:00Z">>, |
| 156 | + details = #domain_InvoiceDetails{ |
| 157 | + product = <<"rubberduck">>, |
| 158 | + cart = #domain_InvoiceCart{lines = Lines} |
| 159 | + }, |
| 160 | + mutations = Mutations |
| 161 | +}). |
| 162 | + |
| 163 | +-define(mutated_invoice(OriginalAmount, MutatedAmount, Lines), |
| 164 | + ?invoice(MutatedAmount, Lines, [ |
| 165 | + {amount, #domain_InvoiceAmountMutation{original = OriginalAmount, mutated = MutatedAmount}} |
| 166 | + ]) |
| 167 | +). |
| 168 | + |
| 169 | +-define(not_mutated_invoice(Amount, Lines), ?invoice(Amount, Lines, undefined)). |
| 170 | + |
| 171 | +-define(cart_line(Price), #domain_InvoiceLine{ |
| 172 | + product = <<"product">>, |
| 173 | + quantity = 1, |
| 174 | + price = ?cash(Price), |
| 175 | + metadata = #{} |
| 176 | +}). |
| 177 | + |
| 178 | +-spec apply_mutations_test_() -> [_TestGen]. |
| 179 | +apply_mutations_test_() -> |
| 180 | + lists:flatten([ |
| 181 | + %% Didn't mutate because of conditions |
| 182 | + ?_assertEqual( |
| 183 | + ?not_mutated_invoice(1000_00, [?cart_line(1000_00)]), |
| 184 | + apply_mutations( |
| 185 | + make_mutations(?mutations(100_00, 2, 0, 100_00, 1_00), #{ |
| 186 | + cost => ?cash(1000_00) |
| 187 | + }), |
| 188 | + ?not_mutated_invoice(1000_00, [?cart_line(1000_00)]) |
| 189 | + ) |
| 190 | + ), |
| 191 | + ?_assertEqual( |
| 192 | + ?not_mutated_invoice(1234_00, [?cart_line(1234_00)]), |
| 193 | + apply_mutations( |
| 194 | + make_mutations(?mutations(100_00, 2, 0, 1000_00, 7_00), #{ |
| 195 | + cost => ?cash(1234_00) |
| 196 | + }), |
| 197 | + ?not_mutated_invoice(1234_00, [?cart_line(1234_00)]) |
| 198 | + ) |
| 199 | + ), |
| 200 | + |
| 201 | + %% No deviation, stil did mutate, but amount is same |
| 202 | + ?_assertEqual( |
| 203 | + ?mutated_invoice(100_00, 100_00, [?cart_line(100_00)]), |
| 204 | + apply_mutations( |
| 205 | + make_mutations(?mutations(0, 2, 0, 1000_00, 1_00), #{ |
| 206 | + cost => ?cash(100_00) |
| 207 | + }), |
| 208 | + ?not_mutated_invoice(100_00, [?cart_line(100_00)]) |
| 209 | + ) |
| 210 | + ), |
| 211 | + |
| 212 | + %% Deviate only with 2 other possible values |
| 213 | + [ |
| 214 | + ?_assertMatch( |
| 215 | + ?mutated_invoice(100_00, A, [?cart_line(A)]) when |
| 216 | + A =:= 0 orelse A =:= 100_00 orelse A =:= 200_00, |
| 217 | + apply_mutations( |
| 218 | + make_mutations(Mutations, #{cost => ?cash(100_00)}), |
| 219 | + ?not_mutated_invoice(100_00, [?cart_line(100_00)]) |
| 220 | + ) |
| 221 | + ) |
| 222 | + || Mutations <- lists:duplicate(10, ?mutations(100_00, 4, 0, 1000_00, 1_00)) |
| 223 | + ], |
| 224 | + |
| 225 | + %% Deviate in segment [900_00, 1100_00] without minor units |
| 226 | + [ |
| 227 | + ?_assertMatch( |
| 228 | + ?mutated_invoice(1000_00, A, [?cart_line(A)]) when |
| 229 | + A >= 900_00 andalso A =< 1100_00 andalso A rem 100 =:= 0, |
| 230 | + apply_mutations( |
| 231 | + make_mutations(Mutations, #{cost => ?cash(1000_00)}), |
| 232 | + ?not_mutated_invoice(1000_00, [?cart_line(1000_00)]) |
| 233 | + ) |
| 234 | + ) |
| 235 | + || Mutations <- lists:duplicate(10, ?mutations(100_00, 2, 0, 1000_00, 1_00)) |
| 236 | + ] |
| 237 | + ]). |
| 238 | + |
| 239 | +-endif. |
0 commit comments