Packages

phoenix_kit

1.7.170
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 phoenix_kit migrations postgres v102.ex
Raw

lib/phoenix_kit/migrations/postgres/v102.ex

defmodule PhoenixKit.Migrations.Postgres.V102 do
@moduledoc """
V102: Catalogue discount + smart catalogues.
Two related catalogue features shipped together:
## Discount
Mirrors the markup columns added in V89 (catalogue) and V97 (item override):
- `phoenix_kit_cat_catalogues.discount_percentage DECIMAL(7, 2)
NOT NULL DEFAULT 0` — the catalogue-wide default discount applied on
top of the post-markup sale price.
- `phoenix_kit_cat_items.discount_percentage DECIMAL(7, 2)` — nullable
per-item override. `NULL` = inherit the catalogue's discount; any set
value (including `0`) overrides the catalogue's discount for that item.
The pricing chain becomes `base → markup → discount`:
sale_price = base_price * (1 + effective_markup / 100)
final_price = sale_price * (1 - effective_discount / 100)
## Smart catalogues
A smart catalogue's items reference *other* catalogues with a value +
unit (e.g. a "Delivery" item says "5% of Kitchen, 3% of Plumbing, plus
$20 flat of Hardware"). Consumers do the math; this module stores the
user's intent.
- `phoenix_kit_cat_catalogues.kind VARCHAR(20) NOT NULL DEFAULT 'standard'`
— one of `'standard'` (existing behavior) or `'smart'` (items
reference other catalogues).
- `phoenix_kit_cat_items.default_value DECIMAL(12, 4)` and
`default_unit VARCHAR(20)` (both nullable) — per-item fallback that
applies when a rule row has NULL `value`/`unit`.
- New table `phoenix_kit_cat_item_catalogue_rules` storing one row per
(item, referenced_catalogue) pair, with nullable `value` + `unit`
(inherit from item defaults) and a `position INT` for UI ordering.
Unit vocabulary is open-ended VARCHAR; v1 uses `'percent'` and
`'flat'`. Self- and smart-to-smart references are intentionally
allowed — consumers handle cycles at math time.
All operations are idempotent.
"""
use Ecto.Migration
def up(opts) do
prefix = Map.get(opts, :prefix, "public")
p = prefix_str(prefix)
schema = if prefix == "public", do: "public", else: prefix
# ── Discount columns ─────────────────────────────────────────
execute("""
DO $$
BEGIN
-- Catalogue-wide discount: NOT NULL with a 0 default so all
-- existing rows preserve today's "no discount" behavior.
IF EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = '#{schema}' AND table_name = 'phoenix_kit_cat_catalogues'
) AND NOT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_cat_catalogues'
AND column_name = 'discount_percentage'
) THEN
ALTER TABLE #{p}phoenix_kit_cat_catalogues
ADD COLUMN discount_percentage DECIMAL(7, 2) NOT NULL DEFAULT 0;
END IF;
-- Per-item discount override: nullable with no default. NULL = inherit.
IF EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = '#{schema}' AND table_name = 'phoenix_kit_cat_items'
) AND NOT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_cat_items'
AND column_name = 'discount_percentage'
) THEN
ALTER TABLE #{p}phoenix_kit_cat_items
ADD COLUMN discount_percentage DECIMAL(7, 2);
END IF;
-- Smart-catalogue `kind` flag on catalogues: standard (default) or smart.
IF EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = '#{schema}' AND table_name = 'phoenix_kit_cat_catalogues'
) AND NOT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_cat_catalogues'
AND column_name = 'kind'
) THEN
ALTER TABLE #{p}phoenix_kit_cat_catalogues
ADD COLUMN kind VARCHAR(20) NOT NULL DEFAULT 'standard';
END IF;
-- Per-item default value + unit for smart rules.
IF EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = '#{schema}' AND table_name = 'phoenix_kit_cat_items'
) AND NOT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_cat_items'
AND column_name = 'default_value'
) THEN
ALTER TABLE #{p}phoenix_kit_cat_items
ADD COLUMN default_value DECIMAL(12, 4);
END IF;
IF EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = '#{schema}' AND table_name = 'phoenix_kit_cat_items'
) AND NOT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_cat_items'
AND column_name = 'default_unit'
) THEN
ALTER TABLE #{p}phoenix_kit_cat_items
ADD COLUMN default_unit VARCHAR(20);
END IF;
END $$;
""")
# ── Smart rules table ───────────────────────────────────────
execute("""
CREATE TABLE IF NOT EXISTS #{p}phoenix_kit_cat_item_catalogue_rules (
uuid UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
item_uuid UUID NOT NULL REFERENCES #{p}phoenix_kit_cat_items(uuid) ON DELETE CASCADE,
referenced_catalogue_uuid UUID NOT NULL REFERENCES #{p}phoenix_kit_cat_catalogues(uuid) ON DELETE CASCADE,
value DECIMAL(12, 4),
unit VARCHAR(20),
position INTEGER NOT NULL DEFAULT 0,
inserted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
""")
execute("""
CREATE UNIQUE INDEX IF NOT EXISTS phoenix_kit_cat_item_catalogue_rules_pair_index
ON #{p}phoenix_kit_cat_item_catalogue_rules (item_uuid, referenced_catalogue_uuid)
""")
execute("""
CREATE INDEX IF NOT EXISTS phoenix_kit_cat_item_catalogue_rules_item_index
ON #{p}phoenix_kit_cat_item_catalogue_rules (item_uuid)
""")
execute("""
CREATE INDEX IF NOT EXISTS phoenix_kit_cat_item_catalogue_rules_referenced_index
ON #{p}phoenix_kit_cat_item_catalogue_rules (referenced_catalogue_uuid)
""")
# ── Data-integrity constraints ─────────────────────────────
# `kind` is a closed vocabulary at the DB layer (Ecto validates it
# too, but raw INSERTs from import scripts / IEx must also be
# blocked). `unit` stays open per the smart-catalogue moduledoc —
# consumers can introduce new units without a migration.
execute("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT FROM pg_constraint
WHERE conname = 'phoenix_kit_cat_catalogues_kind_check'
) THEN
ALTER TABLE #{p}phoenix_kit_cat_catalogues
ADD CONSTRAINT phoenix_kit_cat_catalogues_kind_check
CHECK (kind IN ('standard', 'smart'));
END IF;
IF NOT EXISTS (
SELECT FROM pg_constraint
WHERE conname = 'phoenix_kit_cat_catalogues_discount_pct_check'
) THEN
ALTER TABLE #{p}phoenix_kit_cat_catalogues
ADD CONSTRAINT phoenix_kit_cat_catalogues_discount_pct_check
CHECK (discount_percentage >= 0 AND discount_percentage <= 100);
END IF;
IF NOT EXISTS (
SELECT FROM pg_constraint
WHERE conname = 'phoenix_kit_cat_items_discount_pct_check'
) THEN
ALTER TABLE #{p}phoenix_kit_cat_items
ADD CONSTRAINT phoenix_kit_cat_items_discount_pct_check
CHECK (discount_percentage IS NULL OR
(discount_percentage >= 0 AND discount_percentage <= 100));
END IF;
IF NOT EXISTS (
SELECT FROM pg_constraint
WHERE conname = 'phoenix_kit_cat_items_default_value_check'
) THEN
ALTER TABLE #{p}phoenix_kit_cat_items
ADD CONSTRAINT phoenix_kit_cat_items_default_value_check
CHECK (default_value IS NULL OR default_value >= 0);
END IF;
IF NOT EXISTS (
SELECT FROM pg_constraint
WHERE conname = 'phoenix_kit_cat_item_catalogue_rules_value_check'
) THEN
ALTER TABLE #{p}phoenix_kit_cat_item_catalogue_rules
ADD CONSTRAINT phoenix_kit_cat_item_catalogue_rules_value_check
CHECK (value IS NULL OR value >= 0);
END IF;
END $$;
""")
# Partial index: smart catalogues are typically <1% of total
# catalogues but the smart-item edit form filters by them on every
# mount. A standard b-tree on `kind` would be wasteful; this scoped
# index covers the only query that filters by kind.
execute("""
CREATE INDEX IF NOT EXISTS phoenix_kit_cat_catalogues_kind_smart_index
ON #{p}phoenix_kit_cat_catalogues (uuid)
WHERE kind = 'smart'
""")
execute("COMMENT ON TABLE #{p}phoenix_kit IS '102'")
end
@doc """
Rolls V102 back: drops the rules table and both sets of added columns
(discount + smart-catalogue).
**Lossy rollback:** all discount values, smart-catalogue rules, item
defaults, and the `kind` distinction are lost. Back up before rolling
back in production.
"""
def down(opts) do
prefix = Map.get(opts, :prefix, "public")
p = prefix_str(prefix)
# Partial index + CHECK constraints first; PG drops them implicitly with
# DROP COLUMN, but being explicit keeps `down` reversible and idempotent.
execute("DROP INDEX IF EXISTS #{p}phoenix_kit_cat_catalogues_kind_smart_index")
execute("""
ALTER TABLE #{p}phoenix_kit_cat_item_catalogue_rules
DROP CONSTRAINT IF EXISTS phoenix_kit_cat_item_catalogue_rules_value_check
""")
execute("DROP TABLE IF EXISTS #{p}phoenix_kit_cat_item_catalogue_rules")
execute("""
ALTER TABLE #{p}phoenix_kit_cat_items
DROP CONSTRAINT IF EXISTS phoenix_kit_cat_items_default_value_check,
DROP CONSTRAINT IF EXISTS phoenix_kit_cat_items_discount_pct_check
""")
execute("ALTER TABLE #{p}phoenix_kit_cat_items DROP COLUMN IF EXISTS default_unit")
execute("ALTER TABLE #{p}phoenix_kit_cat_items DROP COLUMN IF EXISTS default_value")
execute("""
ALTER TABLE #{p}phoenix_kit_cat_catalogues
DROP CONSTRAINT IF EXISTS phoenix_kit_cat_catalogues_kind_check,
DROP CONSTRAINT IF EXISTS phoenix_kit_cat_catalogues_discount_pct_check
""")
execute("ALTER TABLE #{p}phoenix_kit_cat_catalogues DROP COLUMN IF EXISTS kind")
execute("ALTER TABLE #{p}phoenix_kit_cat_items DROP COLUMN IF EXISTS discount_percentage")
execute(
"ALTER TABLE #{p}phoenix_kit_cat_catalogues DROP COLUMN IF EXISTS discount_percentage"
)
execute("COMMENT ON TABLE #{p}phoenix_kit IS '101'")
end
defp prefix_str("public"), do: "public."
defp prefix_str(prefix), do: "#{prefix}."
end