Skip to content

feat(billing): unify upgrade routing with reason context + storage/tables limit emails#5171

Open
waleedlatif1 wants to merge 6 commits into
stagingfrom
feat/usage-limit-upgrade-system
Open

feat(billing): unify upgrade routing with reason context + storage/tables limit emails#5171
waleedlatif1 wants to merge 6 commits into
stagingfrom
feat/usage-limit-upgrade-system

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • Add a single upgrade-reason registry (lib/billing/upgrade-reasons.ts) — the source of truth for the language shown when a usage limit routes a user to the upgrade page. The same copy drives both the upgrade-page header and the threshold emails so they never drift.
  • Upgrade page reads ?reason= (nuqs) and swaps its header ("Upgrade to scale your tables", "…with your teammates", etc.); generic header when absent.
  • Every in-app limit surface now deep-links through buildUpgradeHref(workspaceId, reason): credits chip (credits), teammates (seats), tables row-limit toast (tables), file-upload storage error (storage, via a shared useLimitUpgradeToast). Generic "Explore plans" links (billing settings, deploy gate) route through the same helper without a reason.
  • New storage + tables threshold emails (80% warning / 100% reached) via one parameterized LimitThresholdEmail. Dedup is a race-free atomic claim (single conditional UPDATE … WHERE current < desired RETURNING against a new limit_notifications jsonb column on user_stats/organization, migration 0248), with hysteresis re-arm below 70%. One shared maybeNotifyLimit resolves user vs. org scope for both call sites.
  • Fix a latent bug: the existing credits threshold emails linked to a dead URL (/workspace?billing=upgrade, which redirects to home and drops the param). Re-pointed to the live upgrade/billing-settings routes.

Type of Change

  • New feature (+ bug fix for the dead credits link)

Notes

  • Seats threshold email is intentionally deferred — Team seats auto-scale (Stripe quantity, no cap) and enterprise seats are fixed but redirected away from the upgrade page, so there's no clean trigger. The seats in-app CTA + header ship now; the email infra (LimitCategory includes seats) is ready if the seat model changes.
  • Limit emails respect the same opt-outs as credits (getEmailPreferences + billingUsageNotificationsEnabled) and are best-effort/fire-and-forget so they never block a mutation.

Testing

  • Unit tests added: upgrade-reasons.test.ts (4) and limit-notifications.test.ts (9 — claim win/lose, dead band, re-arm, opt-outs, billing-disabled). Existing logger/tables suites green (60 tests total).
  • bun run check:api-validation, bun run check:react-query, typecheck, and biome all pass.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel

vercel Bot commented Jun 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jun 22, 2026 9:28pm

Request Review

@cursor

cursor Bot commented Jun 22, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches billing notifications, DB state for dedup, and fire-and-forget email on upload/table mutations; logic is well-tested but incorrect thresholds could spam or miss emails.

Overview
Introduces a shared upgrade-reason registry (upgrade-reasons.ts) so limit CTAs, the upgrade page header, and email copy stay aligned. The upgrade page reads ?reason= (credits, storage, tables, seats) and swaps its headline; buildUpgradeHref replaces hard-coded /upgrade links across credits, teammates, tables errors, deploy gate, and billing settings.

In-app limit UX: storage upload failures surface an actionable toast via useLimitUpgradeToast that deep-links to ?reason=storage.

New threshold emails for storage and tables (80% / 100%) use one LimitThresholdEmail template and maybeNotifyLimit, with atomic dedup on new limit_notifications jsonb columns (user_stats / organization, migration 0248), hysteresis re-arm below 70%, and hooks from storage tracking and table row inserts. Credits threshold emails are fixed to use live workspace upgrade/billing URLs instead of the dead /workspace?billing=upgrade link.

Reviewed by Cursor Bugbot for commit 01ee798. Configure here.

Comment thread apps/sim/lib/billing/storage/tracking.ts
@greptile-apps

greptile-apps Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a unified upgrade-routing system with a central upgrade-reasons registry, and adds storage and table row usage-limit emails (80% warning / 100% reached). The atomic dedup mechanism using conditional UPDATE … WHERE current < desired RETURNING is race-free, and the hysteresis re-arm approach correctly prevents re-notification until usage drops below 70%.

  • Upgrade-reason registry (lib/billing/upgrade-reasons.ts): single source of truth for copy on both the upgrade page and limit threshold emails; buildUpgradeHref centralizes all deep-link construction, replacing scattered inline string templates.
  • Storage + tables threshold emails (lib/billing/core/limit-notifications.ts): per-category dedup via a new limit_notifications JSONB column on both user_stats and organization; fire-and-forget so emails never block mutations; per-recipient failures are isolated to avoid one bad address suppressing the rest.
  • Dead-link fix (lib/billing/core/usage.ts): existing credits threshold emails that previously linked to /workspace?billing=upgrade (a dropped param) now use buildUpgradeHref to route to the live upgrade or billing-settings page.

