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