Skip to content

feat(kiloclaw): add subscription billing, trial lifecycle, and Stripe integration#1054

Open
jeanduplessis wants to merge 8 commits intomainfrom
jdp/kiloclaw-billing
Open

feat(kiloclaw): add subscription billing, trial lifecycle, and Stripe integration#1054
jeanduplessis wants to merge 8 commits intomainfrom
jdp/kiloclaw-billing

Conversation

@jeanduplessis
Copy link
Contributor

@jeanduplessis jeanduplessis commented Mar 12, 2026

Summary

Adds the full KiloClaw billing backend and UI, ready for production launch:

  • Database: kiloclaw_subscriptions and kiloclaw_email_log tables (single consolidated migration 0049). GDPR soft-delete coverage added to softDeleteUser — blocks active (not pending cancellation), past_due, and unpaid subscriptions.
  • Stripe integration: Webhook handlers for subscription lifecycle, schedule events, and invoice payment. Commit plan uses Stripe subscription schedules with duration API for exact 6-month calendar periods. Checkout sessions with plan metadata, promo code support (Standard only), Rewardful referral tracking, and configurable billing delay via STRIPE_KILOCLAW_BILLING_START env var. Schedule creation is idempotent on webhook replay — skips if stripe_schedule_id is already set.
  • Access gate: clawAccessProcedure tRPC middleware enforces subscription/trial/earlybird access on all mutating KiloClaw operations. Three access checks (requireKiloClawAccess, ensureProvisionAccess, getBillingStatus.hasAccess) are kept consistent — past_due grants access only until the billing lifecycle cron sets suspended_at.
  • Trial lifecycle: Auto-creates 30-day trial on first provision. Standalone startTrial mutation for the welcome page flow. Billing lifecycle cron (hourly, Vercel) suspends expired trials/subscriptions, destroys instances past grace period, handles earlybird expiry.
  • Transactional emails: 9 Mailgun templates covering trial warnings, suspension notices, destruction warnings, and earlybird expiry. Idempotent via kiloclaw_email_log unique constraint. All email CTAs link to /claw where AccessLockedDialog handles payment recovery via Stripe portal.
  • Welcome page: Shown for users without access and no instance (instance === null). Destroyed instances route to ClawDashboard where AccessLockedDialog handles them. Three options: free trial (conditional on eligibility), Standard, and Commit plans.
  • UI: Billing components wired to real tRPC data. SubscriptionCard correctly distinguishes mandatory commit→standard auto-transitions from user-requested plan switches. BillingWrapper renders billing UI unconditionally (no feature flag).
  • Router endpoints: getBillingStatus, startTrial, createSubscriptionCheckout, cancelSubscription, reactivateSubscription, renewCommit, switchPlan, cancelPlanSwitch, createBillingPortalSession.
  • Refactoring: Dropped redundant Claw prefix from billing components, removed as casts in favor of satisfies and flow-sensitive typing, lazy-initialized Stripe price ID metadata to avoid build-time crashes.

Verification

  • pnpm typecheck — passes across all workspace packages
  • pnpm test — 167 suites, 2648 tests pass, 6 skipped
  • Key test coverage:
    • kiloclaw-billing-router.test.ts — 13 tests: startTrial happy/reject paths, getBillingStatus trial eligibility, createSubscriptionCheckout promo codes/Rewardful/trial_end, handleKiloClawSubscriptionUpdated status mapping
    • user.test.ts — GDPR soft-delete: blocks active (not pending cancellation), allows cancel_at_period_end: true, blocks past_due/unpaid
  • pnpm build — compiles successfully

Visual Changes

Welcome Page

CleanShot 2026-03-14 at 09 28 03@2x

Trial States

State Screenshot
Trial active (22 days left) trial-active
Trial ending soon (5 days) trial-ending-soon
Trial ending very soon (2 days) trial-very-soon
Trial expires today trial-today
Trial expired (instance stopped) trial-expired-stopped
Trial expired (instance destroyed) trial-expired-destroyed

