Phase 3: pricing catalog
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>
This commit is contained in:
133
lib/arcadia_cloud/catalog.ex
Normal file
133
lib/arcadia_cloud/catalog.ex
Normal file
@@ -0,0 +1,133 @@
|
||||
defmodule ArcadiaCloud.Catalog do
|
||||
@moduledoc """
|
||||
Pricing catalog — plans, versioned pricing, plan items (included
|
||||
resources + overage), addons, fallback resource prices.
|
||||
|
||||
Versioning: a subscription binds to a specific plan_version. Raising
|
||||
prices = publish a new version; existing subs are unaffected until
|
||||
explicitly migrated. `active_version/1` returns the version new
|
||||
signups get.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
|
||||
alias ArcadiaCloud.Repo
|
||||
alias ArcadiaCloud.Catalog.{Plan, PlanVersion, PlanItem, Addon, ResourcePrice}
|
||||
|
||||
# ---- plans ----------------------------------------------------------------
|
||||
|
||||
def list_plans do
|
||||
from(p in Plan, where: p.active == true, order_by: p.code)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def get_plan_by_code(code) do
|
||||
Repo.get_by(Plan, code: code)
|
||||
end
|
||||
|
||||
def create_plan(attrs) do
|
||||
%Plan{}
|
||||
|> Plan.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
# ---- versions -------------------------------------------------------------
|
||||
|
||||
@doc "The version a new subscription to this plan would get."
|
||||
def active_version(%Plan{id: plan_id}), do: active_version(plan_id)
|
||||
|
||||
def active_version(plan_id) when is_binary(plan_id) do
|
||||
from(v in PlanVersion,
|
||||
where: v.plan_id == ^plan_id and v.status == "active",
|
||||
order_by: [desc: v.version],
|
||||
limit: 1,
|
||||
preload: [:items]
|
||||
)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
def get_version!(id), do: Repo.get!(PlanVersion, id) |> Repo.preload([:items, :plan])
|
||||
|
||||
def get_version(id) do
|
||||
case Repo.get(PlanVersion, id) do
|
||||
nil -> nil
|
||||
v -> Repo.preload(v, [:items, :plan])
|
||||
end
|
||||
end
|
||||
|
||||
def next_version_number(plan_id) do
|
||||
max =
|
||||
from(v in PlanVersion, where: v.plan_id == ^plan_id, select: max(v.version))
|
||||
|> Repo.one()
|
||||
|
||||
(max || 0) + 1
|
||||
end
|
||||
|
||||
def create_version(attrs) do
|
||||
%PlanVersion{}
|
||||
|> PlanVersion.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Publishes a draft version: retires any currently-active version of the
|
||||
same plan, then flips this one to active.
|
||||
"""
|
||||
def publish_version(%PlanVersion{} = version) do
|
||||
now = DateTime.utc_now() |> DateTime.truncate(:second)
|
||||
|
||||
Repo.transaction(fn ->
|
||||
from(v in PlanVersion,
|
||||
where: v.plan_id == ^version.plan_id and v.status == "active"
|
||||
)
|
||||
|> Repo.update_all(set: [status: "retired", updated_at: now])
|
||||
|
||||
version
|
||||
|> PlanVersion.changeset(%{status: "active", published_at: now})
|
||||
|> Repo.update!()
|
||||
end)
|
||||
end
|
||||
|
||||
# ---- plan items -----------------------------------------------------------
|
||||
|
||||
def add_plan_item(attrs) do
|
||||
%PlanItem{}
|
||||
|> PlanItem.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
# ---- addons ---------------------------------------------------------------
|
||||
|
||||
def list_addons do
|
||||
from(a in Addon, where: a.active == true, order_by: a.code) |> Repo.all()
|
||||
end
|
||||
|
||||
def get_addons(codes) when is_list(codes) do
|
||||
from(a in Addon, where: a.code in ^codes) |> Repo.all()
|
||||
end
|
||||
|
||||
def create_addon(attrs) do
|
||||
%Addon{}
|
||||
|> Addon.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
# ---- resource prices ------------------------------------------------------
|
||||
|
||||
def current_resource_price(resource_kind, on_date \\ Date.utc_today()) do
|
||||
from(p in ResourcePrice,
|
||||
where:
|
||||
p.resource_kind == ^resource_kind and p.effective_from <= ^on_date and
|
||||
(is_nil(p.effective_to) or p.effective_to >= ^on_date),
|
||||
order_by: [desc: p.effective_from],
|
||||
limit: 1
|
||||
)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
def create_resource_price(attrs) do
|
||||
%ResourcePrice{}
|
||||
|> ResourcePrice.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
end
|
||||
27
lib/arcadia_cloud/catalog/addon.ex
Normal file
27
lib/arcadia_cloud/catalog/addon.ex
Normal file
@@ -0,0 +1,27 @@
|
||||
defmodule ArcadiaCloud.Catalog.Addon do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
|
||||
schema "addons" do
|
||||
field :code, :string
|
||||
field :name, :string
|
||||
field :resource_kind, :string
|
||||
field :qty, :decimal, default: Decimal.new(0)
|
||||
field :price_cents, :integer, default: 0
|
||||
field :currency, :string, default: "AUD"
|
||||
field :active, :boolean, default: true
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
def changeset(addon, attrs) do
|
||||
addon
|
||||
|> cast(attrs, [:code, :name, :resource_kind, :qty, :price_cents, :currency, :active])
|
||||
|> validate_required([:code, :name, :resource_kind, :qty, :price_cents])
|
||||
|> validate_length(:currency, is: 3)
|
||||
|> unique_constraint(:code)
|
||||
end
|
||||
end
|
||||
25
lib/arcadia_cloud/catalog/plan.ex
Normal file
25
lib/arcadia_cloud/catalog/plan.ex
Normal file
@@ -0,0 +1,25 @@
|
||||
defmodule ArcadiaCloud.Catalog.Plan do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
|
||||
schema "plans" do
|
||||
field :code, :string
|
||||
field :name, :string
|
||||
field :description, :string
|
||||
field :active, :boolean, default: true
|
||||
|
||||
has_many :versions, ArcadiaCloud.Catalog.PlanVersion
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
def changeset(plan, attrs) do
|
||||
plan
|
||||
|> cast(attrs, [:code, :name, :description, :active])
|
||||
|> validate_required([:code, :name])
|
||||
|> unique_constraint(:code)
|
||||
end
|
||||
end
|
||||
33
lib/arcadia_cloud/catalog/plan_item.ex
Normal file
33
lib/arcadia_cloud/catalog/plan_item.ex
Normal file
@@ -0,0 +1,33 @@
|
||||
defmodule ArcadiaCloud.Catalog.PlanItem do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
|
||||
schema "plan_items" do
|
||||
field :resource_kind, :string
|
||||
field :included_qty, :decimal, default: Decimal.new(0)
|
||||
field :overage_unit, :string
|
||||
field :overage_price_cents, :integer
|
||||
field :hard_cap_qty, :decimal
|
||||
|
||||
belongs_to :plan_version, ArcadiaCloud.Catalog.PlanVersion
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
def changeset(item, attrs) do
|
||||
item
|
||||
|> cast(attrs, [
|
||||
:plan_version_id,
|
||||
:resource_kind,
|
||||
:included_qty,
|
||||
:overage_unit,
|
||||
:overage_price_cents,
|
||||
:hard_cap_qty
|
||||
])
|
||||
|> validate_required([:plan_version_id, :resource_kind, :included_qty])
|
||||
|> unique_constraint([:plan_version_id, :resource_kind])
|
||||
end
|
||||
end
|
||||
31
lib/arcadia_cloud/catalog/plan_version.ex
Normal file
31
lib/arcadia_cloud/catalog/plan_version.ex
Normal file
@@ -0,0 +1,31 @@
|
||||
defmodule ArcadiaCloud.Catalog.PlanVersion do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
|
||||
@statuses ~w(draft active retired)
|
||||
|
||||
schema "plan_versions" do
|
||||
field :version, :integer
|
||||
field :base_price_cents, :integer, default: 0
|
||||
field :currency, :string, default: "AUD"
|
||||
field :status, :string, default: "draft"
|
||||
field :published_at, :utc_datetime
|
||||
|
||||
belongs_to :plan, ArcadiaCloud.Catalog.Plan
|
||||
has_many :items, ArcadiaCloud.Catalog.PlanItem
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
def changeset(version, attrs) do
|
||||
version
|
||||
|> cast(attrs, [:plan_id, :version, :base_price_cents, :currency, :status, :published_at])
|
||||
|> validate_required([:plan_id, :version])
|
||||
|> validate_inclusion(:status, @statuses)
|
||||
|> validate_length(:currency, is: 3)
|
||||
|> unique_constraint([:plan_id, :version])
|
||||
end
|
||||
end
|
||||
32
lib/arcadia_cloud/catalog/resource_price.ex
Normal file
32
lib/arcadia_cloud/catalog/resource_price.ex
Normal file
@@ -0,0 +1,32 @@
|
||||
defmodule ArcadiaCloud.Catalog.ResourcePrice do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
|
||||
schema "resource_prices" do
|
||||
field :resource_kind, :string
|
||||
field :unit, :string
|
||||
field :unit_price_cents, :integer
|
||||
field :currency, :string, default: "AUD"
|
||||
field :effective_from, :date
|
||||
field :effective_to, :date
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
def changeset(price, attrs) do
|
||||
price
|
||||
|> cast(attrs, [
|
||||
:resource_kind,
|
||||
:unit,
|
||||
:unit_price_cents,
|
||||
:currency,
|
||||
:effective_from,
|
||||
:effective_to
|
||||
])
|
||||
|> validate_required([:resource_kind, :unit, :unit_price_cents, :effective_from])
|
||||
|> validate_length(:currency, is: 3)
|
||||
end
|
||||
end
|
||||
67
lib/arcadia_cloud_web/controllers/catalog_controller.ex
Normal file
67
lib/arcadia_cloud_web/controllers/catalog_controller.ex
Normal file
@@ -0,0 +1,67 @@
|
||||
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
|
||||
@@ -25,5 +25,8 @@ defmodule ArcadiaCloudWeb.Router do
|
||||
|
||||
get "/drift", DriftController, :index
|
||||
post "/drift/:id/accept", DriftController, :accept
|
||||
|
||||
get "/catalog/plans", CatalogController, :plans
|
||||
get "/catalog/addons", CatalogController, :addons
|
||||
end
|
||||
end
|
||||
|
||||
87
priv/repo/migrations/20260520140000_create_catalog.exs
Normal file
87
priv/repo/migrations/20260520140000_create_catalog.exs
Normal file
@@ -0,0 +1,87 @@
|
||||
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
|
||||
101
priv/repo/seeds/catalog_seed.exs
Normal file
101
priv/repo/seeds/catalog_seed.exs
Normal file
@@ -0,0 +1,101 @@
|
||||
# Seed example hosting plans. Idempotent — skips plans that already exist.
|
||||
# Run with: mix run priv/repo/seeds/catalog_seed.exs
|
||||
#
|
||||
# Prices are AUD placeholders for Sky AI's hosted-tenant offering; tune
|
||||
# them against real DO COGS once the cost-vs-revenue dashboard is live.
|
||||
|
||||
alias ArcadiaCloud.Catalog
|
||||
|
||||
# {plan_code, name, description, base_price_cents, [plan_items]}
|
||||
# plan_item: {resource_kind, included_qty, overage_unit, overage_price_cents, hard_cap_qty}
|
||||
plans = [
|
||||
{"starter", "Starter", "Single small instance — good for a low-traffic app.", 2000,
|
||||
[
|
||||
{"droplet_hours", 744, "hour", 1, nil},
|
||||
{"spaces_gb_month", 25, "gb_month", 2, nil},
|
||||
{"snapshot_gb_month", 10, "gb_month", 3, nil},
|
||||
{"bandwidth_gb", 1000, "gb", 1, nil},
|
||||
{"dns_zones", 1, "zone", 100, nil}
|
||||
]},
|
||||
{"studio", "Studio", "Two instances + room for assets and a managed LLM budget.", 5000,
|
||||
[
|
||||
{"droplet_hours", 1488, "hour", 1, nil},
|
||||
{"spaces_gb_month", 100, "gb_month", 2, nil},
|
||||
{"snapshot_gb_month", 50, "gb_month", 3, nil},
|
||||
{"bandwidth_gb", 3000, "gb", 1, nil},
|
||||
{"dns_zones", 3, "zone", 100, nil},
|
||||
{"llm_tokens_input", 1_000_000, "1k_tokens", 1, nil},
|
||||
{"llm_tokens_output", 200_000, "1k_tokens", 3, nil}
|
||||
]},
|
||||
{"pro", "Pro", "Production workloads — generous compute, storage and LLM allowance.", 12000,
|
||||
[
|
||||
{"droplet_hours", 4464, "hour", 1, nil},
|
||||
{"spaces_gb_month", 500, "gb_month", 2, nil},
|
||||
{"snapshot_gb_month", 200, "gb_month", 3, nil},
|
||||
{"bandwidth_gb", 10000, "gb", 1, nil},
|
||||
{"dns_zones", 10, "zone", 100, nil},
|
||||
{"llm_tokens_input", 10_000_000, "1k_tokens", 1, nil},
|
||||
{"llm_tokens_output", 2_000_000, "1k_tokens", 3, nil}
|
||||
]}
|
||||
]
|
||||
|
||||
for {code, name, desc, base_cents, items} <- plans do
|
||||
case Catalog.get_plan_by_code(code) do
|
||||
nil ->
|
||||
{:ok, plan} = Catalog.create_plan(%{code: code, name: name, description: desc})
|
||||
|
||||
{:ok, version} =
|
||||
Catalog.create_version(%{
|
||||
plan_id: plan.id,
|
||||
version: 1,
|
||||
base_price_cents: base_cents,
|
||||
currency: "AUD",
|
||||
status: "draft"
|
||||
})
|
||||
|
||||
for {kind, incl, unit, overage, cap} <- items do
|
||||
{:ok, _} =
|
||||
Catalog.add_plan_item(%{
|
||||
plan_version_id: version.id,
|
||||
resource_kind: kind,
|
||||
included_qty: incl,
|
||||
overage_unit: unit,
|
||||
overage_price_cents: overage,
|
||||
hard_cap_qty: cap
|
||||
})
|
||||
end
|
||||
|
||||
{:ok, _} = Catalog.publish_version(version)
|
||||
IO.puts("seeded plan #{code} (#{length(items)} items) @ $#{base_cents / 100}/mo AUD")
|
||||
|
||||
_existing ->
|
||||
IO.puts("plan #{code} already exists — skipped")
|
||||
end
|
||||
end
|
||||
|
||||
# Example addons
|
||||
addons = [
|
||||
{"storage_50gb", "+50 GB Spaces storage", "spaces_gb_month", 50, 750},
|
||||
{"storage_250gb", "+250 GB Spaces storage", "spaces_gb_month", 250, 3000},
|
||||
{"llm_5m_input", "+5M LLM input tokens", "llm_tokens_input", 5_000_000, 4000}
|
||||
]
|
||||
|
||||
for {code, name, kind, qty, price} <- addons do
|
||||
case ArcadiaCloud.Repo.get_by(ArcadiaCloud.Catalog.Addon, code: code) do
|
||||
nil ->
|
||||
{:ok, _} =
|
||||
Catalog.create_addon(%{
|
||||
code: code,
|
||||
name: name,
|
||||
resource_kind: kind,
|
||||
qty: qty,
|
||||
price_cents: price,
|
||||
currency: "AUD"
|
||||
})
|
||||
|
||||
IO.puts("seeded addon #{code} @ $#{price / 100}/mo AUD")
|
||||
|
||||
_ ->
|
||||
IO.puts("addon #{code} already exists — skipped")
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user