Files
arcadia-cloud/lib/arcadia_cloud_web/router.ex
Giuliano Silvestro 33e604b84a 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>
2026-05-20 15:15:48 +10:00

41 lines
1.1 KiB
Elixir

defmodule ArcadiaCloudWeb.Router do
use ArcadiaCloudWeb, :router
pipeline :api do
plug :accepts, ["json"]
end
pipeline :authed do
plug ArcadiaCloudWeb.Plugs.RequireAuth
end
scope "/api", ArcadiaCloudWeb do
pipe_through :api
get "/health", HealthController, :show
end
scope "/api/v1", ArcadiaCloudWeb do
pipe_through [:api, :authed]
get "/inventory", InventoryController, :index
get "/billing/balance", BillingController, :balance
get "/billing/cost-lines", BillingController, :cost_lines
get "/drift", DriftController, :index
post "/drift/:id/accept", DriftController, :accept
get "/catalog/plans", CatalogController, :plans
get "/catalog/addons", CatalogController, :addons
post "/quote", QuoteController, :create
get "/deployments", DeploymentController, :index
post "/deployments", DeploymentController, :create
get "/deployments/:id", DeploymentController, :show
post "/deployments/:id/transition", DeploymentController, :transition
post "/deployments/:id/subscribe", DeploymentController, :subscribe
end
end