Five catalog tables:
- plans — plan identity (code, name); the stable thing.
- plan_versions — versioned pricing (base_price_cents, currency,
status draft/active/retired). A subscription binds
to a version; raising prices = publish a new
version, existing subs unaffected until migrated.
- plan_items — what a version includes per resource_kind, plus
overage terms (overage_unit, overage_price_cents,
hard_cap_qty).
- addons — a la carte upgrades (code, resource_kind, qty,
price_cents).
- resource_prices — effective-dated fallback per-unit pricing for
ad-hoc items not covered by a plan.
ArcadiaCloud.Catalog context: plan + version CRUD, active_version/1
(what a new signup gets), publish_version/1 (retires the prior active
version transactionally then activates the new one),
current_resource_price/2 (effective-dated lookup).
Seed (priv/repo/seeds/catalog_seed.exs, idempotent) creates three AUD
plans — Starter $20, Studio $50, Pro $120/mo — with included
droplet_hours / spaces_gb_month / snapshot_gb_month / bandwidth_gb /
dns_zones (and LLM token allowances on Studio + Pro), plus three storage
/ LLM addons. Prices are placeholders to tune against real DO COGS once
the cost-vs-revenue dashboard lands.
API (authenticated tenants — the catalog is what they pick from):
- GET /api/v1/catalog/plans — plans with active version + items
- GET /api/v1/catalog/addons
Smoke verified: seed creates 3 plans + 3 addons; endpoints return the
shaped catalog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
88 lines
3.2 KiB
Elixir
88 lines
3.2 KiB
Elixir
defmodule ArcadiaCloud.Repo.Migrations.CreateCatalog do
|
|
use Ecto.Migration
|
|
|
|
def change do
|
|
# Plan identity — the stable thing customers refer to ("Studio").
|
|
create table(:plans, primary_key: false) do
|
|
add :id, :binary_id, primary_key: true
|
|
add :code, :string, null: false
|
|
add :name, :string, null: false
|
|
add :description, :text
|
|
add :active, :boolean, null: false, default: true
|
|
|
|
timestamps(type: :utc_datetime)
|
|
end
|
|
|
|
create unique_index(:plans, [:code])
|
|
|
|
# Versioned pricing. A subscription binds to a specific plan_version;
|
|
# raising prices = publishing a new version, existing subs unaffected
|
|
# until explicitly migrated.
|
|
create table(:plan_versions, primary_key: false) do
|
|
add :id, :binary_id, primary_key: true
|
|
add :plan_id, references(:plans, type: :binary_id, on_delete: :delete_all), null: false
|
|
add :version, :integer, null: false
|
|
add :base_price_cents, :integer, null: false, default: 0
|
|
add :currency, :string, null: false, default: "AUD"
|
|
add :status, :string, null: false, default: "draft"
|
|
add :published_at, :utc_datetime
|
|
|
|
timestamps(type: :utc_datetime)
|
|
end
|
|
|
|
create unique_index(:plan_versions, [:plan_id, :version])
|
|
create index(:plan_versions, [:status])
|
|
|
|
# What a plan version INCLUDES, per resource kind, plus overage terms.
|
|
create table(:plan_items, primary_key: false) do
|
|
add :id, :binary_id, primary_key: true
|
|
add :plan_version_id,
|
|
references(:plan_versions, type: :binary_id, on_delete: :delete_all),
|
|
null: false
|
|
add :resource_kind, :string, null: false
|
|
add :included_qty, :decimal, null: false, default: 0
|
|
add :overage_unit, :string
|
|
add :overage_price_cents, :integer
|
|
add :hard_cap_qty, :decimal
|
|
|
|
timestamps(type: :utc_datetime)
|
|
end
|
|
|
|
create unique_index(:plan_items, [:plan_version_id, :resource_kind])
|
|
|
|
# A la carte upgrades. Not versioned for MVP — price changes apply
|
|
# forward; existing subscription addon refs keep the price snapshotted
|
|
# on the subscription (phase 3 subscriptions chunk).
|
|
create table(:addons, primary_key: false) do
|
|
add :id, :binary_id, primary_key: true
|
|
add :code, :string, null: false
|
|
add :name, :string, null: false
|
|
add :resource_kind, :string, null: false
|
|
add :qty, :decimal, null: false, default: 0
|
|
add :price_cents, :integer, null: false, default: 0
|
|
add :currency, :string, null: false, default: "AUD"
|
|
add :active, :boolean, null: false, default: true
|
|
|
|
timestamps(type: :utc_datetime)
|
|
end
|
|
|
|
create unique_index(:addons, [:code])
|
|
|
|
# Fallback per-unit pricing for ad-hoc / pure-usage items not covered
|
|
# by a plan_item. Effective-dated.
|
|
create table(:resource_prices, primary_key: false) do
|
|
add :id, :binary_id, primary_key: true
|
|
add :resource_kind, :string, null: false
|
|
add :unit, :string, null: false
|
|
add :unit_price_cents, :integer, null: false
|
|
add :currency, :string, null: false, default: "AUD"
|
|
add :effective_from, :date, null: false
|
|
add :effective_to, :date
|
|
|
|
timestamps(type: :utc_datetime)
|
|
end
|
|
|
|
create index(:resource_prices, [:resource_kind, :effective_from])
|
|
end
|
|
end
|