Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

377 lines
17 KiB
Markdown

# PRD — Tier Bug Fixes, Trials, Annual Billing & Payment Enforcement
- Slug: `tier-trials-annual`
- Date: `2026-03-09`
- Status: Draft
- Depends on: `2026-03-03-tenant-tier-system`
## Summary
Extends the 2-tier system (Pro/Premium) with: (1) bug fixes and gaps identified in code review, (2) developer documentation for adding new tier-gated features, (3) annual billing at ~17% discount, (4) Stripe-managed trial periods (7-day Pro for new signups, 30-day Premium for paying Pro customers), (5) trial countdown UI in header + account page, (6) payment failure handling with reminders, and (7) a "Request Premium Trial" flow with manual fulfillment via Nine Minds extension.
## Pricing
| | Pro Monthly | Pro Annual | Premium Monthly | Premium Annual |
|---|---|---|---|---|
| Base fee | $89/mo | $890/yr (~$74.17/mo) | $349/mo | $3,490/yr (~$290.83/mo) |
| Per user | $12/user/mo | $120/user/yr (~$10/user/mo) | $25/user/mo | $250/user/yr (~$20.83/user/mo) |
Annual = pay for 10 months, get 12 (~17% discount, "2 months free").
## Problem
The tier system works but has bugs, no trials, no annual billing, no payment failure handling, and no documentation for extending it.
## Goals
1. Fix all identified bugs and gaps in the tier system
2. Document how to add new tier-gated features (developer guide)
3. Add annual billing option with 17% discount
4. Implement 7-day free Pro trial for new accounts (CC required, auto-charge after)
5. Implement 30-day free Premium trial for paying Pro customers
6. Show trial days remaining in header (every page) and account page
7. Handle payment failures with reminders and visual "shame" indicators
8. Allow Pro users to request a Premium trial via in-app message form
9. Enable Nine Minds admins to manually process Premium trial requests
## Non-goals
- Self-service downgrade flow (Premium → Pro)
- Automated Premium trial approval (intentionally manual)
- Public pricing page or tier comparison marketing page
- Refund processing (handled via Stripe dashboard)
- Free tier or freemium model
## Users and Primary Flows
### Personas
1. **New signup** — Visits website, picks Pro plan, enters CC, starts 7-day trial
2. **Paying Pro customer** — Wants to try Premium features, requests 30-day trial
3. **Trialing user (Pro)** — Sees countdown banner, decides to continue or cancel before charge
4. **Trialing user (Premium)** — Sees countdown with pricing breakdown, auto-charged at trial end unless they cancel back to Pro
5. **Delinquent customer** — Card declined, sees payment failure banner on every page
6. **Nine Minds admin** — Receives Premium trial requests, processes them via extension
### Flow 1: New Pro Trial (7-day)
1. User signs up on website → Stripe Checkout with `trial_period_days: 7`
2. CC captured but not charged
3. User lands in app → header shows "Trial: 7 days left"
4. Day 7 → Stripe auto-charges first month (Pro monthly or annual, depending on selection)
5. 24-48 hour grace period: user can cancel for full refund (manual via support or Stripe portal)
6. If charge fails → payment failure flow (see Flow 5)
### Flow 2a: Premium Trial for Paying Pro Customer (30-day)
1. Paying Pro customer (NOT on active trial) clicks "Upgrade to Premium" on account page
2. Instead of immediate upgrade, sees option to "Request 30-day Premium Trial"
3. Fills in message form (textarea) + "Send Request" button
4. Email notification sent to Nine Minds team
5. Admin activates 30-day Premium trial via Nine Minds extension
6. Customer gets Premium features immediately, header shows "Premium Trial: 30 days left"
### Flow 2b: Premium Trial for Pro-Trialing Customer (edge case)
1. Customer on 7-day Pro trial wants Premium immediately
2. They request via same form (or contacts support)
3. Admin processes via Nine Minds extension — single action that:
- Ends Pro trial immediately
- Charges first month of Pro (customer is now a paying Pro customer)
- Converts to Premium subscription with 30-day trial
4. Customer gets Premium features, header shows "Premium Trial: 30 days left"
### Flow 3: Premium Trial Active & Ending
1. Admin activates trial → Stripe creates Premium subscription with `trial_period_days: 30`
2. Customer sees Premium features immediately, header shows "Premium Trial: 30 days left"
3. Account page shows pricing breakdown: "You'll be charged $X/mo when your trial ends on {date}"
4. Customer can cancel Premium trial early → reverts to Pro (cancel button on account page)
5. If customer does nothing → Stripe auto-charges for Premium at trial end, customer becomes paying Premium
6. If charge fails at trial end → payment failure flow (banner of shame)
### Flow 4: Annual Billing
1. During checkout (new signup) or from account page (existing customer)
2. Toggle between monthly/annual pricing
3. Annual shows "2 months free" / "Save 17%"
4. Stripe handles annual recurring billing
### Flow 5: Payment Failure ("Banner of Shame")
1. Stripe charge fails (card declined, expired, etc.)
2. Stripe subscription enters `past_due` status
3. App detects `past_due` from subscription data
4. Header banner: "Payment failed — update your payment method" (persistent, not dismissible)
5. Stripe retries per its retry schedule (Smart Retries)
6. Nine Minds team manually monitors delinquent accounts and contacts them
7. No automated feature lockout — banner of shame is the enforcement for now
## UX / UI Notes
### Trial Banner (Header)
- Position: left side of header, next to tenant UUID badge
- Style: consistent with existing UI (not a disruptive alert bar)
- Content: "{Plan} Trial: {N} days left" with link to account page
- Color: neutral/info for >3 days, warning for ≤3 days
- Always visible during trial, not dismissible
### Payment Failure Banner (Header)
- Position: same area as trial banner (replaces trial banner if both apply)
- Style: destructive/error variant
- Content: "Payment failed — Update payment method" with link to billing portal
- Always visible, not dismissible
### Account Page — Trial Section
- Shows trial status: plan name, days remaining, start/end dates
- Progress bar showing trial progress
- CTA varies by state:
- Pro trial: "You'll be charged $X on {date}" + cancel option
- Premium trial: "You'll be charged $X/mo for Premium on {date}" + "Cancel & revert to Pro" option
- Payment failed: "Update payment method" link to billing portal
### Premium Trial Request Form
- Location: Account Management page, in the Plan & Tier section
- Shown when: user is on Pro (paid or trialing) and clicks "Upgrade to Premium"
- UI: Simple textarea ("Tell us what you'd like to try with Premium") + "Send Request" button
- Confirmation: toast "Request sent! We'll be in touch."
- No dropdown, no complex form — just a message
### Nine Minds Extension — Premium Trial Activation
- Existing Status column enhanced to show: current plan (Pro/Premium), trial status if trialing, subscription status (active/past_due/etc.)
- **"Start Premium Trial"** action button per tenant. Behavior depends on tenant state:
- **Paying Pro**: creates Premium subscription with 30-day trial directly
- **Pro trial**: ends Pro trial → charges first month of Pro → creates Premium subscription with 30-day trial (all in one action)
- Button disabled/hidden for tenants already on Premium or Premium trial
- Premium trial request inbox: list of pending requests with tenant name, email, message, date
- Manual plan overrides (e.g. courtesy access) done directly in DB — no API exposed
## Requirements
### Bug Fixes
#### BF1: Fix stale JSDoc in tenantTiers.ts
- Replace references to `'basic'` with `'pro'` in docstrings for `resolveTier()` and `ResolvedTier`
#### BF2: Fix buildPhaseItems using wrong price for scheduled reductions
- `buildPhaseItems()` in StripeService must resolve price IDs based on the tenant's CURRENT tier, not pick first configured price
- Fetch tenant's plan, then use matching tier's price IDs
#### BF3: Add server-side assertTierAccess to invoice template save
- Verify `assertTierAccess(TIER_FEATURES.INVOICE_DESIGNER)` is called in `saveInvoiceTemplate` action
- If missing, add it (scratchpad says it was applied but analysis found gap — verify and fix)
#### BF4: Deduplicate JWT plan logic in nextAuthOptions.ts
- Extract shared plan-fetching logic into a helper function used by both `buildAuthOptions()` and static `options`
#### BF5: Add warning log for unknown Stripe product names
- In `tierFromStripeProduct()`, log a warning when product name doesn't match any known mapping
- Include product name and ID in the log for debugging
#### BF6: Add loading skeleton to TierGate
- Replace `return null` while loading with a skeleton placeholder
- Prevents empty flash during slow session loads
#### BF7: Add FK on stripe_base_price_id
- New migration adding foreign key from `stripe_subscriptions.stripe_base_price_id` to `stripe_prices.stripe_price_id`
#### BF8: Mark add-ons scaffolding as intentional
- Add clear JSDoc to `addOns.ts` explaining it's intentional scaffolding for future use
- Add `// Scaffolding: not yet integrated into access checks` comment
### Documentation
#### DOC1: Tier Gating Developer Guide
- Create `docs/tier-gating-guide.md`
- Step-by-step: how to add a new feature to the tier gate
1. Add feature to `TIER_FEATURES` enum
2. Add to `TIER_FEATURE_MAP` for appropriate tiers
3. Add to `FEATURE_MINIMUM_TIER` reverse map
4. Gate UI with `TierGate` or `ServerTierGate`
5. Gate server actions with `assertTierAccess()`
6. Add display name in AccountManagement `FEATURE_DISPLAY_NAMES`
7. Test: unit test for feature mapping, integration test for gating
- Include code examples from existing INVOICE_DESIGNER implementation
- Document CE bypass behavior
### Annual Billing
#### AB1: Create annual Stripe prices
- Env vars: `STRIPE_PRO_BASE_ANNUAL_PRICE_ID`, `STRIPE_PRO_USER_ANNUAL_PRICE_ID`, `STRIPE_PREMIUM_BASE_ANNUAL_PRICE_ID`, `STRIPE_PREMIUM_USER_ANNUAL_PRICE_ID`
- Stripe prices created with `recurring.interval: 'year'` and 10-month pricing
#### AB2: Add billing_interval to subscription tracking
- Migration: add `billing_interval` column to `stripe_subscriptions` (`'month'` | `'year'`, default `'month'`)
- Update subscription import/create to track interval
#### AB3: Checkout supports annual option
- Modify checkout session creation to accept `interval` parameter
- Pass annual price IDs when `interval === 'year'`
#### AB4: Account page billing interval toggle
- Show monthly/annual toggle with savings callout
- Switching interval creates new subscription schedule or updates at period end
#### AB5: Upgrade flow supports annual billing
- `upgradeTier()` accepts `interval` parameter
- Uses annual price IDs when `interval === 'year'`
### Trial System
#### TR1: Add trial fields to JWT/Session
- Add `trial_end`, `subscription_status` to JWT token and Session.user
- Refresh alongside plan (5-minute throttle)
#### TR2: Update TierContext with trial state
- Add to TierContextValue: `isTrialing`, `trialDaysLeft`, `trialEndDate`, `subscriptionStatus`
- Compute from session fields
- Add `isPaymentFailed` derived from `subscriptionStatus === 'past_due'`
#### TR3: Pro trial on new signup (7-day)
- Modify checkout session creation: add `subscription_data.trial_period_days: 7`
- CC captured via Checkout, not charged until trial ends
- Stripe auto-charges on trial end
#### TR4: Trial banner in header
- New `TrialBanner` component in Header
- Position: left side, next to tenant badge
- Shows: "{Plan} Trial: {N} days left"
- Links to `/msp/account`
- Warning color when ≤3 days remaining
#### TR5: Payment failure banner in header
- New `PaymentFailedBanner` component in Header
- Shows when `subscriptionStatus === 'past_due'` or `'unpaid'`
- "Payment failed — Update payment method" linking to Stripe billing portal
- Error/destructive styling, not dismissible
#### TR6: Trial status on account page
- New "Trial Status" card in Account Management
- Shows: plan, days remaining, progress bar, start/end dates
- CTA varies by state (continue, upgrade, update payment)
#### TR7: Handle trial end → auto-charge
- Stripe handles this natively via `trial_end` on subscription
- Webhook `customer.subscription.updated` fires when trial ends and billing starts
- Ensure `handleSubscriptionUpdated` correctly processes trial → active transition
#### TR8: Handle trial end → payment failure
- If charge fails at trial end, subscription goes to `past_due`
- Payment failure banner appears
- Stripe Smart Retries handle retry logic
#### TR9: Premium trial (30-day) — manual activation
- New server action: `startPremiumTrialAction(tenantId)` (admin-only, master billing tenant)
- Detects tenant's current state and handles accordingly:
- **Paying Pro**: creates Premium subscription with `trial_period_days: 30`
- **Pro trial**: (a) ends Pro trial by removing `trial_end` on subscription, (b) Stripe charges first month of Pro immediately, (c) then creates Premium subscription with `trial_period_days: 30`
- Updates `tenants.plan` to `'premium'`
- Customer sees Premium features immediately
#### TR10: Premium trial end — auto-charge or cancel
- **Default (customer does nothing)**: Stripe auto-charges for Premium at trial end → customer becomes paying Premium
- **Customer cancels during trial**: "Cancel Premium Trial" button on account page → reverts subscription, sets `tenants.plan` back to `'pro'`
- Webhook `customer.subscription.updated` handles both transitions:
- `trialing``active`: auto-charge succeeded, customer is now paying Premium
- `trialing``canceled`: customer cancelled during trial, revert to Pro
- `trialing``past_due`: charge failed, show payment failure banner
#### TR11: Premium trial request form
- Textarea in Account Management (Plan & Tier section)
- Shown when: `isPro && !isTrialing` and user clicks "Upgrade to Premium"
- Server action: `sendPremiumTrialRequestAction(message)`
- Sends email to Nine Minds support with tenant info + message
#### TR12: Premium trial request — NineMinds extension
- Add "Trial Requests" section to TenantManagementView
- List pending requests with: tenant name, email, message, date
- "Start Premium Trial" action button per request
- Action calls API: `POST /api/v1/tenant-management/start-premium-trial`
- API endpoint: validates admin, calls `startPremiumTrialAction(tenantId)`
### Payment Enforcement
#### PE1: Track subscription_status in session
- Already covered by TR1 (subscription_status in JWT)
#### PE2: Payment failure detection
- `handleSubscriptionUpdated` webhook sets local `status` to match Stripe
- Status propagates to JWT on next refresh (≤5 min)
#### PE3: Visual payment reminder
- Already covered by TR5 (payment failure banner)
#### PE4: Grace period documentation
- Document 24-48 hour cancellation window after first charge post-trial
- This is handled via Stripe billing portal / support — no custom code needed
## Data / API / Integrations
### New Database Columns
**stripe_subscriptions:**
- `billing_interval` (text, default 'month') — 'month' | 'year'
**No new tables needed.** Trial state comes from Stripe subscription fields (`trial_end`, `status`).
### New JWT/Session Fields
- `trial_end: number | null` — Unix timestamp of trial end
- `subscription_status: string | null` — 'active' | 'trialing' | 'past_due' | 'unpaid' | 'canceled'
### New API Endpoints
- `POST /api/v1/tenant-management/start-premium-trial` — Admin-only, starts 30-day Premium trial
- `POST /api/v1/tenant-management/trial-requests` — List pending trial requests
- Server action: `sendPremiumTrialRequestAction(message)` — Sends email notification
### New Env Vars
- `STRIPE_PRO_BASE_ANNUAL_PRICE_ID`
- `STRIPE_PRO_USER_ANNUAL_PRICE_ID`
- `STRIPE_PREMIUM_BASE_ANNUAL_PRICE_ID`
- `STRIPE_PREMIUM_USER_ANNUAL_PRICE_ID`
- `TRIAL_REQUEST_EMAIL` — Email address for Premium trial requests (default: support@nineminds.com)
## Security / Permissions
- `startPremiumTrialAction` restricted to master billing tenant only
- Trial state read from Stripe (authoritative) via webhooks, not client-editable
- Payment failure banner cannot be dismissed (prevents ignoring payment issues)
## Rollout
### Phase 1: Bug Fixes & Documentation
- All BF* and DOC* items — safe to deploy, no behavior change
### Phase 2: Annual Billing
- AB* items — new Stripe prices must be created in Stripe Dashboard first
- Feature-flagged: `annual-billing` flag controls visibility of annual toggle
### Phase 3: Trial System
- TR* and PE* items — requires Stripe price configuration
- Feature-flagged: `trial-system` flag controls trial creation on checkout
- Premium trial is manual-only, no flag needed
## Resolved Questions
1. **What happens when subscription goes `unpaid`?** → Banner of shame + manual monitoring/outreach by Nine Minds. No automated lockout for now.
2. **Annual mid-year cancellation refund?** → Case-by-case manual process via support.
3. **24-48hr grace period after first charge?** → Support policy only. Handled via Stripe portal/support, no custom code.
## Acceptance Criteria
1. All 8 bugs fixed and verified
2. Developer guide published and covers full tier-gating workflow
3. Annual billing toggle works on checkout and account page
4. New signups get 7-day Pro trial with CC capture
5. Trial countdown visible in header on every page
6. Payment failure banner visible when subscription is past_due
7. Pro customers can request Premium trial via in-app form
8. Nine Minds admins can activate Premium trials from extension
9. Premium trial auto-charges at end unless customer cancels back to Pro
10. Customer can cancel Premium trial and revert to Pro from account page