Phase 3: quote engine
ArcadiaCloud.Quoting.quote/3 — turns a plan version + addons + usage into
itemized pricing. The same function serves both provisioning-time
projection (with projected usage) and month-end invoicing (with actual
metered usage), so the two can never disagree on the math.
A quote has:
- recurring — plan base + addon lines (fixed monthly)
- first_invoice — recurring prorated to a mid-month start date
(fraction = days_remaining / days_in_month)
- overage — usage beyond plan+addon allowance, billed in arrears
- all_in_monthly_cents
Overage computation: per plan_item, effective allowance =
included_qty + summed addon qty for the same resource_kind. Overage =
max(0, used - allowance), clamped by hard_cap_qty. overage_price_cents
is per overage_unit; a @unit_factor map converts base-unit usage to
overage units (e.g. 1k_tokens divides token counts by 1000).
Lines are uniform maps (kind/resource_kind/description/qty/unit/
unit_price_cents/amount_cents/meta) — the invoice rollup chunk will
persist them near-verbatim as invoice_lines.
addon_fields/1 accepts either a catalog Addon (projection from the
catalog) or a SubscriptionAddon (actual, using snapshotted price/qty).
API: POST /api/v1/quote { plan_code, addons[], projected_usage{},
starts_on } — tenant-facing pre-provisioning price preview.
Smoke verified against seeded plans:
- Studio bare: $50/mo, no overage.
- Studio + storage_50gb, mid-month start 2026-05-20, projected usage:
recurring $57.50; first invoice prorated 12/31 -> $22.25; overage
LLM 500k-token $5.00 + Spaces 50GB $1.00 (the addon correctly lifted
the Spaces allowance 100->150 GB); droplet_hours exactly at allowance
produced no line; all-in $63.50/mo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
52
lib/arcadia_cloud_web/controllers/quote_controller.ex
Normal file
52
lib/arcadia_cloud_web/controllers/quote_controller.ex
Normal file
@@ -0,0 +1,52 @@
|
||||
defmodule ArcadiaCloudWeb.QuoteController do
|
||||
@moduledoc """
|
||||
Pricing quotes. A tenant hits this before provisioning to see what a
|
||||
plan + addons would cost, optionally with projected usage to preview
|
||||
likely overage.
|
||||
|
||||
Body:
|
||||
plan_code — required
|
||||
addons — optional list of addon codes
|
||||
projected_usage — optional map resource_kind => qty
|
||||
starts_on — optional ISO date; if set, returns prorated
|
||||
first-invoice figures
|
||||
"""
|
||||
|
||||
use ArcadiaCloudWeb, :controller
|
||||
|
||||
alias ArcadiaCloud.{Catalog, Quoting}
|
||||
|
||||
def create(conn, params) do
|
||||
plan = params["plan_code"] && Catalog.get_plan_by_code(params["plan_code"])
|
||||
version = plan && Catalog.active_version(plan)
|
||||
|
||||
cond do
|
||||
is_nil(version) ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "plan_or_version_not_found"})
|
||||
|
||||
true ->
|
||||
addons = Catalog.get_addons(params["addons"] || [])
|
||||
|
||||
opts =
|
||||
[]
|
||||
|> put_opt(:usage, params["projected_usage"])
|
||||
|> put_opt(:starts_on, parse_date(params["starts_on"]))
|
||||
|
||||
quote = Quoting.quote(version, addons, opts)
|
||||
json(conn, %{quote: quote, plan_code: plan.code})
|
||||
end
|
||||
end
|
||||
|
||||
defp put_opt(opts, _key, nil), do: opts
|
||||
defp put_opt(opts, key, value), do: Keyword.put(opts, key, value)
|
||||
|
||||
defp parse_date(nil), do: nil
|
||||
defp parse_date(""), do: nil
|
||||
|
||||
defp parse_date(s) do
|
||||
case Date.from_iso8601(s) do
|
||||
{:ok, d} -> d
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user