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>
Month-end engine — turns a period of metered usage into tenant invoices
(revenue side). Distinct from cloud_invoices, which are DO's bills to
Sky AI (COGS).
tenant_invoices — one per (tenant, period). subtotal/tax/total cents,
status draft/issued/paid/void. unique (tenant_id, period_start).
tenant_invoice_lines — kind plan_base/addon/overage/tax, tagged with
deployment_id (NULL for tenant-level lines like GST) + resource_kind,
so the cost-vs-revenue dashboard can group by deployment and by kind.
ArcadiaCloud.Invoicing.roll_up_period/3:
- groups active subscriptions by tenant
- one tenant_invoice per tenant; per subscription, runs the quote engine
with the deployment's ACTUAL metered usage (Metering.usage_for_period)
and persists the recurring + overage lines tagged with the deployment
- appends a tenant-level GST line (AU 10%, per project_skyai_australia)
- idempotent on (tenant_id, period_start); re-run skips unless force:true
Because the same quote engine serves provisioning-time projection and
month-end invoicing, a tenant's quoted price and invoiced price are
computed identically.
InvoiceRollupWorker — Oban cron, 1st of month 03:00 UTC, invoices the
month just ended.
API (platform_admin sees all; tenants scoped to own):
- GET /api/v1/invoices — tenant invoice list
- GET /api/v1/invoices/:id — invoice with lines
Also: SubscriptionAddon now preloads its :addon so quote/invoice lines
read "Addon: storage_50gb" rather than the addon UUID.
Smoke verified: pilot deployment on Studio + storage_50gb, 3 droplets
metered across all 30 days of April (2160 droplet_hours vs 1488
included) — rollup produced an invoice with plan_base $50 + addon $7.50
+ droplet_hours overage $6.72 (672h x 1c) = $64.22 subtotal, GST $6.42,
total $70.64. Re-run without force correctly skipped.
NOT in this chunk: pushing tenant invoices to skyai-finance as AR —
that needs an income-side endpoint on skyai-finance (the phase-1 push
endpoint creates vendor expense invoices, wrong direction). Deferred to
its own chunk.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>