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,33 @@
defmodule ArcadiaCloud.Billing.Subscription do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
@statuses ~w(active paused cancelled)
schema "subscriptions" do
field :status, :string, default: "active"
field :current_period_start, :date
field :current_period_end, :date
field :trial_ends_at, :date
belongs_to :deployment, ArcadiaCloud.Deployments.CloudDeployment
belongs_to :plan_version, ArcadiaCloud.Catalog.PlanVersion
has_many :addons, ArcadiaCloud.Billing.SubscriptionAddon
timestamps(type: :utc_datetime)
end
@required ~w(deployment_id plan_version_id current_period_start current_period_end)a
@optional ~w(status trial_ends_at)a
def changeset(sub, attrs) do
sub
|> cast(attrs, @required ++ @optional)
|> validate_required(@required)
|> validate_inclusion(:status, @statuses)
|> unique_constraint(:deployment_id)
end
end