Phase 3: deployment model + subscriptions

cloud_deployments — the billable unit (one app instance). A tenant has
1..N deployments; cloud_resources.deployment_id ties resources to one.
Fields: tenant_id, slug (unique per tenant), display_name, region,
state, llm_mode, billing_action_suspended (operator override),
template_code/version (nullable — formal templates land in phase 4).

Lifecycle state machine in ArcadiaCloud.Deployments — states trial /
active / past_due / paused / suspended / cancelled / archived. Every
transition is validated against an explicit @transitions map and
recorded in cloud_deployment_events. create_deployment defaults to
`active` (trial is wired but no flow enters it yet).

subscriptions — one per deployment, binds it to a plan_version. status
active/paused/cancelled, current period dates, trial_ends_at.

subscription_addons — addons attached to a subscription with price + qty
SNAPSHOTTED at attach time, so a later catalog price change can't
retroactively reprice an existing subscriber.

ArcadiaCloud.Subscriptions context: create_subscription (period defaults
to current calendar month), attach_addon (snapshots from the live Addon),
change_plan_version (migrate to a new version — price changes / up-down
grades), get_subscription_for_deployment.

API (platform_admin sees all tenants; others scoped to own tenant_id):
- GET/POST /api/v1/deployments
- GET      /api/v1/deployments/:id            (with subscription + events)
- POST     /api/v1/deployments/:id/transition
- POST     /api/v1/deployments/:id/subscribe  (plan_code + optional addons)

Smoke verified: created a deployment, transitioned active->paused
(events logged with actor), rejected an invalid paused->archived
transition (422), subscribed to Studio with the storage_50gb addon —
addon price snapshotted at 750c/qty 50; show returns deployment +
subscription + event history.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 15:12:15 +10:00
parent c10f87b6e0
commit aee5e07b26
9 changed files with 659 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
defmodule ArcadiaCloud.Deployments.CloudDeployment do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
@states ~w(trial active past_due paused suspended cancelled archived)
@llm_modes ~w(managed byo none)
def states, do: @states
schema "cloud_deployments" do
field :tenant_id, :string
field :slug, :string
field :display_name, :string
field :template_code, :string
field :template_version, :string
field :region, :string
field :state, :string, default: "active"
field :state_since, :utc_datetime
field :llm_mode, :string, default: "none"
field :billing_action_suspended, :boolean, default: false
belongs_to :cloud_project, ArcadiaCloud.Cloud.CloudProject
timestamps(type: :utc_datetime)
end
@required ~w(tenant_id slug state state_since)a
@optional ~w(display_name template_code template_version region llm_mode
billing_action_suspended cloud_project_id)a
def changeset(deployment, attrs) do
deployment
|> cast(attrs, @required ++ @optional)
|> validate_required(@required)
|> validate_inclusion(:state, @states)
|> validate_inclusion(:llm_mode, @llm_modes)
|> validate_format(:slug, ~r/^[a-z0-9][a-z0-9-]*$/,
message: "must be lowercase alphanumeric + hyphens"
)
|> unique_constraint([:tenant_id, :slug])
end
end