Wire a full tenant deployment as one orchestrated, compensating saga:
mark → create droplet → wait active → register in inventory → link to
deployment → point DNS → activate. A failure anywhere rolls the whole
thing back — droplet destroyed, DNS reverted, deployment moved to
cancelled.
- New lifecycle state `provisioning`; deployments created via the
provision path enter here and only reach `active` once the saga's
ActivateDeployment step runs.
- Four new steps: MarkDeploymentProvisioning (owns the deployment's
failure state), LinkDeploymentResource, PointDeploymentDns,
ActivateDeployment.
- Provisioning.provision_deployment/2 assembles + starts the saga.
- DeploymentController: POST /deployments with provision:true creates
in `provisioning` and kicks the saga (202); GET /deployments/:id now
returns the provisioning saga + per-step progress.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
arcadia-app issues the role slug "platform-admin" (hyphen) — confirmed
from a live arcadia-dev JWT (roles: ["admin","platform-admin"]). Every
authorization check here tested for "platform_admin" (underscore), so
real operator tokens got 403 on billing / dashboard / drift and an
empty tenant-scoped result on inventory.
The smoke tests missed it because Guardian.mint_dev_token hardcoded the
underscore form — fixed there too, so the dev helper now matches what
arcadia-app actually emits.
Replaced the string literal "platform_admin" -> "platform-admin" in all
six controllers + guardian.ex. The platform_admin?/1 function names keep
underscores (Elixir identifiers can't contain hyphens) — only the role
string changed.
Verified: with a platform-admin token, /inventory, /billing/balance,
/dashboard/margin and /drift all return 200.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>