2 Commits

Author SHA1 Message Date
3d54078c60 Phase 3: tenant invoice rollup
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>
2026-05-20 15:32:21 +10:00
33e604b84a Phase 3: quote engine
ArcadiaCloud.Quoting.quote/3 — turns a plan version + addons + usage into
itemized pricing. The same function serves both provisioning-time
projection (with projected usage) and month-end invoicing (with actual
metered usage), so the two can never disagree on the math.

A quote has:
- recurring     — plan base + addon lines (fixed monthly)
- first_invoice — recurring prorated to a mid-month start date
                  (fraction = days_remaining / days_in_month)
- overage       — usage beyond plan+addon allowance, billed in arrears
- all_in_monthly_cents

Overage computation: per plan_item, effective allowance =
included_qty + summed addon qty for the same resource_kind. Overage =
max(0, used - allowance), clamped by hard_cap_qty. overage_price_cents
is per overage_unit; a @unit_factor map converts base-unit usage to
overage units (e.g. 1k_tokens divides token counts by 1000).

Lines are uniform maps (kind/resource_kind/description/qty/unit/
unit_price_cents/amount_cents/meta) — the invoice rollup chunk will
persist them near-verbatim as invoice_lines.

addon_fields/1 accepts either a catalog Addon (projection from the
catalog) or a SubscriptionAddon (actual, using snapshotted price/qty).

API: POST /api/v1/quote { plan_code, addons[], projected_usage{},
starts_on } — tenant-facing pre-provisioning price preview.

Smoke verified against seeded plans:
- Studio bare: $50/mo, no overage.
- Studio + storage_50gb, mid-month start 2026-05-20, projected usage:
  recurring $57.50; first invoice prorated 12/31 -> $22.25; overage
  LLM 500k-token $5.00 + Spaces 50GB $1.00 (the addon correctly lifted
  the Spaces allowance 100->150 GB); droplet_hours exactly at allowance
  produced no line; all-in $63.50/mo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:15:48 +10:00