ArcadiaCloud.Analytics — the operator margin view. Revenue (tenant
invoices, ex-GST) vs COGS (DO cost lines), margin = revenue - COGS.
- margin_summary/1 — overall P&L + per-tenant + per-deployment margin
for a month.
- by_kind/1 — revenue and COGS broken down by resource kind (separate
axes; billing kinds and DO kinds don't 1:1).
- live_accrual/0 — current-month unbilled: runs the quote engine with
partial metered usage per active subscription. "What tenants are
racking up right now before the rollup."
COGS-to-tenant attribution uses COALESCE(deployment.tenant_id,
resource.tenant_id) — a resource in a deployment bills to the
deployment's tenant; standalone resources fall back to their own
tenant_id (skyai-internal infra).
API (platform_admin only):
- GET /api/v1/dashboard/margin?period=YYYY-MM-DD
- GET /api/v1/dashboard/accrual
Schema fix: cloud_resources.tenant_id and cloud_projects.tenant_id were
binary_id (UUID) while cloud_deployments / tenant_invoices use string.
Migrated both to text — a UUID is a valid string, arcadia also uses
non-UUID tenant slugs ("platform-admin"), and the type alignment lets
the analytics COALESCE join work. Side benefit: kills the phase-1 bug
where a non-UUID tenant_id claim crashed the inventory query.
Smoke verified against real ingested April DO COGS: pilot deployment on
Studio with 5 droplets, metered + rolled up — by_tenant shows
dashboard-pilot rev $71.12 / COGS $30.53 / margin $40.59 (57.1%);
overall P&L revenue $71.12 vs all-April COGS $86.92 = -$15.80 (the
pilot's revenue doesn't cover Sky AI's full April infra — correct, the
rest is internal/unattributed).
Phase 3 complete: catalog, deployments+subscriptions, quote engine,
metering, invoice rollup, cost-vs-revenue dashboard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Models:
- cloud_projects: arcadia-cloud's mirror of DO Projects, indexed by
(provider, provider_id); tenant_id + purpose classify each project.
- cloud_resources: single unified resource table; kind-specific bits in
attrs JSONB; first_seen_at / last_seen_at / stale_strike_count drive
three-strike deletion.
- cloud_resource_events: append-only audit (discovered, updated, deleted,
drift_detected, tagged, restored).
ArcadiaCloud.Cloud context owns the single upsert chokepoint that:
- inserts new with `discovered` event
- updates existing only when meaningful fields change
- restores tombstoned rows seen again
- bumps last_seen_at and resets strike count
mark_stale/3 implements the three-strike rule.
ArcadiaCloud.DigitalOcean.Client is a Req wrapper with auto-pagination.
Per-purpose token resolution via .Tokens (phase 1: env DO_API_TOKEN;
phase 2: vault). Per project_arcadia_cloud memory the long-term shape
is one PAT per queue purpose for rate-limit isolation.
ArcadiaCloud.Sync.Bootstrap ensures the skyai-internal DO Project exists
on first sync, idempotent thereafter. ArcadiaCloud.Sync.DropletsWorker
runs full droplet sync on the cloud_sync_full Oban queue.
InventoryController wired to real data: platform_admin sees all,
tenants see only their scope.
Live smoke test against real DO: 5 droplets synced; skyai-internal
project auto-created; events written; endpoint returns scoped results.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>