Earlybird States

State Screenshot
Earlybird active earlybird-active
Earlybird ending soon earlybird-ending
Earlybird expired earlybird-expired

Subscription States

State Screenshot
Subscribed (Standard) sub-standard
Subscribed (Commit) sub-commit
Subscription canceling sub-canceling
Subscription past due sub-past-due
Subscription expired (stopped) sub-expired-stopped
Subscription expired (destroyed) sub-expired-destroyed

Dialogs

Dialog Screenshot
Plan selection plan-selection
Cancel subscription cancel-dialog

Emails

Screenshot
Helium 2026-03-13 16 38 32
Helium 2026-03-13 16 38 31
Helium 2026-03-13 16 38 30
Helium 2026-03-13 16 38 30
Helium 2026-03-13 16 38 29
Helium 2026-03-13 16 38 28
Helium 2026-03-13 16 38 27
Helium 2026-03-13 16 38 26

Reviewer Notes

Post-Deployment Actions

  1. Environment variables — set in Vercel before deploying:

    • STRIPE_KILOCLAW_COMMIT_PRICE_ID — Stripe Price ID for the 6-month commit plan (required)
    • STRIPE_KILOCLAW_STANDARD_PRICE_ID — Stripe Price ID for the monthly standard plan (required)
    • STRIPE_KILOCLAW_BILLING_START — ISO 8601 date; checkouts before this date get a delayed trial_end so billing starts on launch day. Leave empty/unset for immediate billing.
  2. Database migration — single consolidated migration 0049 creates both kiloclaw_subscriptions and kiloclaw_email_log tables. Runs automatically.

  3. Stripe Dashboard — create two Products/Prices for the commit and standard plans, and set the env vars above. Optionally create a promo coupon (100% off, repeating, 2 months) restricted to the Standard plan product.

  4. Stripe webhook events — no changes needed. The existing webhook endpoint already handles all required event types (customer.subscription.created/updated/deleted, subscription_schedule.updated, invoice.paid). This PR adds KiloClaw-specific branching within those existing handlers via metadata.type === 'kiloclaw'.

  5. Cron/api/cron/kiloclaw-billing-lifecycle is registered in vercel.json running hourly. Requires CRON_SECRET (likely already set).

  6. No feature flag — billing is always active. There is no rollout toggle.

Key design decisions

  • The kiloclaw_subscriptions table uses a user_id UNIQUE constraint — one billing row per user.
  • Stripe webhook handlers use subscription.metadata.type === 'kiloclaw' to distinguish KiloClaw subscriptions from org/pass subscriptions.
  • Commit plan schedule uses Stripe's duration: { interval: 'month', interval_count: 6 } for exact calendar months. commit_ends_at is derived from the schedule's resolved phases, not current_period_end (which may reflect a delayed-billing trial boundary).
  • Schedule creation in subscription.created is idempotent — skips if stripe_schedule_id is already set, preventing orphaned schedules on webhook replay.
  • renewCommit schedule rebuild finds the currently active phase (not phases[0]), handling delayed-billing prelaunch phases correctly.
  • The SubscriptionCard distinguishes mandatory commit→standard auto-transitions from user-requested plan switches — "Cancel Switch" only appears for genuine user requests.
  • Cron stop()/destroy() calls are wrapped in inner try/catch so DB state transitions proceed even on 404/409 API errors.
  • Email idempotency via kiloclaw_email_log INSERT ... ON CONFLICT DO NOTHING. Failed provider calls delete the log row so the next cron retries.
  • past_due subscriptions retain access only until the billing lifecycle cron sets suspended_at (14-day grace via past_due_since, not updated_at).

@kilo-code-bot
Copy link
Contributor

kilo-code-bot bot commented Mar 12, 2026

Code Review Summary

Status: 1 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 0
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
src/lib/kiloclaw/access-gate.ts 57 Billing enforcement is always on; there is no feature-flag guard around the new KiloClaw access gate.

