Skip to content

Commit f024b0c

Browse files
committed
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.
1 parent d33176c commit f024b0c

File tree

15 files changed

+390
-48
lines changed

15 files changed

+390
-48
lines changed

apps/snitch_api/lib/snitch_api/order/order.ex

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,26 @@ defmodule SnitchApi.Order do
4343
Repo.preload(order, line_items: line_item_query)
4444
end
4545

46+
####################### line item helpers ############################
47+
48+
def add_to_cart(line_item) do
49+
case get_line_item(line_item["order_id"], line_item["product_id"]) do
50+
[] -> LineItemModel.create(line_item)
51+
[l | _] -> update_line_item(l, line_item)
52+
end
53+
end
54+
55+
defp get_line_item(order_id, product_id) do
56+
query = from(l in LineItem, where: l.order_id == ^order_id and l.product_id == ^product_id)
57+
Repo.all(query)
58+
end
59+
60+
defp update_line_item(line_item, params) do
61+
new_count = line_item.quantity + params["quantity"]
62+
new_params = %{params | "quantity" => new_count}
63+
LineItemModel.update(line_item, new_params)
64+
end
65+
4666
def delete_line_item(line_item_id) do
4767
with {:ok, %LineItem{} = line_item} <- LineItemModel.get(line_item_id),
4868
{:ok, _} <- LineItemModel.delete(line_item) do
@@ -52,12 +72,7 @@ defmodule SnitchApi.Order do
5272
end
5373
end
5474

55-
def add_to_cart(line_item) do
56-
case get_line_item(line_item["order_id"], line_item["product_id"]) do
57-
[] -> LineItemModel.create(line_item)
58-
[l | _] -> update_line_item(l, line_item)
59-
end
60-
end
75+
#################### order transition helpers #######################
6176

6277
def add_payment(order_id, payment_method_id) do
6378
with {:ok, order} <- Order.get(order_id),
@@ -132,17 +147,6 @@ defmodule SnitchApi.Order do
132147
end
133148
end
134149

135-
defp get_line_item(order_id, product_id) do
136-
query = from(l in LineItem, where: l.order_id == ^order_id and l.product_id == ^product_id)
137-
Repo.all(query)
138-
end
139-
140-
defp update_line_item(line_item, params) do
141-
new_count = line_item.quantity + params["quantity"]
142-
new_params = %{params | "quantity" => new_count}
143-
LineItemModel.update(line_item, new_params)
144-
end
145-
146150
defp transition_response(%Context{errors: nil}, order_id) do
147151
{:ok, order} = Order.get(order_id)
148152
order = order |> Repo.preload(line_items: [product: [:theme, [options: :option_type]]])

apps/snitch_api/lib/snitch_api_web/controllers/line_item_controller.ex

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ defmodule SnitchApiWeb.LineItemController do
33

44
alias Snitch.Data.Model.LineItem, as: LineItemModel
55
alias Snitch.Data.Model.Order, as: OrderModel
6-
alias Snitch.Data.Schema.{LineItem, Variant, Product}
6+
alias Snitch.Data.Schema.{LineItem, Product}
77
alias Snitch.Core.Tools.MultiTenancy.Repo
8-
import Ecto.Query
98
alias SnitchApi.Order, as: OrderContext
109

1110
plug(SnitchApiWeb.Plug.DataToAttributes)
@@ -35,7 +34,7 @@ defmodule SnitchApiWeb.LineItemController do
3534

3635
def guest_line_item(conn, %{"order_id" => order_id} = params) do
3736
with {:ok, line_item} <- add_line_item(params) do
38-
order = OrderContext.load_order(line_item.order_id)
37+
order = OrderContext.load_order(order_id)
3938

4039
conn
4140
|> put_status(200)

apps/snitch_core/lib/core/data/model/adjustment/promotion_adjustment.ex

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ defmodule Snitch.Data.Model.PromotionAdjustment do
5959
Repo.all(query)
6060
end
6161

