From f024b0ca3dbb13bf3733b5d1d224b8d86bb5c862 Mon Sep 17 00:00:00 2001 From: arjun289 Date: Tue, 19 Feb 2019 19:45:41 +0530 Subject: [PATCH] Promotion re-evaluation for line_item CRUD If an order has promotion applied to it, then if any of the line items are touched(updated, removed) or a new line item is added, promotion associated with it needs to be re-evaluated so that right adjustments are present for the order. This change - Adds a module to handle promotion re-evaluation whenever line items of an order are added, removed or updated. - The ineligible adjustments for an order are now removed so that only eligible adjustments are present. --- apps/snitch_api/lib/snitch_api/order/order.ex | 38 +++-- .../controllers/line_item_controller.ex | 5 +- .../model/adjustment/promotion_adjustment.ex | 27 +++- .../lib/core/data/model/line_item.ex | 2 +- apps/snitch_core/lib/core/data/model/order.ex | 11 +- .../data/model/promotion/order_eligibility.ex | 2 +- .../core/data/model/promotion/promotion.ex | 5 +- .../lib/core/domain/promotion/cart.ex | 149 +++++++++++++++++ ...alter_promo_adjustments_cascade_delete.exs | 10 ++ .../adjustment/promotion_adjustment_test.exs | 7 +- .../test/data/model/order_test.exs | 22 ++- .../data/model/promotion/eligibility_test.exs | 2 +- .../promotion/order_eligibility_test.exs | 2 +- .../data/model/promotion/promotion_test.exs | 5 +- .../test/domain/promotion/cart_test.exs | 151 ++++++++++++++++++ 15 files changed, 390 insertions(+), 48 deletions(-) create mode 100644 apps/snitch_core/lib/core/domain/promotion/cart.ex create mode 100644 apps/snitch_core/priv/repo/migrations/20190218071130_alter_promo_adjustments_cascade_delete.exs create mode 100644 apps/snitch_core/test/domain/promotion/cart_test.exs diff --git a/apps/snitch_api/lib/snitch_api/order/order.ex b/apps/snitch_api/lib/snitch_api/order/order.ex index f48b0f9ee..055bd7da6 100644 --- a/apps/snitch_api/lib/snitch_api/order/order.ex +++ b/apps/snitch_api/lib/snitch_api/order/order.ex @@ -43,6 +43,26 @@ defmodule SnitchApi.Order do Repo.preload(order, line_items: line_item_query) end + ####################### line item helpers ############################ + + def add_to_cart(line_item) do + case get_line_item(line_item["order_id"], line_item["product_id"]) do + [] -> LineItemModel.create(line_item) + [l | _] -> update_line_item(l, line_item) + end + end + + defp get_line_item(order_id, product_id) do + query = from(l in LineItem, where: l.order_id == ^order_id and l.product_id == ^product_id) + Repo.all(query) + end + + defp update_line_item(line_item, params) do + new_count = line_item.quantity + params["quantity"] + new_params = %{params | "quantity" => new_count} + LineItemModel.update(line_item, new_params) + end + def delete_line_item(line_item_id) do with {:ok, %LineItem{} = line_item} <- LineItemModel.get(line_item_id), {:ok, _} <- LineItemModel.delete(line_item) do @@ -52,12 +72,7 @@ defmodule SnitchApi.Order do end end - def add_to_cart(line_item) do - case get_line_item(line_item["order_id"], line_item["product_id"]) do - [] -> LineItemModel.create(line_item) - [l | _] -> update_line_item(l, line_item) - end - end + #################### order transition helpers ####################### def add_payment(order_id, payment_method_id) do with {:ok, order} <- Order.get(order_id), @@ -132,17 +147,6 @@ defmodule SnitchApi.Order do end end - defp get_line_item(order_id, product_id) do - query = from(l in LineItem, where: l.order_id == ^order_id and l.product_id == ^product_id) - Repo.all(query) - end - - defp update_line_item(line_item, params) do - new_count = line_item.quantity + params["quantity"] - new_params = %{params | "quantity" => new_count} - LineItemModel.update(line_item, new_params) - end - defp transition_response(%Context{errors: nil}, order_id) do {:ok, order} = Order.get(order_id) order = order |> Repo.preload(line_items: [product: [:theme, [options: :option_type]]]) diff --git a/apps/snitch_api/lib/snitch_api_web/controllers/line_item_controller.ex b/apps/snitch_api/lib/snitch_api_web/controllers/line_item_controller.ex index 79439602c..09689295e 100644 --- a/apps/snitch_api/lib/snitch_api_web/controllers/line_item_controller.ex +++ b/apps/snitch_api/lib/snitch_api_web/controllers/line_item_controller.ex @@ -3,9 +3,8 @@ defmodule SnitchApiWeb.LineItemController do alias Snitch.Data.Model.LineItem, as: LineItemModel alias Snitch.Data.Model.Order, as: OrderModel - alias Snitch.Data.Schema.{LineItem, Variant, Product} + alias Snitch.Data.Schema.{LineItem, Product} alias Snitch.Core.Tools.MultiTenancy.Repo - import Ecto.Query alias SnitchApi.Order, as: OrderContext plug(SnitchApiWeb.Plug.DataToAttributes) @@ -35,7 +34,7 @@ defmodule SnitchApiWeb.LineItemController do def guest_line_item(conn, %{"order_id" => order_id} = params) do with {:ok, line_item} <- add_line_item(params) do - order = OrderContext.load_order(line_item.order_id) + order = OrderContext.load_order(order_id) conn |> put_status(200) diff --git a/apps/snitch_core/lib/core/data/model/adjustment/promotion_adjustment.ex b/apps/snitch_core/lib/core/data/model/adjustment/promotion_adjustment.ex index 623d8801e..1743f2187 100644 --- a/apps/snitch_core/lib/core/data/model/adjustment/promotion_adjustment.ex +++ b/apps/snitch_core/lib/core/data/model/adjustment/promotion_adjustment.ex @@ -59,6 +59,9 @@ defmodule Snitch.Data.Model.PromotionAdjustment do Repo.all(query) end + @doc """ + Returns all adjustments for the supplied `order` and `promotion`. + """ def order_adjustments_for_promotion(order, promotion) do query = from(adj in AdjustmentSchema, @@ -98,11 +101,11 @@ defmodule Snitch.Data.Model.PromotionAdjustment do @doc """ Processes adjustments for the supplied `order` and `promotion`. - Adjustments are created due for all the `actions` of a `promotion` - for an order. However, these adjustments are created in an ineligible state, + Adjustments are created for all the `actions` of a `promotion` + on an order. However, these adjustments are created in an ineligible state, handled by the `eligible` field in `Adjustments` which is initially false. - The `eligible` field is marked true subject to condition that a better promotion + The `eligible` field is marked true subject to condition that, a better promotion doesn't exist already for the order. In case a better promotion exists `{:error, message}` tuple is returned. Otherwise, the adjustments due to previous promotion are marked as ineligible @@ -122,6 +125,7 @@ defmodule Snitch.Data.Model.PromotionAdjustment do if current_discount > previous_discount do case activate_adjustments( + order, prev_eligible_ids, current_adjustment_ids ) do @@ -138,14 +142,23 @@ defmodule Snitch.Data.Model.PromotionAdjustment do ################## private functions ################ - defp activate_adjustments(prev_eligible_ids, current_adjustment_ids) do + defp activate_adjustments(order, prev_eligible_ids, current_adjustment_ids) do Multi.new() - |> Multi.run(:remove_eligible_adjustments, fn _ -> + |> Multi.run(:remove_previous_eligible_adjustments, fn _ -> {:ok, update_adjustments(prev_eligible_ids, false)} end) |> Multi.run(:activate_new_adjustments, fn _ -> {:ok, update_adjustments(current_adjustment_ids, true)} end) + |> Multi.delete_all( + :delete_ineligible_adjustments, + from( + adj in AdjustmentSchema, + join: p_adj in PromotionAdjustment, + on: p_adj.adjustment_id == adj.id, + where: p_adj.order_id == ^order.id and adj.eligible == false + ) + ) |> Repo.transaction() |> case do {:ok, data} -> @@ -215,8 +228,8 @@ defmodule Snitch.Data.Model.PromotionAdjustment do # Run the accumulated multi struct defp persist(multi) do case Repo.transaction(multi) do - {:ok, %{promo_adj: promo_adjustment}} -> - {:ok, promo_adjustment} + {:ok, %{adjustment: adjustment}} -> + {:ok, adjustment} {:error, _, data, _} -> {:error, data} diff --git a/apps/snitch_core/lib/core/data/model/line_item.ex b/apps/snitch_core/lib/core/data/model/line_item.ex index dd4d2d1a0..12d95e517 100644 --- a/apps/snitch_core/lib/core/data/model/line_item.ex +++ b/apps/snitch_core/lib/core/data/model/line_item.ex @@ -6,7 +6,7 @@ defmodule Snitch.Data.Model.LineItem do import Ecto.Changeset, only: [change: 1] - alias Snitch.Data.Model.{Variant, Product} + alias Snitch.Data.Model.Product alias Snitch.Data.Schema.LineItem alias Snitch.Domain.Order alias Snitch.Tools.Money, as: MoneyTools diff --git a/apps/snitch_core/lib/core/data/model/order.ex b/apps/snitch_core/lib/core/data/model/order.ex index c76f013f6..37b6fbdc8 100644 --- a/apps/snitch_core/lib/core/data/model/order.ex +++ b/apps/snitch_core/lib/core/data/model/order.ex @@ -9,6 +9,8 @@ defmodule Snitch.Data.Model.Order do alias Snitch.Data.Model.LineItem, as: LineItemModel @order_states ["confirmed", "complete"] + @non_complete_order_states ~w(cart address delivery payment)a + @doc """ Creates an order with supplied `params` and `line_items`. @@ -44,17 +46,16 @@ defmodule Snitch.Data.Model.Order do @doc """ Returns an `order` struct for the supplied `user_id`. - An existing `order` associated with the user is present in either `cart` or - `address` state is returned if found. If no order is present for the user - in `cart` or `address` state a new order is created returned. + An existing `order` associated with the user present in "#{inspect(@non_complete_order_states)}" + state is returned if found. If no order is present for the user + in these states, a new order is created and returned. """ @spec user_order(non_neg_integer) :: Order.t() def user_order(user_id) do query = from( order in Order, - where: - order.user_id == ^user_id and order.state in ["cart", "address", "delivery", "payment"] + where: order.user_id == ^user_id and order.state in ^@non_complete_order_states ) query diff --git a/apps/snitch_core/lib/core/data/model/promotion/order_eligibility.ex b/apps/snitch_core/lib/core/data/model/promotion/order_eligibility.ex index 1e3faf772..afa41cd9b 100644 --- a/apps/snitch_core/lib/core/data/model/promotion/order_eligibility.ex +++ b/apps/snitch_core/lib/core/data/model/promotion/order_eligibility.ex @@ -6,7 +6,7 @@ defmodule Snitch.Data.Model.Promotion.OrderEligibility do use Snitch.Data.Model alias Snitch.Data.Model.PromotionAdjustment - @valid_order_states ~w(delivery address)a + @valid_order_states ~w(delivery address cart)a @success_message "promotion applicable" @error_message "coupon not applicable" @coupon_applied "coupon already applied" diff --git a/apps/snitch_core/lib/core/data/model/promotion/promotion.ex b/apps/snitch_core/lib/core/data/model/promotion/promotion.ex index 4aefd425d..d8a54577b 100644 --- a/apps/snitch_core/lib/core/data/model/promotion/promotion.ex +++ b/apps/snitch_core/lib/core/data/model/promotion/promotion.ex @@ -194,9 +194,9 @@ defmodule Snitch.Data.Model.Promotion do end end - def update_usage_count(promotion) do + def update_usage_count(promotion, count) do current_usage_count = promotion.current_usage_count - params = %{current_usage_count: current_usage_count + 1} + params = %{current_usage_count: current_usage_count + count} QH.update(Promotion, params, promotion, Repo) end @@ -216,6 +216,7 @@ defmodule Snitch.Data.Model.Promotion do defp process_adjustments(order, promotion) do case PromotionAdjustment.process_adjustments(order, promotion) do {:ok, _data} -> + update_usage_count(promotion, 1) {:ok, @messages.coupon_applied} {:error, _message} = error -> diff --git a/apps/snitch_core/lib/core/domain/promotion/cart.ex b/apps/snitch_core/lib/core/domain/promotion/cart.ex new file mode 100644 index 000000000..d90c4d06d --- /dev/null +++ b/apps/snitch_core/lib/core/domain/promotion/cart.ex @@ -0,0 +1,149 @@ +defmodule Snitch.Domain.Promotion.CartHelper do + @moduledoc """ + Module exposes functions to handle promotion related checks on + an order. + + If an order has any promotion associated with it then, on addition, updation + or removal of lineitem it may become eligible or ineligible for the promotion. + """ + + import Ecto.Query + alias Ecto.Multi + alias Snitch.Core.Tools.MultiTenancy.Repo + alias Snitch.Data.Schema.{Adjustment, Promotion, PromotionAdjustment} + alias Snitch.Data.Model.Promotion.Eligibility + alias Snitch.Data.Model.Promotion, as: PromotionModel + alias Snitch.Data.Model.PromotionAdjustment, as: PromoAdjModel + alias Snitch.Data.Model.Promotion.OrderEligibility + + @success_message "coupon applied" + @failure_message "promotion not applicable" + + @doc """ + Revaluates the promotion for order for which the line_item was + touched(created, updated or removed). + + Finds out the `promotion` for which the `order` has active promotios. + + > Active promotions for an order is found out by checking if `eligible` promotion related + > adjustments are present for the order. + > See Snitch.Data.Model.PromotionAdjustment + """ + @spec evaluate_promotion(Order.t()) :: {:ok, map} | {:error, String.t()} + def evaluate_promotion(order) do + case get_promotion(order) do + nil -> + {:error, "no promotion applied to order"} + + promotion -> + process_adjustments(order, promotion) + end + end + + # Handles adjustments for the promotion returned for the order. If the order + # becomes ineligible for the promotion then removes all the records for the order + # and the promotion. In case order is still eligible for promotion new adjustments + # as line item has modified the order and promotion actions will act differently + # on the modified order. The older adjustments existing for the order are removed. + defp process_adjustments(order, promotion) do + with {:ok, _message} <- eligibility_checks(order, promotion) do + if PromotionModel.activate?(order, promotion) do + activate_recent_adjustments(order, promotion) + end + else + {:error, _message} -> + remove_adjustments(order, promotion) + end + end + + defp remove_adjustments(order, promotion) do + Multi.new() + |> Multi.delete_all( + :remove_adjustments_for_promotion, + from( + adj in Adjustment, + join: p_adj in PromotionAdjustment, + on: p_adj.adjustment_id == adj.id, + where: p_adj.order_id == ^order.id and p_adj.promotion_id == ^promotion.id + ) + ) + |> Multi.run(:update_promotion_count, fn _ -> + PromotionModel.update_usage_count(promotion, -1) + end) + |> persist() + end + + defp activate_recent_adjustments(order, promotion) do + adjustments = PromoAdjModel.order_adjustments_for_promotion(order, promotion) + + old_adjustment_ids = + Enum.flat_map(adjustments, fn adj -> + if adj.eligible == true, do: [adj.id], else: [] + end) + + new_adjustment_ids = + Enum.flat_map(adjustments, fn adj -> + if adj.eligible == false, do: [adj.id], else: [] + end) + + Multi.new() + |> Multi.delete_all( + :remove_old_adjustments, + from(adj in Adjustment, where: adj.id in ^old_adjustment_ids) + ) + |> Multi.update_all( + :mark_new_adjustment_as_eligible, + from(adj in Adjustment, where: adj.id in ^new_adjustment_ids), + set: [eligible: true] + ) + |> persist() + end + + defp get_promotion(order) do + case get_active_promotion_id(order) do + nil -> + nil + + id -> + Repo.get(Promotion, id) + end + end + + defp get_active_promotion_id(order) do + query = + from(adj in Adjustment, + join: p_adj in PromotionAdjustment, + on: adj.id == p_adj.adjustment_id, + where: p_adj.order_id == ^order.id and adj.eligible == true, + limit: 1, + select: p_adj.promotion_id + ) + + Repo.one(query) + end + + # Run the accumulated multi struct + defp persist(multi) do + case Repo.transaction(multi) do + {:ok, data} -> + {:ok, data} + + {:error, _, data, _} -> + {:error, data} + end + end + + defp eligibility_checks(order, promotion) do + with true <- Eligibility.promotion_level_check(promotion), + {true, _message} <- OrderEligibility.order_promotionable(order), + {true, _message} <- OrderEligibility.rules_check(order, promotion) do + {:ok, @success_message} + else + {false, message} -> + {:error, message} + + false -> + {:error, @failure_message} + end + end +end diff --git a/apps/snitch_core/priv/repo/migrations/20190218071130_alter_promo_adjustments_cascade_delete.exs b/apps/snitch_core/priv/repo/migrations/20190218071130_alter_promo_adjustments_cascade_delete.exs new file mode 100644 index 000000000..f8b5cfb2d --- /dev/null +++ b/apps/snitch_core/priv/repo/migrations/20190218071130_alter_promo_adjustments_cascade_delete.exs @@ -0,0 +1,10 @@ +defmodule Snitch.Repo.Migrations.AlterPromoAdjustmentsCascadeDelete do + use Ecto.Migration + + def change do + execute "ALTER TABLE snitch_promotion_adjustments DROP CONSTRAINT snitch_promotion_adjustments_adjustment_id_fkey" + alter table("snitch_promotion_adjustments") do + modify(:adjustment_id, references("snitch_adjustments", on_delete: :delete_all)) + end + end +end diff --git a/apps/snitch_core/test/data/model/adjustment/promotion_adjustment_test.exs b/apps/snitch_core/test/data/model/adjustment/promotion_adjustment_test.exs index c87df17be..d95f1a5e6 100644 --- a/apps/snitch_core/test/data/model/adjustment/promotion_adjustment_test.exs +++ b/apps/snitch_core/test/data/model/adjustment/promotion_adjustment_test.exs @@ -4,7 +4,7 @@ defmodule Snitch.Data.Model.PromotionAdjustmentTest do use ExUnit.Case use Snitch.DataCase import Snitch.Factory - alias Snitch.Data.Model.{Promotion, PromotionAdjustment} + alias Snitch.Data.Model.{Promotion, PromotionAdjustment, Adjustment} describe "promotion adjustment queries" do setup do @@ -46,8 +46,9 @@ defmodule Snitch.Data.Model.PromotionAdjustmentTest do test "order_adjustments_for_promotion/2", context do %{order: order, promotion1: promotion1, promotion2: promotion2} = context - # since promotion is applied twice there should be 8 adjustments + # since two promotions are applied twice there should be 8 adjustments # promotion1 -> 1 order + 3 lineitems = 4 + # promotion2 -> 1 order + 3 lineitems = 4 Promotion.activate?(order, promotion1) Promotion.activate?(order, promotion2) @@ -72,8 +73,6 @@ defmodule Snitch.Data.Model.PromotionAdjustmentTest do data = PromotionAdjustment.eligible_order_adjustments(order) assert data == [] - current_adj_ids = Enum.map(adjustments, fn adjustment -> adjustment.id end) - # activate adjustments for promotion1 {:ok, _data} = PromotionAdjustment.process_adjustments(order, promotion1) diff --git a/apps/snitch_core/test/data/model/order_test.exs b/apps/snitch_core/test/data/model/order_test.exs index 3c0412cdf..1171cfbb6 100644 --- a/apps/snitch_core/test/data/model/order_test.exs +++ b/apps/snitch_core/test/data/model/order_test.exs @@ -130,7 +130,7 @@ defmodule Snitch.Data.Model.OrderTest do end describe "order" do - test "count by state", %{order_params: params, variants: vs} do + test "count by state", %{order_params: params} do {:ok, order} = Order.create(params) Order.partial_update(order, %{state: :confirmed}) @@ -148,7 +148,7 @@ defmodule Snitch.Data.Model.OrderTest do assert order_state_count.state == :confirmed end - test "count by date", %{order_params: params, variants: vs} do + test "count by date", %{order_params: params} do {:ok, order} = Order.create(params) next_date = @@ -165,6 +165,24 @@ defmodule Snitch.Data.Model.OrderTest do end end + describe "user_order/1" do + test "returns same order if, it's in non complete state" do + user = insert(:user) + order = insert(:order, user: user, state: :cart) + + assert {:ok, user_order} = Order.user_order(user.id) + assert user_order.id == order.id + end + + test "returns new order if, order in any completed state" do + user = insert(:user) + order = insert(:order, user: user, state: :confirmed) + + assert {:ok, user_order} = Order.user_order(user.id) + assert user_order.id != order.id + end + end + defp get_naive_date_time(date) do Date.from_iso8601(date) |> elem(1) diff --git a/apps/snitch_core/test/data/model/promotion/eligibility_test.exs b/apps/snitch_core/test/data/model/promotion/eligibility_test.exs index 70ed6f2c5..c64900f89 100644 --- a/apps/snitch_core/test/data/model/promotion/eligibility_test.exs +++ b/apps/snitch_core/test/data/model/promotion/eligibility_test.exs @@ -42,7 +42,7 @@ defmodule Snitch.Data.Model.Promotion.EligibilityTest do describe "order_level_check/2" do test "fails as order not in valid state" do - order = insert(:order, state: :cart) + order = insert(:order, state: :confirmed) promotion = insert(:promotion, match_policy: "all") assert {false, message} = Eligibility.order_level_check(order, promotion) diff --git a/apps/snitch_core/test/data/model/promotion/order_eligibility_test.exs b/apps/snitch_core/test/data/model/promotion/order_eligibility_test.exs index 14d0b8659..73fb45bfa 100644 --- a/apps/snitch_core/test/data/model/promotion/order_eligibility_test.exs +++ b/apps/snitch_core/test/data/model/promotion/order_eligibility_test.exs @@ -21,7 +21,7 @@ defmodule Snitch.Data.Model.Promotion.OrderEligibilityTest do end test "fails if order state not valid for promotion" do - order = insert(:order, state: :cart) + order = insert(:order, state: :confirmed) assert {false, message} = OrderEligibility.valid_order_state(order) assert message == "promotion not applicable to order" diff --git a/apps/snitch_core/test/data/model/promotion/promotion_test.exs b/apps/snitch_core/test/data/model/promotion/promotion_test.exs index ede129b96..332eb3632 100644 --- a/apps/snitch_core/test/data/model/promotion/promotion_test.exs +++ b/apps/snitch_core/test/data/model/promotion/promotion_test.exs @@ -337,14 +337,11 @@ defmodule Snitch.Data.Model.PromotionTest do assert message == @messages.coupon_applied old_adjustments = PromotionAdjustment.order_adjustments_for_promotion(order, promotion) + assert old_adjustments == [] new_adjustments = PromotionAdjustment.order_adjustments_for_promotion(order, promotion_other) - Enum.each(old_adjustments, fn adjustment -> - assert adjustment.eligible == false - end) - Enum.each(new_adjustments, fn adjustment -> assert adjustment.eligible == true end) diff --git a/apps/snitch_core/test/domain/promotion/cart_test.exs b/apps/snitch_core/test/domain/promotion/cart_test.exs new file mode 100644 index 000000000..e5498da72 --- /dev/null +++ b/apps/snitch_core/test/domain/promotion/cart_test.exs @@ -0,0 +1,151 @@ +defmodule Snitch.Domain.Promotion.CartHelperTest do + use ExUnit.Case + use Snitch.DataCase + import Snitch.Factory + alias Snitch.Domain.Promotion.CartHelper + alias Snitch.Data.Model.{Promotion, PromotionAdjustment, LineItem} + + describe "evaluate_promotion/1" do + test "all adjustments are removed if promotion becomes ineligible" do + item_info = %{quantity: 2, price: Money.new!(currency(), 10)} + + %{order: order, line_items: line_items, order_total: order_total, product_ids: product_ids} = + setup_order(item_info) + + [line_item_1, _, _] = line_items + + order_params = %{order: order, order_total: order_total, product_ids: product_ids} + apply_promotion(order_params) + + adjustments = PromotionAdjustment.order_adjustments(order) + + assert adjustments != [] + + Enum.each(adjustments, fn adj -> + assert adj.eligible == true + end) + + # Remove a line item such that order becomes ineligible for promotion. + LineItem.delete(line_item_1) + + assert {:ok, _data} = CartHelper.evaluate_promotion(order) + adjustments = PromotionAdjustment.order_adjustments(order) + assert adjustments == [] + end + + test "returns 'no promotion found' if promotion not applied on order" do + item_info = %{quantity: 2, price: Money.new!(currency(), 10)} + %{order: order} = setup_order(item_info) + + assert {:error, message} = CartHelper.evaluate_promotion(order) + assert message == "no promotion applied to order" + end + + test "new adjustments created for order as line items touched" do + item_info = %{quantity: 2, price: Money.new!(currency(), 10)} + + %{order: order, line_items: line_items, order_total: order_total, product_ids: product_ids} = + setup_order(item_info) + + [line_item_1, _, _] = line_items + + order_params = %{order: order, order_total: order_total, product_ids: product_ids} + apply_promotion(order_params) + + adjustments = PromotionAdjustment.order_adjustments(order) + + assert adjustments != [] + + Enum.each(adjustments, fn adj -> + assert adj.eligible == true + end) + + LineItem.update(line_item_1, %{quantity: 3}) + assert {:ok, _data} = CartHelper.evaluate_promotion(order) + + new_adjustments = PromotionAdjustment.order_adjustments(order) + assert MapSet.disjoint?(MapSet.new(adjustments), MapSet.new(new_adjustments)) + end + end + + defp apply_promotion(order_params) do + action_manifest = %{order_action: Decimal.new(10), line_item_action: Decimal.new(10)} + %{order: order, order_total: order_total, product_ids: product_ids} = order_params + + item_total_cost = Decimal.sub(order_total.amount, 1) + + rule_manifest = %{item_total_cost: item_total_cost, product_ids: product_ids} + + promotion = insert(:promotion) + set_rules_and_actions(promotion, rule_manifest, action_manifest) + + {:ok, _message} = Promotion.apply(order, promotion.code) + + %{promotion: promotion} + end + + defp set_rules_and_actions(promotion, rule_manifest, action_manifest) do + %{item_total_cost: cost, product_ids: product_ids} = rule_manifest + %{order_action: order_action_data, line_item_action: line_item_action_data} = action_manifest + + insert(:order_total_rule, + promotion: promotion, + preferences: %{lower_range: cost, upper_range: Decimal.new(0)} + ) + + insert(:product_rule, + promotion: promotion, + preferences: %{match_policy: "all", product_list: product_ids} + ) + + insert(:promotion_order_action, + preferences: %{ + calculator_module: "Elixir.Snitch.Domain.Calculator.FlatRate", + calculator_preferences: %{amount: order_action_data} + }, + promotion: promotion + ) + + insert(:promotion_line_item_action, + preferences: %{ + calculator_module: "Elixir.Snitch.Domain.Calculator.FlatRate", + calculator_preferences: %{amount: line_item_action_data} + }, + promotion: promotion + ) + end + + defp setup_order(item_info) do + %{quantity: quantity, price: price} = item_info + + products = insert_list(3, :product, promotionable: true, selling_price: price) + + order = insert(:order, state: "delivery") + + line_items = + Enum.map(products, fn product -> + insert(:line_item, + order: order, + product: product, + quantity: quantity, + unit_price: product.selling_price + ) + end) + + cost = + Enum.reduce(line_items, Money.new!(currency(), 0), fn item, acc -> + sum = Money.mult!(item.unit_price, item.quantity) + Money.add!(acc, sum) + end) + + product_ids = Enum.map(products, fn product -> product.id end) + + %{ + order: order, + line_items: line_items, + products: products, + product_ids: product_ids, + order_total: cost + } + end +end