Commit Graph
23 Commits
Author SHA1 Message Date
Neil 612269c105 fix(billing): remove the overage item on opt-out once nothing is left to invoice
Opting out left the metered overage item on the Stripe subscription, so every later
invoice carried a '0 std hour x Orca sandbox overage' $0 line (confirmed on a
throwaway test-mode subscription). The item now comes off at once when nothing was
metered this Stripe period, or at the first sync after the period rolls over when
there is usage still to invoice. Re-opting in within 24h got the removed item's id
replayed by Stripe idempotency (also confirmed live); the add now retries under a
key naming the removed item. Nothing meters while opted out or with a $0 cap.
2026-11-10 20:51:23 -08:00
Neil 077d4c8104 feat(billing): usage history, top workspaces and members, one spend cap field (JA-18)
Plan & billing now reads '<Team> · Team plan', shows the last 30 days of sandbox
hours as a bar chart, and (owners/admins) the top workspaces and members. Members
see the team chart and their own hours. The overage toggle becomes one 'Monthly
spend cap' field ($0 = off, 'Turn on with a $25 cap'); the rate is a one-line
hint; alert levels are read-only values. The manual refresh icon is gone.

Billing Worker: additive GET /v1/billing/usage (read-only over usage_events,
no migration).
2026-11-10 20:02:29 -08:00
Neil 70f0ed4217 test(billing): every billing wrangler config that names a control plane binds it as SANDBOX_CONTROL
#305 moved billing's GitHub install-url call onto a service binding because same-account Workers
can't fetch each other (Cloudflare error 1042). No prod billing config exists yet (launch runbook
step 1.2); this test fails if the new wrangler.prod.jsonc names SANDBOX_CONTROL_URL without the
binding, or binds another environment's control plane.
2026-11-10 15:19:35 -08:00
Neil ff64af20b0 fix(cloud-setup): the composer follows its setup session; billing reaches the control plane for GitHub
- The composer read the setup session with a bare readCloudSetupSession() call in render. The React
  Compiler memoizes that on the session id, so the panel kept its first (empty) read: rows never
  moved past 'In browser', the finished session never auto-started the workspace, and the GitHub
  row stayed 'Needed' after the browser said 'You're all set'. Read it through
  useCloudSetupSession (useSyncExternalStore with a per-poll cached snapshot) instead.
- Billing asked the control plane for the GitHub install URL with a plain fetch to its workers.dev
  origin. Same-account Workers can't do that (Cloudflare error 1042), so every setup GitHub step
  showed 'Orca Cloud setup is unavailable'. Bind the control plane as a service (SANDBOX_CONTROL)
  in the dev config and send the request through it when bound.

Found by the NP-4 end-to-end run (np4-e2e) on m4air against staging.
2026-11-10 14:32:37 -08:00
Neil 062c05d2fe fix(billing): grant the trial to the desktop's active team when setup starts on it
Live m4air run: a signed-in owner whose active team never had a trial was sent
to T3/Checkout. Only teams bound in the browser got the trial. Session creation
now runs the same grant (N11 limits) for the active team, so the chain skips
the plan page.
2026-11-10 08:49:41 -08:00
Neil 5a877a01d7 feat(billing): setup sessions use the one-time trial (N11) through the TrialPort
A team that setup creates or picks gets the trial when it binds, with the email,
GitHub user and ip abuse keys. Teams that can't get one go through T3 and
Checkout.
2026-11-10 06:27:45 -08:00
Neil 51ace4d05f feat(billing): one-time Orca Cloud trial (N11) and its desktop surfaces (T1)
Billing Worker:
- trials + trial_claims (migration 0005): 2 sandbox hours per new team, no card, no Stripe.
- POST /v1/billing/trial grants it (idempotent, any member); grantTrial/trialOffer exported for
  the setup session. offers.trial on GET entitlements only when this caller could still claim it.
- Abuse limits: once per team, Orca user, normalized verified email, GitHub user (token claim)
  and GitHub owner (POST /v1/internal/trial/github from the binding; a reused identity revokes),
  disposable-domain blocklist, 3 grants per IP per day. Keys stored as SHA-256 hashes.
- Entitlement trial {includedSeconds, usedSeconds, remainingSeconds, exhausted}; metered from
  usage_events, concurrency 1, never overage; trial_used_up (suspendRunning) / trial_unavailable.