62+
@doc """
63+
Returns all adjustments for the supplied `order` and `promotion`.
64+
"""
6265
def order_adjustments_for_promotion(order, promotion) do
6366
query =
6467
from(adj in AdjustmentSchema,
@@ -98,11 +101,11 @@ defmodule Snitch.Data.Model.PromotionAdjustment do
98101
@doc """
99102
Processes adjustments for the supplied `order` and `promotion`.
100103
101-
Adjustments are created due for all the `actions` of a `promotion`
102-
for an order. However, these adjustments are created in an ineligible state,
104+
Adjustments are created for all the `actions` of a `promotion`
105+
on an order. However, these adjustments are created in an ineligible state,
103106
handled by the `eligible` field in `Adjustments` which is initially false.
104107
105-
The `eligible` field is marked true subject to condition that a better promotion
108+
The `eligible` field is marked true subject to condition that, a better promotion
106109
doesn't exist already for the order.
107110
In case a better promotion exists `{:error, message}` tuple is returned.
108111
Otherwise, the adjustments due to previous promotion are marked as ineligible
@@ -122,6 +125,7 @@ defmodule Snitch.Data.Model.PromotionAdjustment do
122125

123126
if current_discount > previous_discount do
124127
case activate_adjustments(
128+
order,
125129
prev_eligible_ids,
126130
current_adjustment_ids
127131
) do
@@ -138,14 +142,23 @@ defmodule Snitch.Data.Model.PromotionAdjustment do
138142

139143
################## private functions ################
140144

141-
defp activate_adjustments(prev_eligible_ids, current_adjustment_ids) do
145+
defp activate_adjustments(order, prev_eligible_ids, current_adjustment_ids) do
142146
Multi.new()
143-
|> Multi.run(:remove_eligible_adjustments, fn _ ->
147+
|> Multi.run(:remove_previous_eligible_adjustments, fn _ ->
144148
{:ok, update_adjustments(prev_eligible_ids, false)}
145149
end)
146150
|> Multi.run(:activate_new_adjustments, fn _ ->
147151
{:ok, update_adjustments(current_adjustment_ids, true)}
148152
end)
153+
|> Multi.delete_all(
154+
:delete_ineligible_adjustments,
155+
from(
156+
adj in AdjustmentSchema,
157+
join: p_adj in PromotionAdjustment,
158+
on: p_adj.adjustment_id == adj.id,
159+
where: p_adj.order_id == ^order.id and adj.eligible == false
160+
)
161+
)
149162
|> Repo.transaction()
150163
|> case do
151164
{:ok, data} ->
@@ -215,8 +228,8 @@ defmodule Snitch.Data.Model.PromotionAdjustment do
215228
# Run the accumulated multi struct
216229
defp persist(multi) do
217230
case Repo.transaction(multi) do
218-
{:ok, %{promo_adj: promo_adjustment}} ->
219-
{:ok, promo_adjustment}
231+
{:ok, %{adjustment: adjustment}} ->
232+
{:ok, adjustment}
220233

221234
{:error, _, data, _} ->
222235
{:error, data}

apps/snitch_core/lib/core/data/model/line_item.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ defmodule Snitch.Data.Model.LineItem do
66

77
import Ecto.Changeset, only: [change: 1]
88

9-
alias Snitch.Data.Model.{Variant, Product}
9+
alias Snitch.Data.Model.Product
1010
alias Snitch.Data.Schema.LineItem
1111
alias Snitch.Domain.Order
1212
alias Snitch.Tools.Money, as: MoneyTools

apps/snitch_core/lib/core/data/model/order.ex

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ defmodule Snitch.Data.Model.Order do
99
alias Snitch.Data.Model.LineItem, as: LineItemModel
1010

1111
@order_states ["confirmed", "complete"]
12+
@non_complete_order_states ~w(cart address delivery payment)a
13+
1214
@doc """
1315
Creates an order with supplied `params` and `line_items`.
1416
@@ -44,17 +46,16 @@ defmodule Snitch.Data.Model.Order do
4446
@doc """
4547
Returns an `order` struct for the supplied `user_id`.
4648
47-
An existing `order` associated with the user is present in either `cart` or
48-
`address` state is returned if found. If no order is present for the user
49-
in `cart` or `address` state a new order is created returned.
49+
An existing `order` associated with the user present in "#{inspect(@non_complete_order_states)}"
50+
state is returned if found. If no order is present for the user
51+
in these states, a new order is created and returned.
5052
"""
5153
@spec user_order(non_neg_integer) :: Order.t()
5254
def user_order(user_id) do
5355
query =
5456
from(
5557
order in Order,
56-
where:
57-
order.user_id == ^user_id and order.state in ["cart", "address", "delivery", "payment"]
58+
where: order.user_id == ^user_id and order.state in ^@non_complete_order_states
5859
)
5960

6061
query

apps/snitch_core/lib/core/data/model/promotion/order_eligibility.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ defmodule Snitch.Data.Model.Promotion.OrderEligibility do
66
use Snitch.Data.Model
77
alias Snitch.Data.Model.PromotionAdjustment
88

9-
@valid_order_states ~w(delivery address)a
9+
@valid_order_states ~w(delivery address cart)a
1010
@success_message "promotion applicable"
1111
@error_message "coupon not applicable"
1212
@coupon_applied "coupon already applied"

apps/snitch_core/lib/core/data/model/promotion/promotion.ex

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,9 +194,9 @@ defmodule Snitch.Data.Model.Promotion do
194194
end
195195
end
196196

197-
def update_usage_count(promotion) do
197+
def update_usage_count(promotion, count) do
198198
current_usage_count = promotion.current_usage_count
199-
params = %{current_usage_count: current_usage_count + 1}
199+
params = %{current_usage_count: current_usage_count + count}
200200

201201
QH.update(Promotion, params, promotion, Repo)
202202
end
@@ -216,6 +216,7 @@ defmodule Snitch.Data.Model.Promotion do
216216
defp process_adjustments(order, promotion) do
217217
case PromotionAdjustment.process_adjustments(order, promotion) do
218218
{:ok, _data} ->
219+
update_usage_count(promotion, 1)
219220
{:ok, @messages.coupon_applied}
220221

221222
{:error, _message} = error ->
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
defmodule Snitch.Domain.Promotion.CartHelper do
2+
@moduledoc """
3+
Module exposes functions to handle promotion related checks on
4+
an order.
5+
6+
If an order has any promotion associated with it then, on addition, updation
7+
or removal of lineitem it may become eligible or ineligible for the promotion.
8+
"""
9+
10+
import Ecto.Query
11+
alias Ecto.Multi
12+
alias Snitch.Core.Tools.MultiTenancy.Repo
13+
alias Snitch.Data.Schema.{Adjustment, Promotion, PromotionAdjustment}
14+
alias Snitch.Data.Model.Promotion.Eligibility
15+
alias Snitch.Data.Model.Promotion, as: PromotionModel
16+
alias Snitch.Data.Model.PromotionAdjustment, as: PromoAdjModel
17+
alias Snitch.Data.Model.Promotion.OrderEligibility
18+
19+
@success_message "coupon applied"
20+
@failure_message "promotion not applicable"
21+
22+
@doc """
23+
Revaluates the promotion for order for which the line_item was
24+
touched(created, updated or removed).
25+
26+
Finds out the `promotion` for which the `order` has active promotios.
27+
28+
> Active promotions for an order is found out by checking if `eligible` promotion related
29+
> adjustments are present for the order.
30+
> See Snitch.Data.Model.PromotionAdjustment
31+
"""
32+
@spec evaluate_promotion(Order.t()) :: {:ok, map} | {:error, String.t()}
33+
def evaluate_promotion(order) do
34+
case get_promotion(order) do
35+
nil ->
36+
{:error, "no promotion applied to order"}
37+
38+
promotion ->
39+
process_adjustments(order, promotion)
40+
end
41+
end
42+
43+
# Handles adjustments for the promotion returned for the order. If the order
44+
# becomes ineligible for the promotion then removes all the records for the order
45+
# and the promotion. In case order is still eligible for promotion new adjustments
46+
# as line item has modified the order and promotion actions will act differently
47+
# on the modified order. The older adjustments existing for the order are removed.
48+
defp process_adjustments(order, promotion) do
49+
with {:ok, _message} <- eligibility_checks(order, promotion) do
50+
if PromotionModel.activate?(order, promotion) do
51+
activate_recent_adjustments(order, promotion)
52+
end
53+
else
54+
{:error, _message} ->
55+
remove_adjustments(order, promotion)
56+
end
57+
end
58+
59+
defp remove_adjustments(order, promotion) do
60+
Multi.new()
61+
|> Multi.delete_all(
62+
:remove_adjustments_for_promotion,
63+
from(
64+
adj in Adjustment,
65+
join: p_adj in PromotionAdjustment,
66+
on: p_adj.adjustment_id == adj.id,
67+
where: p_adj.order_id == ^order.id and p_adj.promotion_id == ^promotion.id
68+
)
69+
)
70+
|> Multi.run(:update_promotion_count, fn _ ->
71+
PromotionModel.update_usage_count(promotion, -1)
72+
end)
73+
|> persist()
74+
end
75+
76+
defp activate_recent_adjustments(order, promotion) do
77+
adjustments = PromoAdjModel.order_adjustments_for_promotion(order, promotion)
78+
79+
old_adjustment_ids =
80+
Enum.flat_map(adjustments, fn adj ->
81+
if adj.eligible == true, do: [adj.id], else: []
82+
end)
83+
84+
new_adjustment_ids =
85+
Enum.flat_map(adjustments, fn adj ->
86+
if adj.eligible == false, do: [adj.id], else: []
87+
end)
88+
89+
Multi.new()
90+
|> Multi.delete_all(
91+
:remove_old_adjustments,
92+
from(adj in Adjustment, where: adj.id in ^old_adjustment_ids)
93+
)
94+
|> Multi.update_all(
95+
:mark_new_adjustment_as_eligible,
96+
from(adj in Adjustment, where: adj.id in ^new_adjustment_ids),
97+
set: [eligible: true]
98+
)
99+
|> persist()
100+
end
101+
102+
defp get_promotion(order) do
103+
case get_active_promotion_id(order) do
104+
nil ->
105+
nil
106+
107+
id ->
108+
Repo.get(Promotion, id)
109+
end
110+
end
111+
112+
defp get_active_promotion_id(order) do
113+
query =
114+
from(adj in Adjustment,
115+
join: p_adj in PromotionAdjustment,
116+
on: adj.id == p_adj.adjustment_id,
117+
where: p_adj.order_id == ^order.id and adj.eligible == true,
118+
limit: 1,
119+
select: p_adj.promotion_id
120+
)
121+
122+
Repo.one(query)
123+
end
124+
125+
# Run the accumulated multi struct
126+
defp persist(multi) do
127+
case Repo.transaction(multi) do
128+
{:ok, data} ->
129+
{:ok, data}
130+
131+
{:error, _, data, _} ->
132+
{:error, data}
133+
end
134+
end
135+
136+
defp eligibility_checks(order, promotion) do
137+
with true <- Eligibility.promotion_level_check(promotion),
138+
{true, _message} <- OrderEligibility.order_promotionable(order),
139+
{true, _message} <- OrderEligibility.rules_check(order, promotion) do
140+
{:ok, @success_message}
141+
else
142+
{false, message} ->
143+
{:error, message}
144+
145+
false ->
146+
{:error, @failure_message}
147+
end
148+
end
149+
end
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
defmodule Snitch.Repo.Migrations.AlterPromoAdjustmentsCascadeDelete do
2+
use Ecto.Migration
3+
4+
def change do
5+
execute "ALTER TABLE snitch_promotion_adjustments DROP CONSTRAINT snitch_promotion_adjustments_adjustment_id_fkey"
6+
alter table("snitch_promotion_adjustments") do
7+
modify(:adjustment_id, references("snitch_adjustments", on_delete: :delete_all))
8+
end
9+
end
10+
end

0 commit comments

Comments
 (0)