Packages
phoenix_kit
1.7.27
1.7.220
1.7.219
1.7.218
1.7.217
1.7.216
1.7.215
1.7.214
1.7.213
1.7.212
1.7.211
1.7.210
1.7.209
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/modules/shop/schemas/cart_item.ex
defmodule PhoenixKit.Modules.Shop.CartItem do
@moduledoc """
Cart item schema with price snapshot for consistency.
When a product is added to the cart, we snapshot the current price and product
details. This ensures that:
1. Price changes after adding don't affect the cart total unexpectedly
2. If the product is deleted, we still have the title and other info
3. We can show users when prices have changed since they added items
## Fields
- `cart_id` - Reference to the cart (required)
- `product_id` - Reference to the product (nullable, ON DELETE SET NULL)
- `product_title` - Product title snapshot (required)
- `product_slug` - Product slug snapshot
- `product_sku` - Product SKU snapshot
- `product_image` - Product image URL snapshot
- `unit_price` - Price per unit at time of adding (required)
- `compare_at_price` - Original price for showing discounts
- `quantity` - Number of items (required, > 0)
- `line_total` - Calculated: unit_price * quantity
- `weight_grams` - Weight for shipping calculation
- `taxable` - Whether item is taxable
- `variant_id` - For future variant support
- `variant_options` - JSON object for variant options (size, color, etc.)
"""
use Ecto.Schema
import Ecto.Changeset
alias PhoenixKit.Modules.Shop.Cart
alias PhoenixKit.Modules.Shop.Product
schema "phoenix_kit_shop_cart_items" do
field :uuid, Ecto.UUID
belongs_to :cart, Cart
belongs_to :product, Product
# Snapshot
field :product_title, :string
field :product_slug, :string
field :product_sku, :string
field :product_image, :string
# Pricing (snapshot)
field :unit_price, :decimal
field :compare_at_price, :decimal
field :currency, :string, default: "USD"
# Quantity
field :quantity, :integer, default: 1
# Calculated
field :line_total, :decimal
# Physical
field :weight_grams, :integer, default: 0
field :taxable, :boolean, default: true
# Variants (for future)
field :variant_id, :integer
field :variant_options, :map, default: %{}
field :metadata, :map, default: %{}
timestamps(type: :utc_datetime)
end
@doc """
Changeset for cart item creation and updates.
"""
def changeset(item, attrs) do
item
|> cast(attrs, [
:cart_id,
:product_id,
:product_title,
:product_slug,
:product_sku,
:product_image,
:unit_price,
:compare_at_price,
:currency,
:quantity,
:line_total,
:weight_grams,
:taxable,
:variant_id,
:variant_options,
:metadata
])
|> validate_required([:cart_id, :product_title, :unit_price, :quantity])
|> validate_number(:quantity, greater_than: 0)
|> validate_number(:unit_price, greater_than_or_equal_to: 0)
|> validate_length(:currency, is: 3)
|> calculate_line_total()
|> maybe_generate_uuid()
|> foreign_key_constraint(:cart_id)
|> foreign_key_constraint(:product_id)
end
@doc """
Creates changeset attributes from a product.
## Examples
iex> from_product(product, 2)
%{
product_id: 1,
product_title: "Widget",
product_slug: "widget",
unit_price: Decimal.new("19.99"),
quantity: 2,
...
}
"""
def from_product(%Product{} = product, quantity \\ 1) do
%{
product_id: product.id,
product_title: product.title,
product_slug: product.slug,
product_image: product.featured_image,
unit_price: product.price,
compare_at_price: product.compare_at_price,
currency: product.currency,
quantity: quantity,
weight_grams: product.weight_grams || 0,
taxable: product.taxable
}
end
@doc """
Returns true if product data has changed since the item was added.
Useful for showing price change warnings.
"""
def product_changed?(%__MODULE__{product_id: nil}, _product), do: true
def product_changed?(%__MODULE__{} = item, %Product{} = product) do
Decimal.compare(item.unit_price, product.price) != :eq
end
@doc """
Returns the price difference if the product price has changed.
Positive = price increased, Negative = price decreased.
"""
def price_difference(%__MODULE__{} = item, %Product{} = product) do
Decimal.sub(product.price, item.unit_price)
end
@doc """
Returns true if this item is on sale (has compare_at_price > unit_price).
"""
def on_sale?(%__MODULE__{compare_at_price: nil}), do: false
def on_sale?(%__MODULE__{compare_at_price: compare, unit_price: price}) do
Decimal.compare(compare, price) == :gt
end
@doc """
Returns discount percentage for sale items.
"""
def discount_percentage(%__MODULE__{} = item) do
if on_sale?(item) do
diff = Decimal.sub(item.compare_at_price, item.unit_price)
diff
|> Decimal.div(item.compare_at_price)
|> Decimal.mult(100)
|> Decimal.round(0)
|> Decimal.to_integer()
else
0
end
end
@doc """
Returns true if the product has been deleted (product_id is nil after SET NULL).
"""
def product_deleted?(%__MODULE__{product_id: nil}), do: true
def product_deleted?(_), do: false
# Private helpers
defp calculate_line_total(changeset) do
quantity = get_field(changeset, :quantity) || 1
unit_price = get_field(changeset, :unit_price) || Decimal.new("0")
line_total = Decimal.mult(unit_price, quantity)
put_change(changeset, :line_total, line_total)
end
defp maybe_generate_uuid(changeset) do
case get_field(changeset, :uuid) do
nil -> put_change(changeset, :uuid, Ecto.UUID.generate())
_ -> changeset
end
end
end