Re: Ах да, мое решение :)
От: Mamut Швеция http://dmitriid.com
Дата: 06.02.15 09:22
Оценка: 48 (2)
Одна из проблем, в котором я столкнулся в обсуждении в соседней ветке — это какой-то мегастранный подход апологетов типизации к решению задачи. Почему-то несколько раз для них являлось откровением, что все проверки можно и нужно делать не в местах, откуда вызывается 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.


dmitriid.comGitHubLinkedIn
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.