Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
8.8 KiB
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)
sendEventEmailatserver/src/lib/notifications/sendEventEmail.ts— for trial request emails - (2026-03-09) Existing
buildPhaseItemsprice 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: 7on checkout session = 7-day trial- CC captured at checkout but not charged
- Stripe automatically charges when
trial_endpasses - If charge fails → subscription goes to
past_due subscription.status='trialing'during trial,'active'aftersubscription.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:
- Creates Premium subscription with
trial_period_days: 30 - Sets
tenants.plan = 'premium'
Path B — Pro trial customer (wants Premium from day 1):
- Ends Pro trial:
stripe.subscriptions.update(sub, { trial_end: 'now' })→ triggers immediate charge for first month of Pro - Verifies Pro charge succeeded
- Creates Premium subscription with
trial_period_days: 30 - 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 revertstenants.planto'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):
- Trial: Add
subscription_data: { trial_period_days: 7 }to Stripe session creation insrc/utils/stripe.tsfor tiered products - Annual billing: Add monthly/annual toggle to
OrderForm.tsx, pass annual price IDs when selected - 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)