Fix these issues in Kilo Cloud

Other Observations (not in diff)

Issues found in unchanged code that cannot receive inline comments:

N/A

Files Reviewed (9 files)
  • src/lib/kiloclaw/access-gate.ts - 1 issue
  • src/routers/kiloclaw-router.ts - 0 issues
  • src/lib/kiloclaw/stripe-handlers.ts - 0 issues
  • src/lib/kiloclaw/billing-lifecycle-cron.ts - 0 issues
  • src/app/(app)/claw/components/billing/BillingWrapper.tsx - 0 issues
  • src/app/(app)/claw/page.tsx - 0 issues
  • src/app/(app)/claw/components/ClawDashboard.tsx - 0 issues
  • src/lib/kiloclaw/stripe-invoice-classifier.server.ts - 0 issues
  • src/lib/stripe.ts - 0 issues

Reviewed by gpt-5.4-20260305 · 880,829 tokens

@jeanduplessis jeanduplessis force-pushed the jdp/kiloclaw-billing branch 2 times, most recently from 4e38b88 to 87adb5e Compare March 13, 2026 09:46
@jeanduplessis
Copy link
Contributor Author

In case anyone is interested here's my local verification test plan:

Here's the full local setup guide and manual verification checklist for the KiloClaw billing feature.

---

KiloClaw Billing — Local Verification Guide

1. Local Environment Setup

Prerequisites

bash
nvm use 22
brew install pnpm stripe/stripe-cli/stripe
pnpm add -g vercel


Install & Configure

bash
pnpm install && git lfs pull

# Pull env vars from Vercel (includes all Stripe keys, email API keys, etc.)
vercel login && vercel link --project kilocode-app && vercel env pull


Database

bash
docker compose -f dev/docker-compose.yml up -d   # Postgres 18 + Redis 7
pnpm drizzle migrate                              # apply migrations (incl. 0049/0050 for billing tables)


Stripe Webhooks

In a separate terminal:

bash
stripe login
pnpm stripe   # forwards webhooks to localhost:3000/api/stripe/webhook


Copy the whsec_... secret from the CLI output into .env.development.local:


STRIPE_WEBHOOK_SECRET="whsec_..."


KiloClaw-specific env vars

Verify these are present after vercel env pull (check .env.local):

| Variable | Purpose |
|---|---|
| STRIPE_KILOCLAW_COMMIT_PRICE_ID | Stripe price for commit plan ($54/6mo) |
| STRIPE_KILOCLAW_STANDARD_PRICE_ID | Stripe price for standard plan ($25/mo) |
| STRIPE_KILOCLAW_EARLYBIRD_PRICE_ID | Stripe price for earlybird one-time purchase |
| STRIPE_KILOCLAW_EARLYBIRD_COUPON_ID | (optional) Stripe coupon for earlybird |
| STRIPE_KILOCLAW_BILLING_START | ISO 8601 date; delayed billing if before this date |
| KILOCLAW_API_URL | Base URL of the KiloClaw worker API |
| KILOCLAW_INTERNAL_API_SECRET | Auth secret for internal worker calls |
| CRON_SECRET | Bearer token for cron endpoint auth |
| EMAIL_PROVIDER | customerio or mailgun |

Start the dev server

bash
pnpm dev   # http://localhost:3000


Fake Login

Construct a URL like:


http://localhost:3000/users/sign_in?fakeUser=kilo-<your-username>-<HHmmss>@example.com&callbackPath=/claw


For admin access use @admin.example.com. Wait for the "creating your account" spinner to finish before proceeding.

---

2. Admin Email Preview

Before testing live flows, preview all 9 email templates via the admin panel:

1. Log in as admin (@admin.example.com)
2. Open admin panel via the account icon dropdown (top-right)
3. Navigate to Email Testing
4. Select each claw* template from the dropdown, pick Mailgun or Customer.io, and hit Preview