- Any subscription ends the trial; unused trial hours lapse.
- e2e: trial path (grant, no Stripe customer, concurrency, metering to used up, once per user,
  plan ends it) with cleanup of the run's abuse keys.

Desktop: trial_used_up / trial_unavailable classify as 'plan'; copy in billing, phone and CLI;
"Trial" badge, TrialPlanSummary in Plan & billing and the plans page strip, account menu row
"Trial · {{count}} sandbox hours left" (Choose plan for owners/admins); billingClaimTrial IPC
and claimOrcaCloudTrial() for the composer.
2026-11-10 06:20:22 -08:00
Neil 33950fa436 feat(cloud-setup): desktop side of the one-tab setup session (N1), team-first plans (O1)
- Signed out, Set up and start opens one tab. The authorize page shows the
  setup account step (A2). Once the code is exchanged, the loopback holds the tab,
  creates the billing setup session and 302s the same tab into the setup pages.
- Signed in, the session is created and its start URL is opened.
- IPC: orcaProfiles.cloudSetupStart / cloudSetupGet / cloudSetupLink (Copy setup
  link mints a fresh one-time link).
- Renderer: cloud-setup-session.ts polls the session (2 s for 3 min, then 10 s),
  keeps a snapshot for the composer and tracker, and switches the profile to the
  team setup chose.
- Plans dialog: Checkout joins the open setup session. A personal account
  (409 team_required) goes through setup instead, which makes or picks a team
  first.
- Billing: POST /v1/cloud-setup/sessions/:id/link.
2026-11-10 06:07:35 -08:00
Neil c14d5856f2 feat(billing): cloud setup sessions (N1) and team-first Checkout (N4/O1)
Setup session API on the billing Worker: the desktop creates a session after
sign-in and polls its steps (account, team, plan/trial, github); the setup pages
on Orca Cloud auth redeem a one-time browser ticket, bind the team (granting the
trial through a port np4-trial's module plugs into), open Checkout that returns
to the setup pages, and ask the control plane for the GitHub install URL. The
control plane flips the GitHub step via POST /v1/internal/cloud-setup/github.

Checkout now refuses personal accounts with 409 team_required: a user: plan
could never move to a team later.
2026-11-10 06:07:27 -08:00
Neil 21ac87b972 feat(cloud-start): setup return link, setup telemetry, X1 error model, B2 team-plans lookup (NP-4)
- N9: orca://cloud/setup/return?session=cs_… is routed (open-url + second
  instance), validated, focuses Orca and fans out to renderer listeners
  (onCloudSetupReturn) so the setup session re-reads at once.
- N10: cloud_setup_step_{started,done,failed}, cloud_setup_first_sandbox and
  cloud_setup_plan_chosen, enum-only, derived from the setup rows.
- X1: setup error kinds (timed_out, checkout_canceled, bank_step,
  trial_unavailable, team_create_failed, wrong_account) and their actions.
- B2: billing POST /v1/billing/team-plans returns other teams' plan state with
  the caller's own token; auth confirms membership from that token (no new
  credential). Desktop IPC billingTeamPlans + useSwitchToPlannedTeam. The team
  switch logic moves out of SidebarTeamSubmenu so the composer can reuse it.
2026-11-10 05:50:29 -08:00
Neil dd34314dc0 feat(billing): bill overage by the minute, put sandboxes to sleep at the cap (U22, U24)
- Overage stays $1.00 per sandbox hour (ORCA_CLOUD_OVERAGE_CENTS_PER_HOUR) and is now billed by
  the minute: a per-minute metered TEST price (unit_amount_decimal = rate / 60 to 12 decimals,
  transform_quantity divide_by 60, round up), created the same way by stripe-test-setup.ts.
  capSeconds rounds the cap down to whole minutes and spentCents mirrors the invoice, so the
  cap math matches Stripe. The meter still counts seconds.
- Opting in re-prices an overage item still on an older price (e.g. $0.75 / h) instead of adding
  a second item on the same meter, and removes any extra stale one.
- Alerts at 75%, 90% and 100% of included hours (warnAtPercent).
- New top-level entitlement.suspendRunning: payment grace expired, or at the limit (out of hours
  with overage off, or at the spend cap). The start check and usage answers carry it, so the
  control plane puts running sandboxes to sleep (snapshot kept, never deleted). Payment fields and
  /v1/internal/suspensions stay payment-only, so no data-retention clock starts at a cap.
