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

129 lines
8.8 KiB
Markdown

# Scratchpad — Tier Trials, Annual Billing & Bug Fixes
- Plan slug: `tier-trials-annual`
- Created: `2026-03-09`
- Updated: `2026-03-09`
## What This Is
Rolling notes for implementing trials, annual billing, bug fixes, and documentation on top of the existing tier system.
## Decisions
- (2026-03-09) **Pricing confirmed**: Pro $89/mo base + $12/user/mo, Premium $349/mo base + $25/user/mo
- (2026-03-09) **Annual = pay for 10, get 12** (~17% discount, marketed as "2 months free")
- (2026-03-09) **Pro trial = 7 days**, CC upfront, Stripe auto-charges after
- (2026-03-09) **Premium trial = 30 days**, only for paying Pro customers (not during active Pro trial)
- (2026-03-09) **Premium trial is manual**: customer requests via form, Nine Minds processes via extension
- (2026-03-09) **Premium trial = auto-charge at end**: Stripe charges for Premium when trial ends. Customer must actively cancel to revert to Pro. No "commit" step needed — accepting the trial IS the commitment.
- (2026-03-09) **Cancel Premium Trial**: customer can cancel during trial → reverts to Pro. Cancel button on account page.
- (2026-03-09) **Payment failure = "banner of shame"**: persistent banner in header, not dismissible. No automated lockout — Nine Minds manually monitors and contacts delinquent accounts.
- (2026-03-09) **24-48hr grace period**: support policy only, not enforced in code
- (2026-03-09) **Annual cancellation refund**: case-by-case manual process via support, no automated refunds
- (2026-03-09) **Trial state from Stripe**: use subscription.trial_end and status, no custom trial tables
- (2026-03-09) **Trial request storage**: email notification only (no DB table for requests initially)
## Discoveries / Constraints
- (2026-03-09) Stripe subscription status already includes 'trialing' in schema enum
- (2026-03-09) No existing trial_end or trial columns in DB — need to pull from Stripe subscription
- (2026-03-09) JWT already refreshes plan every 5 min — trial_end and subscription_status can piggyback on same query
- (2026-03-09) Header component at `server/src/components/layout/Header.tsx` — trial banner goes next to tenant badge (left side)
- (2026-03-09) CancellationFeedbackModal at `ee/server/src/components/settings/account/CancellationFeedbackModal.tsx` — pattern for Premium trial request form
- (2026-03-09) NineMinds extension TenantManagementView at `ee/extensions/nineminds-reporting/src/iframe/main.tsx` (lines ~2908-3700)
- (2026-03-09) Extension API calls go through WASM proxy: UI → bridge → handler.ts → uiProxy → Next.js API
- (2026-03-09) All tenant management APIs require master billing tenant auth
- (2026-03-09) `sendEventEmail` at `server/src/lib/notifications/sendEventEmail.ts` — for trial request emails
- (2026-03-09) Existing `buildPhaseItems` price bug: picks first configured price ID, not tenant's actual tier
## Key File Paths
| File | Purpose |
|------|---------|
| `packages/types/src/constants/tenantTiers.ts` | Tier types + resolveTier (BF1) |
| `packages/types/src/constants/addOns.ts` | Add-ons scaffolding (BF8) |
| `ee/server/src/lib/stripe/StripeService.ts` | buildPhaseItems (BF2), upgradeTier, checkout |
| `ee/server/src/lib/stripe/stripeTierMapping.ts` | tierFromStripeProduct (BF5) |
| `packages/billing/src/actions/invoiceTemplates.ts` | saveInvoiceTemplate (BF3) |
| `packages/auth/src/lib/nextAuthOptions.ts` | JWT plan logic (BF4), trial fields (TR1-3) |
| `server/src/components/tier-gating/TierGate.tsx` | Loading state (BF6) |
| `server/src/context/TierContext.tsx` | Trial state (TR4-5) |
| `server/src/components/layout/Header.tsx` | Trial banner (TR7), payment banner (TR10) |
| `ee/server/src/components/settings/account/AccountManagement.tsx` | Trial status, request form |
| `ee/server/src/components/settings/account/CancellationFeedbackModal.tsx` | Pattern for request form |
| `ee/server/src/lib/actions/license-actions.ts` | Server actions for trials |
| `ee/extensions/nineminds-reporting/src/iframe/main.tsx` | Extension UI for trial management |
| `ee/extensions/nineminds-reporting/src/handler.ts` | Extension handler routing |
## Stripe Trial Implementation Notes
### How Stripe Trials Work
- `subscription_data.trial_period_days: 7` on checkout session = 7-day trial
- CC captured at checkout but not charged
- Stripe automatically charges when `trial_end` passes
- If charge fails → subscription goes to `past_due`
- `subscription.status` = `'trialing'` during trial, `'active'` after
- `subscription.trial_end` = Unix timestamp of trial end
### Premium Trial Activation (Manual via Extension)
`startPremiumTrialAction(tenantId)` detects tenant state and handles:
**Path A — Paying Pro customer:**
1. Creates Premium subscription with `trial_period_days: 30`
2. Sets `tenants.plan = 'premium'`
**Path B — Pro trial customer (wants Premium from day 1):**
1. Ends Pro trial: `stripe.subscriptions.update(sub, { trial_end: 'now' })` → triggers immediate charge for first month of Pro
2. Verifies Pro charge succeeded
3. Creates Premium subscription with `trial_period_days: 30`
4. Sets `tenants.plan = 'premium'`
Both paths end with: customer sees Premium features + countdown banner + pricing breakdown on account page.
**No direct plan override API** — manual overrides (courtesy access etc.) are done directly in DB.
### Premium Trial End States
- **Customer does nothing** → Stripe auto-charges at trial end → `trialing``active` → customer is paying Premium
- **Customer cancels** → Cancel Premium subscription → `trialing``canceled` → webhook reverts `tenants.plan` to `'pro'`
- **Charge fails** → `trialing``past_due` → banner of shame, Nine Minds contacts manually
## nm-store Dependency
The 7-day Pro trial and annual billing toggle both require changes in **nm-store** (separate repo at `/Users/natalliabukhtsik/Desktop/projects/nm-store`).
Existing plan: `/Users/natalliabukhtsik/Desktop/projects/nm-store/.ai/tier-checkout-plan.md`
nm-store changes needed (out of scope for this plan, but must coordinate):
1. **Trial**: Add `subscription_data: { trial_period_days: 7 }` to Stripe session creation in `src/utils/stripe.ts` for tiered products
2. **Annual billing**: Add monthly/annual toggle to `OrderForm.tsx`, pass annual price IDs when selected
3. **Annual 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`
The alga-psa side (this plan) handles: receiving trial subscriptions via webhook, displaying trial state, managing trials post-creation. nm-store handles: creating the initial checkout session with trial + pricing.
## Implementation Log
### Phase 1: Bug Fixes & Documentation (2026-03-09)
**BF1** — Fixed JSDoc in `tenantTiers.ts`: replaced 'basic' with 'pro' in `ResolvedTier` and `resolveTier()` docstrings.
**BF2** — Fixed `buildPhaseItems` in `StripeService.ts`: now fetches `tenants.plan` and uses `getTierPriceIds()` to resolve the correct tier-specific prices for scheduled reductions, instead of blindly picking first configured price.
**BF3** — Verified: `saveInvoiceTemplate` serves both visual AND code templates. Can't blanket-gate the save action without distinguishing template type. The visual designer is already UI-gated via `canUseVisualDesigner` in `BillingPageClient.tsx`. Server-side enforcement of visual-only saves deferred — would require a `templateType` flag in the save payload.
**BF4** — Extracted `fetchTenantPlan(tenantId)` helper at module level in `nextAuthOptions.ts`. Replaced all 4 duplicated DB query blocks (2 initial sign-in + 2 throttled refresh) with calls to the shared helper.
**BF5** — Added `console.warn` in `tierFromStripeProduct()` when product name is null or doesn't match any known mapping. Includes the product name in the warning for debugging.
**BF6** — Replaced `return null` in `TierGate` loading state with animated skeleton (3 pulse bars using theme border colors).
**BF7** — Created migration `20260309100000_add_fk_stripe_base_price_id.cjs` adding FK from `stripe_subscriptions.stripe_base_price_id` to `stripe_prices.stripe_price_id` with CASCADE delete.
**BF8** — Added detailed JSDoc to `addOns.ts` explaining it's intentional scaffolding and how to wire it in when first add-on is defined.
**DOC1** — Created `docs/tier-gating-guide.md` with 7-step guide: enum → feature map → minimum tier → UI gate → server gate → display name → tests. Includes code examples from existing INVOICE_DESIGNER implementation, CE bypass docs, and key file reference table.
## Open Questions
- What if a paying Pro customer's card fails during Premium trial activation? (Premium trial is free, so this shouldn't matter — they keep Pro subscription active)
- Should we track trial request messages in a DB table for audit, or is email sufficient? (Starting with email only)