Confidence Score: 5/5

Safe to merge — the dedup claim is race-free, the re-arm hysteresis is correct, all email paths are fire-and-forget, and the migration is a backward-compatible additive column with a sensible default.

The atomic UPDATE … WHERE current < desired RETURNING claim correctly prevents duplicate emails even under concurrent calls. The re-arm path now receives priorUsage for the tables case and uses rearmOnly=true on the storage decrement path. Per-recipient email failures are isolated in individual try/catch blocks. The new limit_notifications JSONB column ships with NOT NULL DEFAULT '{}', so existing rows are handled without a backfill. Dead-link fix for credits emails is verified correct. No regressions found in any call site.

No files require special attention — the core notification logic, migration, email template, and routing changes all look correct.

Important Files Changed

Filename Overview
apps/sim/lib/billing/core/limit-notifications.ts Core notification logic — atomic claim, re-arm, and per-recipient isolation all look correct; matches the pattern from the credits path in usage.ts
apps/sim/lib/billing/upgrade-reasons.ts Clean registry pattern; buildUpgradeHref and isUpgradeReason are well-typed; credits email subject fields in the registry are never used by getLimitEmailSubject (credits uses the old path) but this is documented intent
apps/sim/lib/billing/storage/tracking.ts Correct fire-and-forget storage notification wired after the DB update; priorUsage not passed (by design — post-increment re-reads correct org/user totals via getUserStorageUsage)
apps/sim/lib/table/billing.ts Table row notification correctly passes pre-insert count as priorUsage to enable re-arm on large jumps, addressing the previously flagged structural issue
packages/db/migrations/0248_limit_notifications.sql Additive migration — two NOT NULL DEFAULT '{}'::jsonb columns on organization and user_stats; safe to apply without downtime
apps/sim/lib/billing/core/usage.ts Dead-link fix for credits threshold emails: /workspace?billing=upgrade replaced with live upgrade and billing-settings URLs via buildUpgradeHref
apps/sim/components/emails/billing/limit-threshold-email.tsx Parameterized email template driven by UPGRADE_REASON_COPY; consistent with existing email layout and style patterns
apps/sim/app/workspace/[workspaceId]/upgrade/upgrade.tsx Upgrade page now reads ?reason= via nuqs and swaps the h1 header from UPGRADE_REASON_COPY; Suspense boundary correctly added in page.tsx for the useQueryState call
apps/sim/lib/billing/core/limit-notifications.test.ts 14 tests covering claim win/lose, dead band, re-arm, opt-outs, billing-disabled, zero usage, and wipe-then-rebuild; mock setup correctly isolates DB and email side-effects
apps/sim/lib/billing/client/use-limit-upgrade-toast.ts Simple hook that wraps buildUpgradeHref into an actionable toast; correctly uses useParams within the [workspaceId] route context

Reviews (5): Last reviewed commit: "fix(billing): resolve recipients before ..." | Re-trigger Greptile

Comment thread apps/sim/lib/table/billing.ts
Comment thread apps/sim/lib/billing/core/limit-notifications.ts Outdated
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/billing/core/limit-notifications.ts Outdated
Comment thread apps/sim/lib/billing/core/limit-notifications.ts
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/billing/storage/tracking.ts Outdated
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/billing/core/limit-notifications.ts
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 01ee798. Configure here.

const projected = params.currentRowCount + params.addedRows
if ((projected / limit) * 100 >= TABLE_ROW_NOTIFY_PERCENT) {
void maybeNotifyTableRowLimit(params.workspaceId, params.currentRowCount, projected, limit)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upsert inserts skip table emails

Medium Severity

Table row threshold emails run only from assertRowCapacity, but upsertRow enforces capacity on the insert branch with wouldExceedRowLimit and never calls that helper. Upserts that add rows and cross the 80%/100% bands therefore skip warning and limit-reached emails that inserts and batch inserts receive.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 01ee798. Configure here.

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