Templates to verify:

| # | Template | Subject |
|---|---|---|
| 1 | clawTrialEndingSoon | Your KiloClaw Trial Ends in 5 Days |
| 2 | clawTrialExpiresTomorrow | Your KiloClaw Trial Expires Tomorrow |
| 3 | clawSuspendedTrial | Your KiloClaw Trial Has Ended |
| 4 | clawSuspendedSubscription | Your KiloClaw Subscription Has Ended |
| 5 | clawSuspendedPayment | Action Required: KiloClaw Payment Overdue |
| 6 | clawDestructionWarning | Your KiloClaw Instance Will Be Deleted in 2 Days |
| 7 | clawInstanceDestroyed | Your KiloClaw Instance Has Been Deleted |
| 8 | clawEarlybirdEndingSoon | Your KiloClaw Earlybird Access Ends Soon |
| 9 | clawEarlybirdExpiresTomorrow | Your KiloClaw Earlybird Access Expires Tomorrow |

You can also Send Test to your own email to verify delivery end-to-end.

---

3. Manual Verification Checklist

A. Welcome Page & Trial

| # | Scenario | Steps | Expected |
|---|---|---|---|
| A1 | Welcome page shown for new user | Log in as a fresh user, navigate to /claw | WelcomePage renders with 3 cards: Free Trial, Commit ($54/6mo), Standard ($25/mo) |
| A2 | Start free trial | Click "Start Free Trial" | DB row: plan=trial, status=trialing, trial_ends_at = now+30d. Redirects to dashboard. |
| A3 | Trial active banner | View /claw dashboard while trialing | Blue info banner shows "X days remaining", "Subscribe Now" CTA |
| A4 | Trial not re-startable | Try startTrial again (e.g. refresh or API call) | Error: "You already have a subscription." |
| A5 | Trial ineligible after provision | Destroy instance, create new user with same email (or manually delete sub row) but leave instance row | trialEligible: false — no Free Trial card on WelcomePage |

B. Stripe Checkout — Standard Plan

| # | Scenario | Steps | Expected |
|---|---|---|---|
| B1 | Checkout redirects to Stripe | From WelcomePage or PlanSelectionDialog, select Standard, click Subscribe | Redirected to Stripe Checkout; allow_promotion_codes: true |
| B2 | Successful checkout | Complete Stripe checkout with test card 4242 4242 4242 4242 | Redirected to /payments/kiloclaw/success; spinner polls; shows "Subscription Active!"; auto-redirects to /claw |
| B3 | Webhook processes | Check Stripe CLI terminal | customer.subscription.created fired; DB row: plan=standard, status=active |
| B4 | Dashboard post-subscribe | View /claw | No billing banner. SubscriptionCard shows plan=Standard, next billing date. |

C. Stripe Checkout — Commit Plan

| # | Scenario | Steps | Expected |
|---|---|---|---|
| C1 | Checkout redirects to Stripe | Select Commit plan, click Subscribe | Redirected to Stripe Checkout; allow_promotion_codes: false |
| C2 | Successful checkout | Complete with test card | Same success flow as B2 |
| C3 | Subscription + schedule created | Check Stripe CLI / Dashboard | subscription.created + subscription_schedule created with phases: commit (6mo) → standard |
| C4 | Dashboard | View /claw | SubscriptionCard shows plan=Commit, commit end date, scheduled switch to Standard |

D. Delayed Billing (Prelaunch)

| # | Scenario | Steps | Expected |
|---|---|---|---|
| D1 | Billing delayed if before start date | Set STRIPE_KILOCLAW_BILLING_START to a future date, create checkout | Stripe session has subscription_data.trial_end = billing start timestamp |
| D2 | Stripe trialing maps to active | After checkout, verify DB | status=active (not trialing) even though Stripe shows trialing |
| D3 | No delay after start date | Set STRIPE_KILOCLAW_BILLING_START to a past date, create checkout | No trial_end in Stripe session; immediate charge |

