Add operator write endpoints for the pricing catalog

CatalogController gains platform-admin-gated writes: create a plan,
create a draft version (with its plan items, transactionally), publish
a version, create an addon — plus a plan-detail endpoint exposing every
version. Pricing stays versioned: create_version always makes a new
draft, publish retires the prior active version, existing subscriptions
are untouched. The Catalog context functions already existed; this just
exposes them over HTTP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 12:21:29 +10:00
parent e1f0aedcf7
commit 34fd811e91
3 changed files with 218 additions and 37 deletions

View File

@@ -25,6 +25,22 @@ defmodule ArcadiaCloud.Catalog do
Repo.get_by(Plan, code: code)
end
@doc "A plan with every version (newest first) + each version's items."
def get_plan(id) do
case Repo.get(Plan, id) do
nil ->
nil
plan ->
versions =
from(v in PlanVersion, where: v.plan_id == ^plan.id, order_by: [desc: v.version])
|> Repo.all()
|> Repo.preload(:items)
%{plan | versions: versions}
end
end
def create_plan(attrs) do
%Plan{}
|> Plan.changeset(attrs)
@@ -69,6 +85,48 @@ defmodule ArcadiaCloud.Catalog do
|> Repo.insert()
end
@doc """
Creates a new draft version of a plan together with its plan items, in
one transaction. The version number is assigned automatically. Pricing
edits are always a new version — never an in-place mutation — so
existing subscriptions keep the terms they signed up on.
`items` is a list of maps (string or atom keys) shaped like PlanItem.
Returns `{:ok, version}` (with items preloaded) or `{:error, changeset}`.
"""
def create_plan_version(plan_id, attrs, items) when is_list(items) do
version_attrs =
attrs
|> Map.new(fn {k, v} -> {to_string(k), v} end)
|> Map.merge(%{
"plan_id" => plan_id,
"version" => next_version_number(plan_id),
"status" => "draft"
})
Repo.transaction(fn ->
version =
case %PlanVersion{} |> PlanVersion.changeset(version_attrs) |> Repo.insert() do
{:ok, v} -> v
{:error, cs} -> Repo.rollback(cs)
end
Enum.each(items, fn item ->
item_attrs =
item
|> Map.new(fn {k, v} -> {to_string(k), v} end)
|> Map.put("plan_version_id", version.id)
case %PlanItem{} |> PlanItem.changeset(item_attrs) |> Repo.insert() do
{:ok, _} -> :ok
{:error, cs} -> Repo.rollback(cs)
end
end)
get_version!(version.id)
end)
end
@doc """
Publishes a draft version: retires any currently-active version of the
same plan, then flips this one to active.