Plan parameters schema and Trial seed

Task 5256 · Done · Coder · created · updated

## Original requirement (epic, verbatim) Fold Trial mode into Billing plans: - Trial becomes a regular Plan, that is pre-seeded but may be changed by Operator as they please - All functions of Trial become configurable parameters of the plan - Trial node: each plan may have multiple products that are coming with the plan. One of them is a Jaah-provided node - operator can pick what kind of node - same as with Trial now and the same controls (reuse) as with user adding products. They may be arbitrary number of products attached - When user is signing up he is picking the plan from offered plans that are self-served. Operator can change this plan to any other configured in the system that are active/offered, including those that are not Self-served. E.g. Operator may choose a different flavor of a Trial, such as one without a node - Operator can override Plan parameters per organization, instance of the plan - One of the goals is to make it more flexible with less specialized configuration knobs Settled inputs: BA PRD (Task 5248: R1–R9, D-1…D-9, A-1…A-11, NC-1…NC-9) and Architecture (Task 5249: DA-1…DA-6, §1–§13). Use `task_read` on 5248/5249 for the full documents; this body carries everything this child needs. This child is C1 of 9 (C1…C9) of epic "Fold Trial Mode Into Billing Plans". Binding operator decisions: D-1 bundled products included in plan price · D-2 leaving a plan → today's trial-node keep-or-stop rule generalized · D-3 paid sign-up plan paid after approval · D-4 old knobs move onto plans and are deleted; empty plan value = unlimited/off, not platform default · D-5 attachable products: Shared node (tier), Dedicated node (size), Jaah AI · D-6 per-org overrides: limits, duration/end date, bundled products, price · D-7 any plan with a duration pauses work like Trial when it ends · D-8 operator-chosen Default plan knob (seeded Trial); any plan retirable except the current default · D-9 Jaah AI bundle credits at plan start + every billing month, may be zero, no rollover · DA-1 paid subscription ends → Default plan · DA-2 plan products edit applies to all members without a products override · DA-3 duration edit is live: end = plan_started_at + duration_days unless overridden · DA-4 plan move clears ALL overrides incl. caps · DA-5 operator-created orgs get bundled products · DA-6 returning to a plan is fresh every time. Quality bar: every UI string follows docs/standards/ui-copy-standards.md; empty values read "Unlimited"/"Never", never a blank. A literally-compliant but confusing/unreadable result fails this child. Everything is live on dev.jaah.ai after merge — no feature flag (AC9.1, R29). ## This child — scope (HOW, from Architecture Task 5249 §2) One Alembic revision + SQLAlchemy models; expand-only — nothing is dropped, nothing existing is rewritten except the backfills listed: - `billing_plans` += `duration_days INTEGER NULL CHECK >0` (NULL = never ends), `hosting_allowed BOOLEAN NOT NULL DEFAULT true`. Existing six `limit_*` unchanged in type; this revision backfills every NULL `limit_*` on existing rows (team, enterprise) with today's knob value (A-3) so C2 can read NULL as unlimited. - New `billing_plan_products` (operator plane, ADR-0076 entity shape): `id uuid pk`, `plan_id uuid FK billing_plans ON DELETE CASCADE`, `kind text CHECK IN ('shared_node','dedicated_node','jaah_ai_credit')`, `quantity int NOT NULL CHECK >=1`, `tier_key`, `instance_type`, `storage_gb`, `credit_cents bigint CHECK >=0`, `sort_order`, entity fields. Per-kind CHECKs: shared ⇒ tier_key; dedicated ⇒ instance_type, storage_gb; credit ⇒ credit_cents. No FK to `rental_prices` (NC-2). - `orgs` += `plan_started_at timestamptz NOT NULL DEFAULT now()`, `plan_end_overridden bool NOT NULL DEFAULT false`, `plan_end_override timestamptz NULL`, `hosting_allowed_override bool NULL`, `products_overridden bool NOT NULL DEFAULT false`, `pending_plan_key text NULL`. Existing `orgs.limit_*`: NULL = plan, value = override. - New `org_plan_products` (OrgScoped, `org_unique`) — org-scoped copy of billing_plan_products keyed by org_id (whole-list products override). `org_signups.plan_key text NULL`. - New `org_bundled_products` (org-scoped instances; replaces `org_trial_nodes` later): `id uuid`, `org_id`, `fingerprint text`, `ordinal int CHECK >=1`, `kind text`, `node_id uuid NULL` (NO FK), `hosting_id uuid NULL` FK node_rentals SET NULL, `state text CHECK IN ('provisioning','failed','live','released','kept','ended')`, `released_at`, `error`, `attempts int`, entity fields. Unique partial index `(org_id, fingerprint, ordinal) WHERE state NOT IN ('kept','ended')`. Fingerprint grammar pinned: `shared_node:<tier_key>` · `dedicated_node:<instance_type>:<storage_gb>`. - `node_rentals` CHECKs widened: `kind` + 'bundled', `stop_cause` + 'plan_end'; `NODE_ORDER_PATHS` + 'bundle_keep'; event kinds + `rental.bundle_ending`. NO rewrite of existing node_rentals/org_trial_nodes rows (C4 adopts them in code — Architecture §4.7 — because today's `trial_hostings._adopt` would re-create a trial hosting). - Seed/backfill (§2.6), knob values read from `system_state` (operator tier) or, when no row exists, the knob's code default hard-coded in the revision: 1. Insert plan `trial`: name "Trial", blurb = today's `TRIAL_BLURB`, self_serve=true, active=true, sort_order = min−1, duration_days = trial_length_days (0⇒NULL), caps = org_limit_* (0⇒NULL), hosting_allowed = NOT rent_requires_paid_plan (operator value). If trial_node_placement='multi_node_host': one product shared_node × 1, tier_key='shared'. 2. Other plan rows: NULL limit_* ← knob value (A-3). 3. `system_state` knob row `default_plan_key='trial'` (D-8); `rental_aws_credential_id` ← `trial_node_aws_credential_id` when the former is empty (AC8.3). 4. Org-tier `rent_requires_paid_plan` org_settings rows ⇒ `orgs.hosting_allowed_override = NOT value`. 5. plan_started_at: created_at for plan_key='trial' orgs, else COALESCE(plan_chosen_at, created_at). 6. Trial-org end with 1-hour tolerance: trial_ends_at NULL while knob>0 ⇒ plan_end_overridden=true, override NULL; |trial_ends_at − (created_at + knob days)| > 1h ⇒ override = trial_ends_at; else no override. Knob=0 and trial_ends_at set ⇒ override = trial_ends_at. 7. Every org with plan_key='trial', lifecycle='active' and no org_trial_nodes row ⇒ products_overridden=true with no lines + one `org_plan_overrides_migrated` activity row each. Pending orgs and Team/Enterprise orgs are NOT overridden. - Models only: no consumer rewiring here (that is C2+). Revision id follows the repo's sequential-revision rule; migration must be additive (docs/standards/coding-standards.md). - Migration test on a fixture: trial org with node, trial without node, pending org, no-end org, edited end, team plan with NULL caps, org-tier hosting row — proving AC9.2 equality of the migrated values. ## Acceptance criteria (PRD, verbatim) carried by this child - AC1.1 (part: the row) A plan row "Trial" (key `trial`) exists in Settings > Billing > Plans on every environment after deploy, listed alongside Team/Enterprise with the same editor and actions (edit name, blurb, sort, self-serve, price, parameters, products, retire). — this child delivers the row; C6 delivers the editor. - AC8.3 (part: the value) The AWS credential previously on `trial_node_aws_credential_id` still governs where hosted machines launch when no hosting credential is set — moved to its living home (Billing > Hosting), not lost. - AC9.2 Existing orgs after migration: same plan name, same effective caps, same end date, same trial node running untouched, same dispatch eligibility as before (A-2, A-3). Verify: before/after snapshot on dev of `GET /api/orgs/{id}/limits`, `/plan`, trial-node status for every tenant org — diff empty except vocabulary renames. - AC9.3 (part: expand) Migrations are additive while two releases overlap (coding standards): the old knob rows are read by nothing new and deleted in the release after, or the deploy is proven single-release safe by the Architect. ## Out of scope: Do NOT touch any consumer of the plan (org_limits, pool_state, hosting_catalog, org_plan_admin, billing/*, trial_node.py, trial_hostings.py, web) — C2–C8 own those. Do NOT delete any knob, column, table or CHECK value (C9 contracts). Do NOT rewrite existing `node_rentals` or `org_trial_nodes` rows. Only C1, C2 and C9 add Alembic revisions in this epic. Epic out of scope (PRD §5, verbatim): - NC-1 Stripe money model: monthly USD, Stripe owns the price; no new intervals/currencies, no proration redesign. `billing/plans.py::push_price` mechanics unchanged except generalizing the per-org custom price beyond Enterprise. - NC-2 The hosting price catalogue (`rental_prices`), tiers/sizes and their editor — read by bundles, not changed. - NC-3 Placement/isolation of Shared nodes (`pick_shared_host`, ADR-0130) — reused as is. - NC-4 Abuse quotas, cron minimum period, sign-up knobs, `rental_grace_days`/`rental_retention_days`, hosting offering switches (A-9) stay global knobs. - NC-5 Jaah AI wallet/top-up/refund mechanics (`billing/jaah_credit.py`, `token_billing`) — bundles only add a monthly credit entry; no change to top-ups, auto-recharge or refunds. - NC-6 No change to the approval policy itself (manual review, auto-approve domains and their daily cap). - NC-7 No new product types beyond D-5 (no storage-only, GitHub runner, or self-hosted products as bundles). - NC-8 Operator orgs (org zero / own machines) keep their exemptions; bundles never provision into them. - NC-9 No forge/Slack/notification-channel redesign; reuse existing billing notices for warnings. Deferred (do NOT build): abuse quotas as plan parameters; Jaah AI credit rollover; annual plans; a configurable bundle provision throttle; tenant self-service switching between free plans. Done means: - Migration fixture test passes: each fixture org's migrated plan_started_at / end override / hosting_allowed_override / products_overridden matches the §2.6 rules; team/enterprise have no NULL `limit_*` that previously resolved to a knob; trial plan row values equal the knob values (oracle: the new pytest, cited output). - On dev after deploy: `SELECT key,name,duration_days,limit_runner_slots,limit_nodes,limit_projects,limit_users,limit_active_worktrees,limit_github_runners,hosting_allowed FROM billing_plans` shows the Trial row with today's knob values (14 days, 4/2/10/10/4/2 unless edited) — cite the query output. - `alembic upgrade head` then `downgrade -1` then `upgrade head` on the local stack succeeds. - BEFORE merging: capture an AC9.2 baseline on dev of `GET /api/orgs/{id}/limits`, `GET /api/orgs/{id}/plan` and trial-node status for EVERY tenant org, and upload it as a `file` task artifact so C4 and C9 can diff against it. After deploy: re-snapshot and show the diff is empty except new fields. - `./dc check full` green; PR merged into develop; the change is live on dev.jaah.ai with no flag (AC9.1). - Rendered report cites file:line per AC above and the observed output of each oracle.

Outcome

Revision 0374 merged (#4618): plan params, product/bundle tables, Trial seeded; full gate green. Dev at 0374, AC9.2 before/after diff empty (both uploaded). Code Trial ignores the row until C2.

Pull request

Artifacts

Activity

  • Draft
  • Ready
  • Runner started
  • Awaiting response
  • Awaiting response
  • Runner resumed
  • Awaiting response
  • Awaiting response
  • Runner resumed
  • Awaiting response
  • Awaiting response
  • Runner resumed
  • Runner ended (done)