E. Plan Switching

| # | Scenario | Steps | Expected |
|---|---|---|---|
| E1 | Standard → Commit | From SubscriptionCard, click "Switch Plan", select Commit | Stripe schedule created: current → commit (6mo) → standard. DB: scheduled_plan=commit |
| E2 | Commit → Standard | From SubscriptionCard, click "Switch Plan", select Standard | Stripe schedule: current → standard. DB: scheduled_plan=standard |
| E3 | Cancel plan switch | Click "Cancel Plan Switch" | Schedule released. DB: scheduled_plan=null, stripe_schedule_id=null |
| E4 | Schedule completes | Wait for (or simulate) schedule phase transition | subscription_schedule.updated webhook fires with status=completed; DB plan updates to scheduled_plan |

F. Commit Renewal

| # | Scenario | Steps | Expected |
|---|---|---|---|
| F1 | Renew early | On commit plan, click "Renew Early $54" | Invoice created + paid; commit_ends_at extended by 6 months; Stripe schedule rebuilt |

G. Cancellation & Reactivation

| # | Scenario | Steps | Expected |
|---|---|---|---|
| G1 | Cancel subscription | Click "Cancel", confirm in CancelDialog | DB: cancel_at_period_end=true. Stripe: cancel_at_period_end=true. Banner: "Subscription canceling — access until [date]" |
| G2 | Reactivate | Click "Reactivate" on CancelingSubscriptionCard | DB: cancel_at_period_end=false. For commit plans: schedule recreated |
| G3 | Period ends after cancel | Let period expire (or delete sub in Stripe) | subscription.deleted webhook → DB: status=canceled |

H. Billing Lifecycle Cron — Trial Expiry Path

Invoke the cron locally:
bash
curl -H "Authorization: Bearer <CRON_SECRET>" http://localhost:3000/api/cron/kiloclaw-billing-lifecycle


| # | Scenario | DB Setup | Expected |
|---|---|---|---|
| H1 | Trial 5-day warning | status=trialing, trial_ends_at = now+4d | claw_trial_5d email sent; email_log row created |
| H2 | Trial 1-day warning | status=trialing, trial_ends_at = now+0.5d | claw_trial_1d email sent |
| H3 | Trial expiry (Sweep 1) | status=trialing, trial_ends_at in the past | Instance stopped; status→canceled, suspended_at set, destruction_deadline = now+7d; claw_suspended_trial email |
| H4 | Destruction 2-day warning (Sweep 2.5) | Suspended, destruction_deadline = now+1.5d | claw_destruction_warning email |
| H5 | Instance destruction (Sweep 3) | Suspended, destruction_deadline in the past | Instance destroyed via API; destroyed_at set; claw_instance_destroyed email |
| H6 | Email idempotency | Run cron again on same user | Emails skipped (email_log prevents duplicates); emails_skipped count increments |

I. Billing Lifecycle Cron — Subscription Expiry Path

| # | Scenario | DB Setup | Expected |
|---|---|---|---|
| I1 | Subscription period expired (Sweep 2) | status=canceled, current_period_end in the past, not suspended | Instance stopped; suspended_at set, destruction_deadline = now+7d; claw_suspended_subscription email |
| I2 | Past-due cleanup (Sweep 4) | status=past_due, updated_at > 14 days ago | Instance stopped; suspended_at set, destruction_deadline = now+7d; claw_suspended_payment email |

J. Billing Lifecycle Cron — Earlybird Path

| # | Scenario | DB Setup | Expected |
|---|---|---|---|
| J1 | Earlybird 14-day warning | Earlybird purchase, no active sub, expiry = now+12d | claw_earlybird_14d email |
| J2 | Earlybird 1-day warning | Earlybird purchase, no active sub, expiry = now+0.5d | claw_earlybird_1d email |

K. Auto-Resume on Resubscription

