Phase 12: pricing model alignment + discount ladder + quota purchase model (D-21) #13

Open
opened 2026-05-21 22:53:03 +00:00 by mik-tf · 1 comment
Owner

Sub-issue of hero_onboarding#1.

Closes meta-issue Q#7 + aligns the pricing model to the meta-issue pricing table ($10/5VMs-month + $20/10VMs-month). Pre-Phase-12 code shipped a single-VM-$10-for-1-week placeholder; this rewrites the flow as a quota purchase model (Reading B): pay for a plan once → gain an N-VM allowance for the next 30 days → spin up VMs one-at-a-time over the period.

What landed

Schema (back-compat-safe, all new fields Option):

  • Billing +6 optional fields: current_plan_id, vm_quota_total, vm_quota_used, vm_quota_period_end, first_paid_at, last_active_at
  • VmAllocation +plan_id?: str
  • UsageRecord +applies_to?: ResourceCategory (new enum: hero_os_hourly | llm | vm)

Server:

  • new crates/hero_onboarding_server/src/plans.rs (~270 LOC, 12 unit tests) — PlanConfig + parse_plans_json + default_plans matching meta-issue ($10/5VMs-month + $20/10VMs-month) + validate_plans + find_plan_by_id
  • new crates/hero_onboarding_server/src/discount_ladder.rs (~280 LOC, 21 unit tests) — effective_cost_cents pure fn (Q#7 multiplier table), tier_for_tenure_days, has_active_plan, should_set_first_paid_at, touch_last_active, run_tick
  • new POST /plan/buy route (cookie + KYC + balance pre-flight + active-plan gate; 6 error codes)
  • rewritten vm_allocate_post for quota model (active-plan check + quota check + plan_id snapshot + expires_at override to match vm_quota_period_end + quota_used increment; per-VM cost = plan.cost_cents / plan.vm_count for audit)
  • new POST /admin/discount-ladder-tick admin-secret-gated route
  • discount-aware usage_aggregate::apply_record (multiplier table at decrement site + touch_last_active)
  • webhook_post stamps first_paid_at on first credit (set-once invariant)
  • RecordUsagePayload +applies_to producer-side pipe-through

Dashboard: plan-selector buttons (no active plan) OR quota panel + Spin up VM button (active plan); 6 new error codes + plan_purchased event banner; discount tier label next to credit balance.

CLI: new hero_onboarding_discount_ladder_cron action (24h default; HERO_ONBOARDING_DISCOUNT_LADDER_CRON_INTERVAL_MS override). FORWARDED_ENV +2.

Admin daemon: BillingRow +6 fields; AllocationRow +plan_id; new discount-ladder --once subcommand mirror of aggregate --once; /users +4 columns; /allocations +1 column.

Runbook: new §3.7 Plan configuration + §3.8 Discount ladder operator semantics (~330 LOC).

D-21

Minted at decisions/D-21-discount-ladder-mechanics.md (workspace, not in this repo). Locks 6 surfaces:

  1. Reading B (quota purchase) over Reading A (bulk allocation) — flip cost ~100 LOC + multi-VM rollback semantics.
  2. Q#7(a) wall-clock since first_paid_at (no inactivity reset in v0; slot reserved for the ~15 LOC bolt-on later).
  3. Q#7(b) Hero OS + LLM only (VM cost is the operator's TFGrid cost-recovery floor — never discounted). Flip cost ~20 LOC.
  4. Q#7(c) multiplicativenone ×1.0, week ×0.5, month ×0.25 (75% total at month-tier). Flip to additive cap is a 1-char change.
  5. Plan registry env-var JSON with default_plans() fallback matching meta-issue. Flip to OSIS rootobject is ~40 LOC + 7 trigger stubs.
  6. One active plan at a time/plan/buy rejects with ?error=plan_active if vm_quota_period_end > now. Flip to upgrade-while-active is ~10 LOC.

Acceptance

  • cargo test --workspace 110/110 (+33 vs s2-013 baseline of 77: +12 plans + +21 discount_ladder)
  • cargo fmt --check clean
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • lab build --release --install --workspace VICTORY 3/3 (build #16)
  • lab infocheck 3/3 clean / 0 findings
  • scripts/smoke_discount_ladder.sh 24/24 GREEN
  • scripts/smoke_vm_allocate.sh (rewritten for quota model) 27/27 GREEN

Phase B findings

  1. Line drift -83/-134 on §3 plan cites for webhook_post:1630 (cited :1713) + vm_allocate_post:2501 (cited :2635). Substantive shapes hold.
  2. D-NN race resolution — prompt2.md §3 said "D-20 candidate" but Track A claimed D-20 at s139 for email-provider-sendgrid. Per race rule, Phase 12 mints D-21.
  3. ResourceKind + ResourceCategory kept separate. ResourceKind is producer-side measurement unit (vm_hour, llm_token, hero_os_hour, other); ResourceCategory is aggregator-side discount-applicability category (hero_os_hourly, llm, vm). 1-1 mappable but schema language has no type aliases. Back-compat default applies_to: None → treated as hero_os_hourly.
  4. All new schema fields use ?: Option<T> for back-compat with pre-Phase-12 OSIS rows (codegen emits serde-strict fields without #[serde(default)]).

Phase B.5 SKIPPED per standing criteria (no theorem / wire protocol / migration / crypto; pure-fn business logic).

Tracking

  • Parent: hero_onboarding#1 (meta-issue Q#7)
  • Squash-merged to development (commit TBD recorded at /stop2)
  • Sessions manifest: sessions/2-014-hero-onboarding-phase-12-pricing-discount.yml

Signed-by: mik-tf mik-tf@noreply.invalid

Sub-issue of [hero_onboarding#1](https://forge.ourworld.tf/lhumina_code/hero_onboarding/issues/1). Closes meta-issue Q#7 + aligns the pricing model to the meta-issue pricing table ($10/5VMs-month + $20/10VMs-month). Pre-Phase-12 code shipped a single-VM-$10-for-1-week placeholder; this rewrites the flow as a **quota purchase model** (Reading B): pay for a plan once → gain an N-VM allowance for the next 30 days → spin up VMs one-at-a-time over the period. ### What landed **Schema (back-compat-safe, all new fields Option<T>):** - `Billing` +6 optional fields: `current_plan_id`, `vm_quota_total`, `vm_quota_used`, `vm_quota_period_end`, `first_paid_at`, `last_active_at` - `VmAllocation` +`plan_id?: str` - `UsageRecord` +`applies_to?: ResourceCategory` (new enum: `hero_os_hourly | llm | vm`) **Server:** - new `crates/hero_onboarding_server/src/plans.rs` (~270 LOC, 12 unit tests) — PlanConfig + parse_plans_json + default_plans matching meta-issue ($10/5VMs-month + $20/10VMs-month) + validate_plans + find_plan_by_id - new `crates/hero_onboarding_server/src/discount_ladder.rs` (~280 LOC, 21 unit tests) — `effective_cost_cents` pure fn (Q#7 multiplier table), `tier_for_tenure_days`, `has_active_plan`, `should_set_first_paid_at`, `touch_last_active`, `run_tick` - new `POST /plan/buy` route (cookie + KYC + balance pre-flight + active-plan gate; 6 error codes) - rewritten `vm_allocate_post` for quota model (active-plan check + quota check + plan_id snapshot + expires_at override to match `vm_quota_period_end` + quota_used increment; per-VM cost = `plan.cost_cents / plan.vm_count` for audit) - new `POST /admin/discount-ladder-tick` admin-secret-gated route - discount-aware `usage_aggregate::apply_record` (multiplier table at decrement site + `touch_last_active`) - `webhook_post` stamps `first_paid_at` on first credit (set-once invariant) - `RecordUsagePayload` +`applies_to` producer-side pipe-through **Dashboard:** plan-selector buttons (no active plan) OR quota panel + Spin up VM button (active plan); 6 new error codes + `plan_purchased` event banner; discount tier label next to credit balance. **CLI:** new `hero_onboarding_discount_ladder_cron` action (24h default; `HERO_ONBOARDING_DISCOUNT_LADDER_CRON_INTERVAL_MS` override). FORWARDED_ENV +2. **Admin daemon:** BillingRow +6 fields; AllocationRow +`plan_id`; new `discount-ladder --once` subcommand mirror of `aggregate --once`; `/users` +4 columns; `/allocations` +1 column. **Runbook:** new §3.7 Plan configuration + §3.8 Discount ladder operator semantics (~330 LOC). ### D-21 Minted at [`decisions/D-21-discount-ladder-mechanics.md`](https://forge.ourworld.tf/lhumina_code/hero_onboarding) (workspace, not in this repo). Locks 6 surfaces: 1. **Reading B (quota purchase)** over Reading A (bulk allocation) — flip cost ~100 LOC + multi-VM rollback semantics. 2. **Q#7(a) wall-clock since `first_paid_at`** (no inactivity reset in v0; slot reserved for the ~15 LOC bolt-on later). 3. **Q#7(b) Hero OS + LLM only** (VM cost is the operator's TFGrid cost-recovery floor — never discounted). Flip cost ~20 LOC. 4. **Q#7(c) multiplicative** — `none ×1.0`, `week ×0.5`, `month ×0.25` (75% total at month-tier). Flip to additive cap is a 1-char change. 5. **Plan registry env-var JSON** with `default_plans()` fallback matching meta-issue. Flip to OSIS rootobject is ~40 LOC + 7 trigger stubs. 6. **One active plan at a time** — `/plan/buy` rejects with `?error=plan_active` if `vm_quota_period_end > now`. Flip to upgrade-while-active is ~10 LOC. ### Acceptance - `cargo test --workspace` **110/110** (+33 vs s2-013 baseline of 77: +12 `plans` + +21 `discount_ladder`) - `cargo fmt --check` clean - `cargo clippy --workspace --all-targets -- -D warnings` clean - `lab build --release --install --workspace` VICTORY 3/3 (build #16) - `lab infocheck` 3/3 clean / 0 findings - `scripts/smoke_discount_ladder.sh` **24/24 GREEN** - `scripts/smoke_vm_allocate.sh` (rewritten for quota model) **27/27 GREEN** ### Phase B findings 1. Line drift -83/-134 on §3 plan cites for `webhook_post:1630` (cited :1713) + `vm_allocate_post:2501` (cited :2635). Substantive shapes hold. 2. **D-NN race resolution** — prompt2.md §3 said "D-20 candidate" but Track A claimed D-20 at s139 for `email-provider-sendgrid`. Per race rule, Phase 12 mints **D-21**. 3. `ResourceKind` + `ResourceCategory` kept separate. ResourceKind is producer-side measurement unit (`vm_hour`, `llm_token`, `hero_os_hour`, `other`); ResourceCategory is aggregator-side discount-applicability category (`hero_os_hourly`, `llm`, `vm`). 1-1 mappable but schema language has no type aliases. Back-compat default `applies_to: None` → treated as `hero_os_hourly`. 4. All new schema fields use `?: Option<T>` for back-compat with pre-Phase-12 OSIS rows (codegen emits serde-strict fields without `#[serde(default)]`). **Phase B.5 SKIPPED** per standing criteria (no theorem / wire protocol / migration / crypto; pure-fn business logic). ### Tracking - Parent: [hero_onboarding#1](https://forge.ourworld.tf/lhumina_code/hero_onboarding/issues/1) (meta-issue Q#7) - Squash-merged to `development` (commit TBD recorded at /stop2) - Sessions manifest: `sessions/2-014-hero-onboarding-phase-12-pricing-discount.yml` Signed-by: mik-tf <mik-tf@noreply.invalid>
Author
Owner

Squash-merged to development as f667272. Feature branch track-agent-2/phase-12-pricing-discount deleted (remote + local). Acceptance gates GREEN: cargo test 110/110, fmt + clippy clean, lab build VICTORY 3/3 (build #16), lab infocheck 3/3 clean, smoke_discount_ladder 24/24, smoke_vm_allocate 27/27.

Signed-by: mik-tf mik-tf@noreply.invalid

Squash-merged to `development` as [`f667272`](https://forge.ourworld.tf/lhumina_code/hero_onboarding/commit/f667272). Feature branch `track-agent-2/phase-12-pricing-discount` deleted (remote + local). Acceptance gates GREEN: cargo test 110/110, fmt + clippy clean, lab build VICTORY 3/3 (build #16), lab infocheck 3/3 clean, smoke_discount_ladder 24/24, smoke_vm_allocate 27/27. Signed-by: mik-tf <mik-tf@noreply.invalid>
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_onboarding#13
No description provided.