Commit graph

5 commits

Author SHA1 Message Date
Paulo Castellano
cbf8fb283c feat: per-workspace pricing, onboarding, and billing overhaul
Pricing
- Bill per workspace ($12/mo or $120/yr each); Stripe quantity tracks the
  workspace count and syncs on workspace create/delete.
- 2,500 AI credits per workspace, pooled at the account level; monthly reset
  on the billing anniversary, annual granted upfront (no rollover).
- One social account per network per workspace; remove all count-based limits
  (workspace/social/member) and the legacy plan tiers (single Workspace plan).

Onboarding (cloud only: SELF_HOSTED=false + PostHog)
- Replace the /subscribe plan picker with /onboarding persona selection
  (Creator/Freelancer/Startup/Agency/Small business/Other), saved on the user
  (users.persona) and mirrored to PostHog, then Stripe Checkout on the monthly
  price. 8-day trial so Stripe displays 7.

Billing screen
- Remove the Change Plan dialog (dead with a single plan); add an annual-upgrade
  banner for monthly subscribers (swapToYearly).
- Current-plan card shows the workspace count instead of the plan name.

System AI
- Brand analyzer / workspace autofill is always allowed and never debits credits
  (system feature, not the user's usage).

Self-hosted (SELF_HOSTED=true) bypasses all billing, credit, limit, network,
and onboarding logic.
2026-06-21 20:40:03 -03:00
Paulo Castellano
b709c19862 fix: PostHog property keys, deletion idempotency and full enabled gate
Three fixes from a fresh code review:

1. SyncUser identify used 'email' / 'name' instead of the PostHog
   special person properties '\$email' / '\$name'. The frontend already
   used the correct keys; the backend identify (sole source for users
   who sign up but never log in) would have populated only custom
   properties, leaving the built-in person profile email/name blank
   in the PostHog UI.

2. handleSubscriptionDeleted now short-circuits when plan_id is already
   null. Stripe re-delivers webhooks on transient failures, and the
   prior version would dispatch a duplicate 'subscription.cancelled'
   event and re-flush the (already empty) Pennant cache on each retry.

3. useTracking composable called posthog.capture directly, bypassing
   the new enabled gate. While posthog-js queues calls before init
   (so no events leaked over the network in self-hosted mode), the
   buffer grew unbounded and would fire all queued events in bulk if
   init was ever called. Replaced with a gated captureEvent helper
   exported from posthog.ts.

Plus: drop the now-trivial 'updating non-plan fields does not flush
the pennant cache' test (no observer to test against), refresh stale
doc comments referencing the removed SyncUserToPostHog filename, and
add a Bus::assertNotDispatched check to the deletion-idempotency test.
2026-05-07 13:15:36 -03:00
Paulo Castellano
f72a97f676 refactor: explicit Pennant cache reset on plan_id change
Replaces the implicit Account::booted() observer with an explicit
Account::forgetPlanFeatureCache() method called from each plan_id
mutation site (StripeEventListener x3, BillingController x2). Self-hosted
installs naturally never reach any of these callsites — Stripe webhooks
do not fire and the billing controllers redirect to /calendar before any
plan mutation happens — so the Pennant flush is now guaranteed to be a
cloud-only operation.

Adds integration coverage proving the full chain webhook -> plan_id
update -> Pennant flush -> next Feature::value resolves against the new
plan limit.
2026-05-07 10:59:48 -03:00
Paulo Castellano
afc47e9532 feat: transition AI usage from feature-based limits to a centralized monthly credit system with token tracking. 2026-05-03 19:36:26 -03:00
Paulo Castellano
0f385e1b59 refactor: reorganize billing language keys and implement AccountPolicy for subscription management 2026-05-03 17:26:55 -03:00