Per-agent pricing and yearly price

Task 6736 · Done · Coder · created · updated

Epic 6714 (per-agent pricing model). Child C2 of the architecture. Covers PRD asks A1 and A2. `depends_on` the plan-engine child only to serialize schema migrations in this wave (no data dependency). ## Original requirement (epic body, verbatim) "Plan knobs: per-agent pricing with a minimum and maximum agent count (agents bought set the org's concurrent-agent limit); a yearly price; ..." Out of scope: "prices (no figures are set yet)". ## Acceptance criteria (PRD, verbatim) **Environment:** "dev" is `dev.jaah.ai` (browser face `https://app.dev.jaah.ai`) using Stripe test mode. Every oracle is a browser user flow. Unit tests are additionally required but never sufficient. Each capability is ON by default on dev, behind no flag. - **A1.1** A plan has three operator-editable knobs, each visible and editable in the Plans dialog: price per agent per month, minimum agents and maximum agents (blank = no maximum). - **A1.2** Purchase is per agent. At checkout the org admin picks an agent count, and the Stripe subscription's plan item has quantity = that count at the per-agent price. - **A1.3** The agents bought become the org's `limit_runner_slots`. The 6th concurrent task stays `ready` with a wait reason naming the agent limit. - **A1.4** A count outside [min, max] is refused at checkout and at change time. The message names the allowed range, and the next plan where one exists. All states: below min → refused; within → accepted; above max → refused; max blank → no upper bound. - **A1.5** An org admin can change the agent count on a live subscription within range, without changing plan. Stripe prorates the change, and the limit updates when Stripe confirms. - **A1.6** The billing page shows the agents bought, the range and the per-agent price. A plan with no per-agent price keeps today's single-price behaviour, unchanged. - **A2.1** A plan has an optional yearly per-agent price. When it is set, the org admin chooses Monthly or Yearly at checkout, and the Stripe subscription bills at the yearly interval. - **A2.2** Switching monthly ↔ yearly is available from the billing page and is previewed before confirm. A plan with no yearly price offers no Yearly option. - **Quality** Each new setting appears on the page where peers live. Labels follow `docs/standards/ui-copy-standards.md`. Nothing truncated or overflowing at supported viewport widths. Every refusal names what to do next. A cramped, unlabeled or raw-key UI fails. ## Approach (architecture C2, settled) - A `per_agent` flag on the existing price (not a second price column). Schema (one expand-only migration): `billing_plans.per_agent boolean NOT NULL DEFAULT false`, `min_agents integer NULL CHECK (min_agents>=1)`, `max_agents integer NULL CHECK (max_agents IS NULL OR max_agents >= coalesce(min_agents,1))`, `yearly_stripe_price_id text NULL`, `yearly_price_unit_amount bigint NULL`; `orgs.agents_bought integer NULL`, `orgs.billing_interval text NULL CHECK (billing_interval IN ('month','year'))`. This is pinned seam S2 for the "Five plans" child. - `push_price(..., interval="month"|"year")`; `org_plans.INTERVAL` becomes the default; `org_plans.Plan` gains `yearly`, `per_agent`, `min_agents`, `max_agents`. - `subscriptions.subscribe(..., agents, interval)`: validate range → 422 "Team is for 3–9 agents. For 10 or more, choose Business." (next plan = next active plan by `sort_order` whose range contains the count; omitted when none); plan item `(price_id_for(interval), agents)`. Same check in `change_plan` and `preview`. - Mixed intervals: create every NEW subscription with `billing_mode=flexible`; migrate a classic subscription's billing mode inside `change_interval`; pin the gateway Stripe API version ≥ 2025-06-30.basil in `billing/gateway.py`; extend `tests/billing/fake_stripe.py` to model the rule. - `change_agents` (preview via `invoices_create_preview` with stored `proration_date`, confirm with `proration_behavior=create_prorations`) and `change_interval` (swap price AND resend quantity). Routes beside `org_billing.py`: `POST /agents/preview`, `POST /agents`, `POST /interval/preview`, `POST /interval`. - `org_plan_admin.on_entitlement` writes `orgs.agents_bought` (plan item quantity from the subscription mirror) and `billing_interval`; when a per-agent purchase lands it also clears a pinned `limit_runner_slots` override (recorded in history). `plan_terms.caps_of` runner_slots: override → `agents_bought` when plan is `per_agent` (source "purchase") → plan cap. `plan_terms.assign` clears `agents_bought`/`billing_interval`. Limit changes only on the webhook. Wait reason: "Waiting: agent limit (5/5). Add agents in Billing." - Console: Plans dialog "Priced per agent", "Min agents", "Max agents (blank = no limit)", "Yearly price per agent"; `UpgradeDialog.tsx` agent stepper clamped to range + Monthly/Yearly control (hidden without a yearly price); `SubscriptionCard` "5 agents · $X / agent / month · range 3–9", "Change agents", "Switch to yearly". - The dev verification plan for the oracles is created and deleted through the console (N1). Credentials & reach: - All console oracles on dev: `jaah/debug/main/operator-login` on `app.dev.jaah.ai` — verified — `task_credential_get` succeeded; `app.dev.jaah.ai` answered 200. - A1, A2 Stripe test-mode subscription inspection: `jaah/debug/stripe-dev` — verified — get succeeded; `api.stripe.com` reachable. Out of scope: Do NOT write any price figure into any plan, Stripe Price, migration, seed or doc; dev verification prices live only on a dev-only verification plan in Stripe test mode and are removed after verification (N1). Do NOT touch the marketing site, `apps/site-widget` or `docs/compliance/public/` (N2). Do NOT change any existing org's plan, price or terms; a plan with `per_agent=false` must behave byte-for-byte as today (A1.6, P2). Do NOT make caps other than agents per-agent (D-A1). Do NOT create the five plan rows (the "Five plans" child). No switch/inheritance/rollover work (plan-engine child). Done means: - A1.1 → verify: on dev, open Operator Settings › Billing › Plans, edit a plan, and observe the three fields (and the yearly price field). Save min 3, max 9 and reopen; the value persists. (The `team` = 3–9 values themselves are set by the "Five plans" child.) - A1.2 → verify: on dev, set a temporary test price on a dev-only verification plan (min 3, max 9). As an org admin, buy 5 agents and observe the Stripe test-mode subscription item quantity = 5. - A1.3 → verify: on that org, launch 6 tasks. Observe 5 active and 1 ready, showing the agent-limit message — and that the limit came from the Stripe webhook (A1.5), not a hand-set cap. - A1.4 → verify: on dev, try 2 and 10 on the min 3, max 9 plan and observe the refusal naming the range (and the next plan where one exists). Then try 9 and observe acceptance. Unit test: max blank → no upper bound. - A1.5 → verify: change 5 → 7 on the billing page. Observe the proration preview, the quantity 7 in Stripe and a limit of 7. - A1.6 → verify: an existing org on `trial` shows the same billing page as before. - A2.1 → verify: on the dev verification plan, set a yearly test price. Buy yearly and observe a Stripe subscription with `interval = year` and quantity = the agents bought (with a monthly hosting item alongside if the org has one). - A2.2 → verify: on dev, switch monthly ↔ yearly and observe the preview; on a plan without a yearly price, observe no toggle. - Cleanup → verify: the dev verification plan and its test prices are deleted through the console after verification (N1). - Quality → verify: open the Plans dialog, UpgradeDialog and SubscriptionCard on dev at desktop and phone width and observe readable labels, no truncation or overflow, no raw keys; every refusal names the range and next step.

Outcome

Per-agent pricing + yearly price shipped (PR #5709, merged, live on dev). Full gate green apart from one unrelated vitest flake (re-run 9535/9535). Review lanes cleared. Dev oracles pass: Stripe qty 5→7, year interval, refusals. Visual check done at 1440 and 375px: clean. A1.3 6-task wait unit-tested only. Drafts 6931/6932 filed.

Pull request

Artifacts

Activity

  • Draft
  • Ready
  • Runner started
  • Runner ended (done)