| # | Scenario | Steps | Expected |
|---|---|---|---|
| K1 | Resubscribe after suspension | User is suspended (trial expired). Complete new Stripe checkout. | subscription.created webhook → autoResumeIfSuspended: instance started, suspended_at/destruction_deadline cleared, suspension email_log entries deleted |
| K2 | Resubscribe after destruction | User's instance was destroyed. Complete checkout. | Subscription active, but no auto-start (instance doesn't exist). User must provision new instance. |

L. Access Gate

| # | Scenario | Steps | Expected |
|---|---|---|---|
| L1 | Active sub → access granted | Hit any operational endpoint (start, stop, etc.) | Succeeds |
| L2 | Trialing → access granted | During trial period | Succeeds |
| L3 | Earlybird → access granted | Before 2026-09-26 with earlybird purchase | Succeeds |
| L4 | Canceled → access blocked | After trial/sub expired | FORBIDDEN; AccessLockedDialog shown with appropriate ClawLockReason |
| L5 | Past-due → access granted | During 14-day grace | Succeeds; PastDueSubscriptionCard shown with "Update Payment" |

M. UI Banner States

| # | Banner State | Condition | Visual |
|---|---|---|---|
| M1 | trial_active | Trialing, >5 days left | Blue info: "X days remaining" |
| M2 | trial_ending_soon | Trialing, 2-5 days left | Amber warning: "Trial ends in X days" |
| M3 | trial_ending_very_soon | Trialing, 1-2 days left | Red danger |
| M4 | trial_expires_today | Trialing, <1 day left | Red danger: "Trial expires today" |
| M5 | earlybird_active | Earlybird access, no banner (EarlybirdActiveCard shown instead) | Earlybird card |
| M6 | earlybird_ending_soon | Earlybird, <14 days left | Amber warning |
| M7 | subscription_canceling | cancel_at_period_end=true | Amber: "Access until [date]" |
| M8 | subscription_past_due | status=past_due | Red: "Update Payment" |
| M9 | subscribed | Active, no issues | No banner |

N. AccessLockedDialog Variants

| # | Lock Reason | Trigger | Dialog Copy |
|---|---|---|---|
| N1 | trial_expired_instance_alive | Trial ended, instance not yet destroyed | "Subscribe to Resume" |
| N2 | trial_expired_instance_destroyed | Trial ended, instance destroyed | "Subscribe" (will need new provision) |
| N3 | earlybird_expired | Earlybird passed 2026-09-26 | "Subscribe to Continue" |
| N4 | subscription_expired_instance_alive | Sub canceled, instance alive | "Subscribe to Resume" |
| N5 | subscription_expired_instance_destroyed | Sub canceled, instance destroyed | "Subscribe" |
| N6 | past_due_grace_exceeded | Past due >14 days, suspended | "Update Payment Method" |

---

4. Automated Tests

Run the billing-specific tests:

bash
pnpm test -- src/routers/kiloclaw-billing-router.test.ts


This covers: trial creation/rejection/idempotency, trialEligible logic, checkout promo code behavior, delayed billing trial_end presence, and Stripe status mapping (trialing→active for paid plans).

Run the full suite:

bash
pnpm validate   # typecheck + lint + format + test + cycle check


---

5. Tips for DB Manipulation

To fast-forward through lifecycle states without waiting, directly update the kiloclaw_subscriptions table:

sql
-- Expire a trial immediately
UPDATE kiloclaw_subscriptions SET trial_ends_at = NOW() - INTERVAL '1 hour' WHERE user_id = '<id>';

-- Make a subscription past-due for >14 days
UPDATE kiloclaw_subscriptions SET status = 'past_due', updated_at = NOW() - INTERVAL '15 days' WHERE user_id = '<id>';

-- Set destruction deadline to the past
UPDATE kiloclaw_subscriptions SET destruction_deadline = NOW() - INTERVAL '1 hour' WHERE user_id = '<id>';


