Одна из проблем, в котором я столкнулся в обсуждении в соседней ветке — это какой-то мегастранный подход апологетов типизации к решению задачи. Почему-то несколько раз для них являлось откровением, что все проверки можно и нужно делать не в местах, откуда вызывается change_amount, а внутри самой функции change_amount. Или, например, что в таком случае можно возвращать внятные ошибки
Несмотря на
жалобы ITАвтор: IT
Дата: 06.02.15
, эта задача — реальная задача, решаемая нашей компанией тысячи раз в день. Выросла она на основе тех самых "ad-hoc and often informal specifications", о которых говорится в презентации
тутАвтор: jazzer
Дата: 30.01.15
. Почему-то, несмотря на презентацию, и уверенность в «компиляторе, проверяющем мозаику», никто так и не осилил решить эту задачу типами
Вы думаете, задачи в Standard Chartered Bank сильно отличаются от такой? Вы сильно заблуждаетесь.
В общем, решение в лоб. Примерно в таком виде оно существует и у нас, работает, проблем не вызывает. Изменение суммы производится путем вызова order:change_amount. Эта функция вызывается из трех мест. Все, что в нее передается — это заказ и новая сумма. На выходе — {ok, UpdatedOrder} или {error, Reason}.
Код полупсевдокод (знающие Erlang поймут, почему — в if guard'ах нельзя кастомные функции). Код писал вручную прямо в редакторе сообщений RSDN, так что могут быть ошибки, естественно
Естественно, к коду еще будут приложены тесты, проверяющие ожидаемое поведение для заказов в разных состояниях. Как unit-тесты, проверяющие, правильно ли реализована функция, так и integration-тесты, проверяющие поведение всей системы. А у вас не так?
M>Шаг 1.
M>[*] если заказ неотправлен и непредоплачен, можно увеличивать и уменьшать
M>[*] если заказ отправлен, нельзя увеличивать, можно уменьшать
M>[*] если сумма увеличивается, мы должны провести risk check. если risk check не проходит, товар никак не помечается, но изменение суммы не проходит
M>[*] если товар помечен, как risk, то изменять сумму нельзя
change_amount(Order, NewAmount) ->
case is_forbidden(Order, NewAmount) of
{error, Reason} ->
{error, Reason};
ok ->
case risk_check(Order, NewAmount) of
{error, Reason} -> {error, Reason};
ok -> {ok, Order#order{amount = NewAmount}
end
end.
is_forbidden(Order, NewAmount) ->
if
is_risk(Order) ->
{error, risk};
not is_shipped(Order) andalso not is_prepaid(Order) ->
ok;
is_shipped(Order) andalso NewAmount > amount(Order) ->
{error, amount}
true ->
ok
end.
risk_check(Order, NewAmount) ->
case amount(Order) < Amount of
true -> risk:risk_check(Order, Amount);
ok
end.
M>Шаг 2.
M>Изменение суммы:
M>Все то же самое, что и в шаге первом, только:
M>[*] увеличивать можно на max(фиксированная сумма1, процент1 от оригинальной суммы заказа)
M>[*] уменьшать можно на max(фиксированная сумма2, процент2 от оригинальной суммы заказа),
M>где сумма1, сумма2, процент1, процент2 — это конфигурация, считываемая из базы данных
M>Если изменение не попадает в эти рамки, то увеличить/уменьшить нельзя
Добавляется одна функция и вызов этой функции. Все остальное без изменений.
change_amount(Order, NewAmount) ->
case is_forbidden(Order, NewAmount) of
...
ok ->
case risk_check(Order, NewAmount) of
...
ok ->
update_amount(Order, NewAmount)
end
end.
...
update_amount(Order, NewAmount) when amount(Order) > NewAmount ->
Estore = get_estore(Order)
Max = config:read(Estore, max_decrease),
MaxPct = config:read(Estore, max_increase_pct),
AmountDiff = abs(amount(Order) - NewAmount),
case AmountDiff > Max andalso AmountDiff/amount(Order) > MaxPct of
true -> {error, amount};
false -> {ok, Order#order{amount = NewAmount}}
end;
update_amount(Order, NewAmount) ->
%% то же самое, только для max_increase и max_increase_pct
end.
M>Шаг 3.
M>Все то же самое, что и в шаге 2. Дополнительно:
M>[*] если заказ предоплачен (неважно, отправлен или нет), можно увеличивать сумму, если это разрешено конфигурацией магазина. Сумма, на которую можно увеличивать высчитывается, как в шаге 2
M>[*] если заказ предоплачен, неотправлен, и сумма увеличивается, надо сделать auth-запрос в банк. если он не срабатывает, увеличить нельзя.
M>[*] если заказ предоплачен, отправлен, и сумма увеличивается, надо сделать auth-запрос в банк на разницу в сумме, а потом сделать capture запрос на всю сумму. Если хоть один из них не срабатывает, увеличить нельзя.
Меняются некоторые условия, добавляются функции. В реальности проверок is_forbidden еще около десятка. Полный код.
change_amount(Order, NewAmount) ->
case is_forbidden(Order, NewAmount) of
{error, Reason} ->
{error, Reason};
ok ->
case risk_check(Order, NewAmount) of
{error, Reason} -> {error, Reason};
ok -> update_amount(Order, NewAmount)
end
end.
is_forbidden(Order, NewAmount) ->
if
is_risk(Order) ->
{error, risk};
is_prepaid(Order) andalso NewAmount > amount(Order) ->
can_raise_prepaid_order(Order, NewAmount);
is_shipped(Order) andalso NewAmount > amount(Order) ->
{error, amount}
true ->
ok
end.
can_raise_prepaid_order(Order, NewAmount) ->
Estore = get_estore(Order),
case config:get_config(Estore, can_raise_prepaid) of
false -> {error, amount};
true -> ok
end.
risk_check(Order, NewAmount) ->
case amount(Order) < Amount of
true -> risk:risk_check(Order, Amount);
ok
end.
update_amount(Order, NewAmount) ->
Estore = get_estore(Order)
Max = config:read(Estore, case amount(Order) > NewAmount of true -> max_decrease; false -> max_increase end),
MaxPct = config:read(Estore, case amount(Order) > NewAmount of true -> max_decrease_pct; false -> max_increase_pct end),
AmountDiff = abs(amount(Order) - NewAmount),
case AmountDiff > Max andalso AmountDiff/amount(Order) > MaxPct of
true -> {error, amount};
false ->
auth_and_capture(Order, NewAmount)
end.
auth_and_capture(Order, NewAmount) ->
case is_prepaid(Order) of
false ->
{ok, Order#order{amount = NewAmount};
true ->
case NewAmount > amount(Order) of
true ->
case payments:auth(Order, NewAmount - amount(Order)) of
{error, Reason} -> {error, Reason};
ok -> capture(Order, NewAmount)
end;
false ->
capture(Order, NewAmount)
end
end.
capture(Order, NewAmount) ->
case is_shipped(Order) of
false -> {ok, Order#order{amount = NewAmount};
true ->
case payment:capture(Order, NewAmount) of
{error, Reason} -> {error, Reason};
ok -> {ok, Order#order{amount = NewAmount}
end
end.