berrypod/lib/simpleshop_theme/cart.ex

245 lines
6.4 KiB
Elixir
Raw Normal View History

defmodule SimpleshopTheme.Cart do
@moduledoc """
The Cart context.
Manages shopping cart operations stored in session. Cart items are stored
as a list of {variant_id, quantity} tuples for minimal session storage.
Items are hydrated with full product/variant data when needed for display.
"""
alias SimpleshopTheme.Products
alias SimpleshopTheme.Products.ProductImage
@session_key "cart"
# =============================================================================
# Session Operations
# =============================================================================
@doc """
Gets cart items from session.
Returns a list of {variant_id, quantity} tuples.
"""
def get_from_session(session) do
case Map.get(session, @session_key) do
nil -> []
items when is_list(items) -> items
_ -> []
end
end
@doc """
Puts cart items in session via Plug.Conn.
Used by the CartController to persist cart to session cookie.
"""
def put_in_session(conn, cart_items) do
Plug.Conn.put_session(conn, @session_key, cart_items)
end
# =============================================================================
# Cart Operations
# =============================================================================
@doc """
Adds an item to the cart.
If the variant is already in the cart, increments the quantity.
Returns the updated cart items list.
"""
def add_item(cart_items, variant_id, quantity \\ 1)
when is_integer(quantity) and quantity > 0 do
case List.keyfind(cart_items, variant_id, 0) do
nil ->
cart_items ++ [{variant_id, quantity}]
{^variant_id, existing_qty} ->
List.keyreplace(cart_items, variant_id, 0, {variant_id, existing_qty + quantity})
end
end
@doc """
Updates the quantity of an item in the cart.
If quantity is 0 or less, removes the item.
Returns the updated cart items list.
"""
def update_quantity(cart_items, variant_id, quantity) when is_integer(quantity) do
if quantity <= 0 do
remove_item(cart_items, variant_id)
else
case List.keyfind(cart_items, variant_id, 0) do
nil ->
cart_items
{^variant_id, _} ->
List.keyreplace(cart_items, variant_id, 0, {variant_id, quantity})
end
end
end
@doc """
Removes an item from the cart.
Returns the updated cart items list.
"""
def remove_item(cart_items, variant_id) do
List.keydelete(cart_items, variant_id, 0)
end
@doc """
Gets the quantity of a specific variant in the cart.
Returns 0 if not found.
"""
def get_quantity(cart_items, variant_id) do
case List.keyfind(cart_items, variant_id, 0) do
nil -> 0
{_, qty} -> qty
end
end
# =============================================================================
# Hydration
# =============================================================================
@doc """
Hydrates cart items with full variant and product data.
Takes a list of {variant_id, quantity} tuples and returns a list of maps
with full display data including product name, variant options, price, and image.
"""
def hydrate(cart_items) when is_list(cart_items) do
variant_ids = Enum.map(cart_items, fn {id, _qty} -> id end)
if variant_ids == [] do
[]
else
variants_map = Products.get_variants_with_products(variant_ids)
cart_items
|> Enum.map(fn {variant_id, quantity} ->
case Map.get(variants_map, variant_id) do
nil ->
nil
variant ->
%{
variant_id: variant.id,
product_id: variant.product.id,
name: variant.product.title,
variant: format_variant_options(variant.options),
price: variant.price,
quantity: quantity,
image: variant_image_url(variant.product)
}
end
end)
|> Enum.reject(&is_nil/1)
end
end
defp format_variant_options(options) when is_map(options) and map_size(options) > 0 do
options
|> Map.values()
|> Enum.join(" / ")
end
defp format_variant_options(_), do: nil
defp variant_image_url(product) do
case product.images do
[first | _] -> ProductImage.url(first, 400)
_ -> nil
end
end
# =============================================================================
# Helpers
# =============================================================================
@doc """
Builds the full display state for a cart.
Takes raw cart items (list of {variant_id, quantity} tuples) and returns
a map with hydrated items, count, and formatted subtotal. Single source
of truth for cart state computation used by CartHook.
"""
def build_state(raw_cart) do
hydrated = hydrate(raw_cart)
%{
items: hydrated,
count: item_count(raw_cart),
subtotal: format_subtotal(hydrated)
}
end
@doc """
Returns the total item count in the cart.
"""
def item_count(cart_items) do
Enum.reduce(cart_items, 0, fn {_, qty}, acc -> acc + qty end)
end
@doc """
Calculates the subtotal from hydrated cart items.
Returns the total in pence.
"""
def calculate_subtotal(hydrated_items) do
Enum.reduce(hydrated_items, 0, fn item, acc ->
acc + item.price * item.quantity
end)
end
@doc """
Formats a price in pence as a currency string using ex_money.
"""
def format_price(price_pence) when is_integer(price_pence) do
price_pence
|> Decimal.new()
|> Decimal.div(100)
|> then(&Money.new(:GBP, &1))
|> Money.to_string!()
end
def format_price(_), do: format_price(0)
@doc """
Formats the subtotal from hydrated items as a GBP string.
"""
def format_subtotal(hydrated_items) do
hydrated_items
|> calculate_subtotal()
|> format_price()
end
@doc """
Serializes cart items for JSON transport.
Converts {variant_id, quantity} tuples to [variant_id, quantity] lists
for JSON compatibility.
"""
def serialize(cart_items) do
Enum.map(cart_items, fn {id, qty} -> [id, qty] end)
end
@doc """
Deserializes cart items from JSON transport.
Converts [variant_id, quantity] lists back to {variant_id, quantity} tuples.
"""
def deserialize(items) when is_list(items) do
items
|> Enum.map(fn
[id, qty] when is_binary(id) and is_integer(qty) -> {id, qty}
_ -> nil
end)
|> Enum.reject(&is_nil/1)
end
def deserialize(_), do: []
end