- Copy (desktop billing panel, plan card, build sheet, phone, CLI): "billed by the minute" after
  the rate; at the limit "Running sandboxes go to sleep and new ones can't start. Nothing is
  deleted, and your files are safe."
2026-11-10 04:33:07 -08:00
Neil 01d2482a3a fix(billing): one billing store, skeleton, plurals, plans focus and billing nits (DS-34, DS-44)
DS-34:
- One billing store (billing-summary-store) replaces the summary, the hook's local state and the
  Run-on picker cache; it keeps the last good result through a failed read and still drops answers
  from before a team or account switch (DS-17).
- Skeleton while Plan & billing loads.
- '{{count}} sandbox hours left' and '{{count}} sandbox hours over the included amount this month'
  as i18n plurals; no bare 'hours'; the unreachable 100% branch is gone.
- Usage warning driven by usageWarningPercent ('Your team has used {{percent}}% of this month's
  sandbox hours.'), in Settings, the plans page and the account menu.
- capTooHigh states the maximum ('The spend cap can be at most {{max}}.'), from offers.maxSpendCapCents.
- Plans dialog focuses its heading on open, so Tab reaches the interval toggle before the cards.
- Payment notice uses InlineError; the build card's overage buttons land on the overage section.
- Account menu names the team once and adds the hours-left line.

DS-44: warning tone at 90%+, '$' as an input prefix, neutral paid check, 24px dialog close hit
area, no <p> inside the CheckoutPanel <dl>.
2026-11-10 03:39:21 -08:00
Neil 519f6f6c9b fix(billing): spell out price formats per DS-18
Line items: '{{seats}} x {{price}} per seat, billed yearly' / 'per seat per month'
(plural keys); totals '{{total}} per year' / 'per month'; Worker plan labels
'$50 per seat per month' and '$480 per seat, billed yearly'. Drops /mo, /yr,
/year and /seat/month strings.
2026-11-10 02:27:14 -08:00
Neil dc0bf820ea feat(billing): overage $1.00 per sandbox hour (interim); plans.ts is the single source (setup script, e2e and spend-cap math read it); new Stripe TEST price 2026-11-10 01:26:39 -08:00
Neil ff12245a82 feat(cloud): one sandbox size (8 vCPU · 16 GB, 1×) — billing counts every size 1×, plans/usage/run-on/env/CLI/phone copy from ORCA_CLOUD_SANDBOX_SPEC, size pickers removed 2026-11-10 00:54:28 -08:00
Neil 45caa0c93a fix(team): review round 1 (design + FE): copy, focus, live regions, roster/overlay races, summary generations, checkout watch + single live session 2026-11-09 06:55:56 -08:00
Neil dde40cbdeb wip(team): W10 round-1 fixes, return page, docs 2026-11-09 06:55:55 -08:00
Neil 576b4684f7 feat(billing): peak-seat billing and abuse limits (#25)
- Seats billed = the period's peak membership: adds above paid seats are charged now
  (always_invoice); removals never credit mid-period and only lower the quantity inside
  seatTrueDownHours of renewal (proration none), so remove/re-add churn can't dodge or
  double-charge. The cron applies deferred decreases from the last pushed count.
- Included hours and caps come from billed seats; per-member concurrency cap
  (member_concurrency_limit); usage attributed to the member who started it.
- Start check returns billed `seats` and the derived caps for the control plane.
- Desktop shows when paid seats drop at renewal; proration copy updated.
- Design doc: seat-fairness decision with the GitHub/Cursor/Slack/Linear comparison.
2026-11-09 02:34:43 -08:00
Neil 2711c61e19 feat(billing): in-app plans page (#22)
A pricing/plans page reachable from the account menu ("Plan & billing"), the
Settings plan section ("Choose a plan" / "Compare plans"), and any upsell via
openPlansPage(). Everything comes from the billing service's offers: Team
monthly/annual (with the computed annual saving), pooled hours, Large weight,
concurrency, idle sleep, opt-in overage with spend cap, and Enterprise contact.

- States: choose (admin), locked (member), current plan with usage, payment
  failure (status-first), checkout in progress, loading and failure with retry.
- Seats are read-only: one per team member (billing re-reads the count).
- Shared card shell keeps both cards' footers level; sliding interval toggle and
  price fade respect reduced motion; dialog opens without a pre-focused button.
- Worker offers gain per-seat cents and the Large weight for totals.
- Copy matches the marketing /cloud promises: agents keep running, wake in
  seconds, secrets never stored in the VM.
2026-11-09 00:45:06 -08:00
Neil a6b97fc8ea feat(billing): team seats follow membership, payment grace + suspend signal, Team UI
Worker
- Seats = org members from Orca Cloud auth (single source of truth; pending invites
  never count; min 1): auth pushes POST /v1/internal/seats, billing pulls
  /v1/internal/billing/org-members at Checkout and in a reconcile cron, fails closed
  (503) if auth is configured but unreachable. No token claim carries a count.
- Proration: adds always_invoice, removals create_prorations (Stripe default).
- Billing admin actions require a token minted in the last 6 minutes.
- Payment failure: past_due_since (migration 0003); new starts stop at once;
  after 96 h (paymentGraceHours) reason payment_grace_expired + suspendRunning on
  check/usage, and GET /v1/internal/suspensions for the control plane.
- Portal config: monthly<->annual + cancel only (seats follow membership).
- e2e: seat add/remove proration, grace expiry -> suspend -> pay -> resume.

Desktop
- Team section + account menu item: members/roles, invite, change role, remove
  with confirm, revoke invites (owners/admins, auth role rules mirrored).
- Payment issue banner with suspension date and Update payment method.
2026-11-08 23:26:48 -08:00
Neil c4550cc781 feat(billing): Team pricing, opt-in overage, Stripe test-mode e2e, dev deploy (#18)
* feat(billing): Team-only pricing, opt-in overage with spend cap, live Stripe test-mode e2e

- Catalog: Team $50/seat/mo or $480/seat/yr, 60 Standard h/seat/month pooled
  (monthly windows on annual plans), Large 2x, 3 running/seat, 15-min idle sleep;
  Enterprise is contact-only. All numbers/labels in BILLING_PLANS defaults.
- Overage is opt-in (adds the metered item), $0.75/h, monthly spend cap (default
  $25, admin-raisable) enforced in the meter outbox; 80%/100% usage warnings.
- Checkout: plan/interval/seats with adjustable quantity; portal configuration var.
- Workers reject fetch redirect 'error': use 'manual'. Log route errors.
- scripts/stripe-test-setup.ts creates the test catalog + portal configuration;
  scripts/e2e-test-mode.ts drives Checkout (headless, test card), overage, meter,
  cap, annual, test-clock renewal, payment failure and cancel.
- Desktop: Team/Enterprise cards with interval + seats, usage warnings, overage
  toggle + cap; split into copy/offers/usage modules.

* feat(billing): dev deployment config, dev operator keys, return page

- wrangler.dev.jsonc for orca-billing-dev (workers.dev, dev D1, test-mode prices).
- BILLING_DEV_JWKS (issuer orca-billing-dev-test) accepted only when BILLING_ENV
  is not prod, for e2e runs until auth mints billing tokens.
- GET /return: static 'return to Orca' page for Checkout/Portal redirects.
- Design doc: final pricing, dev URL and resources for the control plane.
2026-11-08 23:06:48 -08:00
Neil e1ee97797a feat(billing): plans and prices from config, opt-in Stripe Tax, real account menu
- Worker: BILLING_PLANS JSON overrides the plan catalog (labels, display price,
  hours, limits); entitlements return the plan list so the desktop shows no
  hardcoded prices. STRIPE_AUTOMATIC_TAX=true adds automatic_tax and tax id
  collection to Checkout (default off). Setup script takes prices from env.
- Desktop: register Plan & billing through the sidebar account menu
  (merged nwparker/sidebar-account-menu) and drop the local adapter.
- Register the billing Worker's tests with the test-runner check.
2026-11-08 22:09:50 -08:00
Neil faaa842a98 feat(billing): Cloudflare Worker for Stripe checkout, portal, webhooks, entitlements and usage (#7)
Test-mode-only foundation for paid Orca plans: hosted Checkout/Portal,
signed + idempotent Stripe webhooks that re-read subscriptions, a D1 ledger
for sandbox usage with overage-only Billing meter events, and entitlement
checks for the desktop and the sandbox control plane.
2026-11-08 22:02:00 -08:00