Packages

phoenix_kit

1.7.38
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
phoenix_kit lib modules shop schemas shipping_method.ex
Raw

lib/modules/shop/schemas/shipping_method.ex

defmodule PhoenixKit.Modules.Shop.ShippingMethod do
@moduledoc """
Shipping method schema for E-Commerce module.
Supports weight-based, price-based, and geographic restrictions.
## Fields
- `name` - Method name (required)
- `slug` - URL-friendly identifier (unique, auto-generated)
- `description` - Method description
- `price` - Shipping cost
- `free_above_amount` - Free shipping threshold
- `min_weight_grams`, `max_weight_grams` - Weight limits
- `min_order_amount`, `max_order_amount` - Order amount limits
- `countries` - Allowed countries (empty = all)
- `excluded_countries` - Excluded countries
- `active` - Enabled/disabled
- `position` - Sort order
- `estimated_days_min`, `estimated_days_max` - Delivery estimate
- `tracking_supported` - Tracking available
"""
use Ecto.Schema
import Ecto.Changeset
@primary_key {:uuid, UUIDv7, autogenerate: true}
schema "phoenix_kit_shop_shipping_methods" do
field :id, :integer, read_after_writes: true
field :name, :string
field :slug, :string
field :description, :string
# Pricing
field :price, :decimal, default: Decimal.new("0")
field :currency, :string, default: "USD"
field :free_above_amount, :decimal
# Constraints
field :min_weight_grams, :integer, default: 0
field :max_weight_grams, :integer
field :min_order_amount, :decimal
field :max_order_amount, :decimal
# Geographic
field :countries, {:array, :string}, default: []
field :excluded_countries, {:array, :string}, default: []
# Status
field :active, :boolean, default: true
field :position, :integer, default: 0
# Delivery info
field :estimated_days_min, :integer
field :estimated_days_max, :integer
field :tracking_supported, :boolean, default: false
field :metadata, :map, default: %{}
timestamps()
end
@required_fields [:name, :price]
@optional_fields [
:slug,
:description,
:currency,
:free_above_amount,
:min_weight_grams,
:max_weight_grams,
:min_order_amount,
:max_order_amount,
:countries,
:excluded_countries,
:active,
:position,
:estimated_days_min,
:estimated_days_max,
:tracking_supported,
:metadata
]
@doc """
Changeset for shipping method creation and updates.
"""
def changeset(method, attrs) do
attrs = normalize_booleans(attrs, [:active, :tracking_supported])
method
|> cast(attrs, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
|> validate_length(:name, max: 255)
|> validate_length(:slug, max: 100)
|> validate_length(:currency, is: 3)
|> validate_number(:price, greater_than_or_equal_to: 0)
|> validate_number(:free_above_amount, greater_than: 0)
|> validate_number(:min_weight_grams, greater_than_or_equal_to: 0)
|> validate_number(:max_weight_grams, greater_than: 0)
|> validate_number(:min_order_amount, greater_than: 0)
|> validate_number(:max_order_amount, greater_than: 0)
|> validate_number(:position, greater_than_or_equal_to: 0)
|> validate_number(:estimated_days_min, greater_than_or_equal_to: 0)
|> validate_number(:estimated_days_max, greater_than: 0)
|> maybe_generate_slug()
|> unique_constraint(:slug)
end
@doc """
Checks if this method is available for given cart parameters.
## Examples
iex> available_for?(method, %{weight_grams: 500, subtotal: Decimal.new("50"), country: "EE"})
true
"""
def available_for?(%__MODULE__{active: false}, _params), do: false
def available_for?(%__MODULE__{} = method, %{
weight_grams: weight,
subtotal: subtotal,
country: country
}) do
weight_ok?(method, weight) &&
amount_ok?(method, subtotal) &&
country_ok?(method, country)
end
def available_for?(%__MODULE__{} = method, params) when is_map(params) do
weight = Map.get(params, :weight_grams, 0)
subtotal = Map.get(params, :subtotal, Decimal.new("0"))
country = Map.get(params, :country)
available_for?(method, %{weight_grams: weight, subtotal: subtotal, country: country})
end
@doc """
Calculates shipping cost for given subtotal.
Returns 0 if free shipping threshold is met.
"""
def calculate_cost(%__MODULE__{free_above_amount: nil, price: price}, _subtotal) do
price
end
def calculate_cost(%__MODULE__{free_above_amount: threshold, price: price}, subtotal) do
if Decimal.compare(subtotal, threshold) != :lt do
Decimal.new("0")
else
price
end
end
@doc """
Returns estimated delivery string.
## Examples
iex> delivery_estimate(%ShippingMethod{estimated_days_min: 3, estimated_days_max: 5})
"3-5 days"
iex> delivery_estimate(%ShippingMethod{estimated_days_min: 1, estimated_days_max: 1})
"1 day"
"""
def delivery_estimate(%__MODULE__{estimated_days_min: nil}), do: nil
def delivery_estimate(%__MODULE__{estimated_days_min: min, estimated_days_max: nil}) do
"#{min}+ days"
end
def delivery_estimate(%__MODULE__{estimated_days_min: 1, estimated_days_max: 1}) do
"1 day"
end
def delivery_estimate(%__MODULE__{estimated_days_min: min, estimated_days_max: max})
when min == max do
"#{min} days"
end
def delivery_estimate(%__MODULE__{estimated_days_min: min, estimated_days_max: max}) do
"#{min}-#{max} days"
end
@doc """
Returns true if method is active.
"""
def active?(%__MODULE__{active: true}), do: true
def active?(_), do: false
@doc """
Checks if shipping is free for the given subtotal.
"""
def free_for?(%__MODULE__{free_above_amount: nil}, _subtotal), do: false
def free_for?(%__MODULE__{free_above_amount: threshold}, subtotal) do
Decimal.compare(subtotal, threshold) != :lt
end
# Private helpers
defp weight_ok?(%{min_weight_grams: min, max_weight_grams: max}, weight) do
min_ok = is_nil(min) or weight >= min
max_ok = is_nil(max) or weight <= max
min_ok and max_ok
end
defp amount_ok?(%{min_order_amount: min, max_order_amount: max}, amount) do
min_ok = is_nil(min) or Decimal.compare(amount, min) != :lt
max_ok = is_nil(max) or Decimal.compare(amount, max) != :gt
min_ok and max_ok
end
defp country_ok?(%{countries: [], excluded_countries: []}, _country), do: true
defp country_ok?(%{countries: [], excluded_countries: excluded}, country) do
is_nil(country) or country not in excluded
end
defp country_ok?(%{countries: allowed, excluded_countries: excluded}, country) do
(is_nil(country) or country in allowed) and
(is_nil(country) or country not in excluded)
end
defp maybe_generate_slug(changeset) do
case get_change(changeset, :slug) do
nil ->
case get_change(changeset, :name) do
nil -> changeset
name -> put_change(changeset, :slug, slugify(name))
end
_ ->
changeset
end
end
defp slugify(text) do
text
|> String.downcase()
|> String.replace(~r/[^\w\s-]/, "")
|> String.replace(~r/\s+/, "-")
|> String.replace(~r/-+/, "-")
|> String.trim("-")
end
defp normalize_booleans(attrs, fields) when is_map(attrs) do
Enum.reduce(fields, attrs, fn field, acc ->
str_key = to_string(field)
cond do
Map.has_key?(acc, field) ->
Map.update!(acc, field, &to_boolean/1)
Map.has_key?(acc, str_key) ->
Map.update!(acc, str_key, &to_boolean/1)
true ->
acc
end
end)
end
defp to_boolean(true), do: true
defp to_boolean(false), do: false
defp to_boolean("true"), do: true
defp to_boolean("false"), do: false
defp to_boolean(1), do: true
defp to_boolean(0), do: false
defp to_boolean("1"), do: true
defp to_boolean("0"), do: false
defp to_boolean(nil), do: nil
defp to_boolean(other), do: other
end