Then invoke the cron:
bash
curl -H "Authorization: Bearer <CRON_SECRET>" http://localhost:3000/api/cron/kiloclaw-billing-lifecycle


The response JSON includes a summary object with counts for each sweep, making it easy to confirm which actions fired.

@jeanduplessis
Copy link
Contributor Author

Manual Verification Report

51 of 56 scenarios verified. 2 code bugs found and fixed. 0 failures remaining.

A. Welcome Page & Trial

# Scenario Result
A1 Welcome page for new user PASS — 3 cards: Free Trial, Commit ($54/6mo), Standard ($25/mo)
A2 Start free trial PASS — DB: plan=trial, status=trialing, trial_ends_at=now+30d
A3 Trial active banner PASS — Blue info: "Free Trial — 30 days remaining"
A4 Trial not re-startable PASS — API returns "You already have a subscription." (400)
A5 Trial ineligible after provision FIXED & PASS — Provision form now checks trialEligible

B. Stripe Checkout — Standard Plan

# Scenario Result
B1 Checkout redirects to Stripe PASSallow_promotion_codes: true
B2 Successful checkout PASS — Test card 4242, auto-redirected to /claw
B3 Webhook processes PASS — DB: plan=standard, status=active
B4 Dashboard post-subscribe PASS — No banner, SubscriptionCard correct

C. Stripe Checkout — Commit Plan

# Scenario Result
C1 Checkout redirects to Stripe PASSallow_promotion_codes: false, shows "$54 every 6 months"
C2 Successful checkout PASS — Completed with test card
C3 Subscription + schedule created PASSscheduled_plan=standard, commit_ends_at=2026-09-23, schedule created
C4 Dashboard PASS — "Commit ($54/6 months)", commit end date, "(Transitions to Standard $25/mo after)"

D. Delayed Billing (Prelaunch)

# Scenario Result
D1 Billing delayed if before start PASS — "9 days free" shown on checkout
D2 Stripe trialing maps to active PASS — DB status=active despite Stripe trialing
D3 No delay after start date PASS (code) — Guard at kiloclaw-router.ts:1126

E. Plan Switching

# Scenario Result
E1 Standard → Commit PASS (code) — 3-phase schedule: standard → commit (6mo) → standard
E2 Commit → Standard PASS (code) — 2-phase schedule: commit → standard
E3 Cancel plan switch PASS (code) — Releases schedule, clears DB fields
E4 Schedule completes PASS (code) — Webhook updates plan = scheduled_plan

F. Commit Renewal

# Scenario Result
F1 Renew early PASS (code) — Invoice + payment, extends commit_ends_at by 6 months

G. Cancellation & Reactivation

# Scenario Result
G1 Cancel subscription PASScancel_at_period_end=true, amber banner shown
G2 Reactivate PASScancel_at_period_end=false, UI restored
G3 Period ends after cancel PASS (code)subscription.deleted webhook → status=canceled

H. Billing Lifecycle Cron — Trial Expiry

# Scenario Result
H1 Trial 5-day warning PASSclaw_trial_5d email sent
H2 Trial 1-day warning PASSclaw_trial_1d email sent
H3 Trial expiry PASSstatus=canceled, suspended, destruction_deadline=now+7d
H4 Destruction 2-day warning PASSclaw_destruction_warning email
H5 Instance destruction PASSdestroyed_at set, claw_instance_destroyed email
H6 Email idempotency PASS — Re-run: 0 emails sent, no duplicates

I. Billing Lifecycle Cron — Subscription Expiry

# Scenario Result
I1 Subscription period expired PASS — Suspended, claw_suspended_subscription email
I2 Past-due cleanup PASS — Suspended, claw_suspended_payment email

J. Billing Lifecycle Cron — Earlybird

# Scenario Result
J1 Earlybird 14-day warning PASS (code) — Cannot live-test (expiry 197 days away)
J2 Earlybird 1-day warning PASS (code)

