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>
68 lines
1.7 KiB
Elixir
68 lines
1.7 KiB
Elixir
defmodule ArcadiaCloudWeb.CatalogController do
|
|
@moduledoc """
|
|
Read-only pricing catalog. Available to any authenticated tenant — the
|
|
catalog is what they pick a plan from. Pricing changes are operator
|
|
actions; no write endpoints here for MVP (seed/CLI only).
|
|
"""
|
|
|
|
use ArcadiaCloudWeb, :controller
|
|
|
|
alias ArcadiaCloud.Catalog
|
|
|
|
def plans(conn, _params) do
|
|
plans =
|
|
Catalog.list_plans()
|
|
|> Enum.map(fn plan ->
|
|
version = Catalog.active_version(plan)
|
|
shape_plan(plan, version)
|
|
end)
|
|
|
|
json(conn, %{plans: plans})
|
|
end
|
|
|
|
def addons(conn, _params) do
|
|
addons =
|
|
Catalog.list_addons()
|
|
|> Enum.map(fn a ->
|
|
%{
|
|
code: a.code,
|
|
name: a.name,
|
|
resource_kind: a.resource_kind,
|
|
qty: a.qty,
|
|
price_cents: a.price_cents,
|
|
currency: a.currency
|
|
}
|
|
end)
|
|
|
|
json(conn, %{addons: addons})
|
|
end
|
|
|
|
defp shape_plan(plan, nil) do
|
|
%{code: plan.code, name: plan.name, description: plan.description, active_version: nil}
|
|
end
|
|
|
|
defp shape_plan(plan, version) do
|
|
%{
|
|
code: plan.code,
|
|
name: plan.name,
|
|
description: plan.description,
|
|
active_version: %{
|
|
id: version.id,
|
|
version: version.version,
|
|
base_price_cents: version.base_price_cents,
|
|
currency: version.currency,
|
|
items:
|
|
Enum.map(version.items, fn i ->
|
|
%{
|
|
resource_kind: i.resource_kind,
|
|
included_qty: i.included_qty,
|
|
overage_unit: i.overage_unit,
|
|
overage_price_cents: i.overage_price_cents,
|
|
hard_cap_qty: i.hard_cap_qty
|
|
}
|
|
end)
|
|
}
|
|
}
|
|
end
|
|
end
|