Skip to content

Commit

Permalink
Promotion re-evaluation for line_item CRUD
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
arjun289 committed Feb 19, 2019
1 parent 848b809 commit 226e1b3
Show file tree
Hide file tree
Showing 15 changed files with 390 additions and 48 deletions.
38 changes: 21 additions & 17 deletions apps/snitch_api/lib/snitch_api/order/order.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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),
Expand Down Expand Up @@ -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]]])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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} ->
Expand Down Expand Up @@ -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}
Expand Down
2 changes: 1 addition & 1 deletion apps/snitch_core/lib/core/data/model/line_item.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions apps/snitch_core/lib/core/data/model/order.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 3 additions & 2 deletions apps/snitch_core/lib/core/data/model/promotion/promotion.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 ->
Expand Down
149 changes: 149 additions & 0 deletions apps/snitch_core/lib/core/domain/promotion/cart.ex
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 226e1b3

Please sign in to comment.