K. Auto-Resume on Resubscription

# Scenario Result
K1 Resubscribe after suspension PASS (code) — Starts instance, clears suspension fields, resets email_log
K2 Resubscribe after destruction PASS (code) — Start skipped, user must provision new instance

L. Access Gate

# Scenario Result
L1 Active sub → access PASS
L2 Trialing → access PASS
L3 Earlybird → access PASS (code)
L4 Canceled → blocked PASS — AccessLockedDialog shown
L5 Past-due → access granted PASS — PastDueSubscriptionCard shown

M. UI Banner States

# Banner State Result
M1 trial_active PASS — Blue: "Free Trial — 30 days remaining"
M2 trial_ending_soon PASS — Amber: "5 days left"
M3 trial_ending_very_soon PASS — Red: "2 days left"
M4 trial_expires_today FIXED — Was unreachable due to Math.ceil; now uses Math.floor
M5 earlybird_active PASS (code)
M6 earlybird_ending_soon PASS (code)
M7 subscription_canceling PASS — Amber: "Your plan ends on [date]"
M8 subscription_past_due PASS — Red: "Payment failed — action required"
M9 subscribed PASS — No banner

N. AccessLockedDialog Variants

# Lock Reason Result
N1 trial_expired_instance_alive PASS (code) — "Subscribe to Resume"
N2 trial_expired_instance_destroyed PASS — "Subscribe to start fresh with a new one"
N3 earlybird_expired PASS (code) — "Earlybird Hosting Expired"
N4 subscription_expired_instance_alive PASS — "Subscribe to resume"
N5 subscription_expired_instance_destroyed PASS — "Subscribe to provision a new one"
N6 past_due_grace_exceeded PASS — "Update Payment Method"

Bugs Fixed

# Issue Fix
1 Provision form showed "Start Free Trial" when trialEligible=false CreateInstanceCard.tsx: replaced isNewUser heuristic with Boolean(billingStatus?.trialEligible)
2 trial_expires_today banner unreachable kiloclaw-router.ts: Math.ceilMath.max(0, Math.floor(...)) for daysRemaining

Both fixes pass pnpm typecheck.

Coverage

Live tested Code verified Fixed Total
Count 38 16 2 56

… webhook handlers

- Stripe integration for commit ($9/mo for 6 months) and standard ($25/mo) plans
- Trial flow with 30-day free period and automatic suspension
- Subscription lifecycle: create, cancel, reactivate, switch plans, renew commit
- Billing lifecycle cron for trial expiry, payment suspension, and instance destruction
- Webhook handlers for subscription.created/updated/deleted and invoice events
- Promo code support via Stripe promotion codes
- Billing UI: welcome page, subscription card, plan selection, cancel dialog
- Access gating with BillingWrapper, lock dialog, and billing banner
- GDPR soft-delete support for billing data
- Idempotent webhook guards against stale/replayed events
- Concurrent checkout session guard
- Hardened error handling: cancellation rollback, reactivation rollback
- Edge-case test coverage for webhook idempotency, concurrent checkouts,
  stale subscription guards, cancellation ordering, and reactivation rollback
…ock dialog

Expired earlybird users who never provisioned an instance were routed to
WelcomePage instead of AccessLockedDialog. The welcome guard now also
checks !billing.earlybird and !billing.trial?.expired so only truly new
users see the onboarding flow.
…ionAccess

Move earlybird lookup before the trial creation path so that:
- Active earlybird users get access without an accidental trial row
- Expired earlybird users cannot regain access by provisioning
- New non-earlybird users still get auto-trial (unchanged)
…Eligible for returning users

- Stale-webhook guard in handleKiloClawSubscriptionCreated now permits
  subscription.created when the existing row is canceled, so re-subscribing
  after cancellation correctly upserts the new Stripe subscription.
- trialEligible now checks for both instance and subscription rows, preventing
  a dead-end free-trial CTA for users with a canceled subscription.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant