fix: send founder email on first paid subscription#247
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThe change adds a subscription confirmation email workflow triggered during Stripe sync when transitioning from non-paid to paid. It computes nextPlan from price IDs, tracks wasPaid, derives isNowPaid, and gates sending via shouldSendSubscriptionConfirmation. When sending, it fetches team users and calls sendSubscriptionConfirmationEmail for valid addresses, using a founder email override via sendMail. TeamService.updateTeam now uses nextPlan when the subscription is not canceled. A new exported function sendSubscriptionConfirmationEmail(email: string) is introduced. An extra closing brace was inserted in TeamService, affecting structure but not method signatures. Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/src/server/mailer.ts (1)
135-157: Bug: cloud email path ignores USESEND_API_KEY and can throw during Stripe syncThe UNSEND-only check blocks sending when only USESEND_API_KEY is set, causing sendSubscriptionConfirmationEmail to throw and potentially fail sync. Fix the guard and error message.
Apply this diff:
- } else if (env.UNSEND_API_KEY && (env.FROM_EMAIL || fromOverride)) { + } else if ( + (env.USESEND_API_KEY || env.UNSEND_API_KEY) && + (env.FROM_EMAIL || fromOverride) + ) { const fromAddress = fromOverride ?? env.FROM_EMAIL!; @@ - } else { - throw new Error("USESEND_API_KEY/UNSEND_API_KEY not found"); + } else { + throw new Error( + "Email sending not configured: set USESEND_API_KEY or UNSEND_API_KEY and FROM_EMAIL (or pass fromOverride)." + ); }
🧹 Nitpick comments (4)
apps/web/src/server/mailer.ts (1)
72-83: Consistency: add dev guard and set reply-to for founder emailsOther mailers skip sending in development; mirror that and set reply-to to the founder email.
export async function sendSubscriptionConfirmationEmail(email: string) { if (!env.FOUNDER_EMAIL) { logger.error("FOUNDER_EMAIL not configured"); return; } + if (env.NODE_ENV === "development") { + logger.info({ email }, "Skipping founder subscription email in development"); + return; + } const subject = "Thanks for subscribing to useSend"; const text = `Hey,\n\nThanks for subscribing to useSend, just wanted to let you know you can join the discord server to have a dedicated support channel for your team. So that we can address your queries / bugs asap.\n\nYou can join over using the link: https://discord.com/invite/BU8n8pJv8S\n\nIf you prefer slack, please let me know\n\ncheers,\nkoushik - useSend`; const html = text.replace(/\n/g, "<br />"); - await sendMail(email, subject, text, html, undefined, env.FOUNDER_EMAIL); + await sendMail(email, subject, text, html, env.FOUNDER_EMAIL, env.FOUNDER_EMAIL); }apps/web/src/server/billing/payments.ts (3)
4-4: Use "~/" alias per apps/web guidelineReplace relative import with the alias to conform to project standards.
-import { sendSubscriptionConfirmationEmail } from "../mailer"; +import { sendSubscriptionConfirmationEmail } from "~/server/mailer";
191-194: Plan update rule change is acceptableSetting plan to nextPlan unless status is "canceled" keeps plan in sync; isActive is tied to "active". This supports your gating. Consider trialing behavior separately if you want emails on activation post-trial only.
196-204: Optional: limit recipients to adminsIf the intent is to notify decision-makers only, filter to ADMINs.
- const emails = teamUsers - .map((tu) => tu.user?.email) + const emails = teamUsers + .filter((tu) => tu.role === "ADMIN") + .map((tu) => tu.user?.email) .filter((email): email is string => Boolean(email));
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/web/src/server/billing/payments.ts(4 hunks)apps/web/src/server/mailer.ts(1 hunks)apps/web/src/server/service/team-service.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/general.mdc)
Include all required imports, and ensure proper naming of key components.
Files:
apps/web/src/server/mailer.tsapps/web/src/server/billing/payments.tsapps/web/src/server/service/team-service.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Use TypeScript with 2-space indentation and semicolons (enforced by Prettier)
ESLint must pass with zero warnings using @usesend/eslint-config
Do not use dynamic imports (avoid import() and dynamic loading)
Files:
apps/web/src/server/mailer.tsapps/web/src/server/billing/payments.tsapps/web/src/server/service/team-service.ts
**/*.{ts,tsx,md}
📄 CodeRabbit inference engine (AGENTS.md)
Format code and docs with Prettier 3
Files:
apps/web/src/server/mailer.tsapps/web/src/server/billing/payments.tsapps/web/src/server/service/team-service.ts
apps/web/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
apps/web/**/*.{ts,tsx}: In apps/web, use the "/" alias for src imports (e.g., import { x } from "/utils/x")
Prefer using tRPC for API calls unless explicitly instructed otherwise
Files:
apps/web/src/server/mailer.tsapps/web/src/server/billing/payments.tsapps/web/src/server/service/team-service.ts
🧬 Code graph analysis (2)
apps/web/src/server/mailer.ts (2)
apps/web/src/env.js (2)
env(5-134)env(5-134)apps/web/src/server/logger/log.ts (1)
logger(31-63)
apps/web/src/server/billing/payments.ts (2)
apps/web/src/server/service/team-service.ts (1)
TeamService(18-567)apps/web/src/server/mailer.ts (1)
sendSubscriptionConfirmationEmail(72-83)
🔇 Additional comments (4)
apps/web/src/server/mailer.ts (1)
116-125: From override may be ignored in self-hostedIn self-hosted, fromOverride is used only if its domain is in availableDomains; otherwise it falls back to hello@. Confirm this is intended for the founder email case; if not, consider allowing fromOverride regardless.
apps/web/src/server/service/team-service.ts (1)
566-569: Confirm helper scope changegetLimitReachedEmail is now a top-level function (outside the class). Behavior is fine (hoisted), but verify this wasn’t meant to remain a private static within TeamService.
apps/web/src/server/billing/payments.ts (2)
127-128: Gating looks correct; double-check prior state assumptionswasPaid uses team.isActive && team.plan !== "FREE". This matches the intent. Verify team.plan is always one of "FREE" | paid values to avoid false positives.
150-153: Correct first-paid detectionnextPlan/isNowPaid/shouldSendSubscriptionConfirmation logic is sound for active, non-FREE transitions.
| if (shouldSendSubscriptionConfirmation) { | ||
| const teamUsers = await TeamService.getTeamUsers(team.id); | ||
| await Promise.all( | ||
| teamUsers | ||
| .map((tu) => tu.user?.email) | ||
| .filter((email): email is string => Boolean(email)) | ||
| .map((email) => sendSubscriptionConfirmationEmail(email)) | ||
| ); | ||
| } |
There was a problem hiding this comment.
Harden: don’t fail Stripe sync if an email send rejects
A single send rejection will reject Promise.all and fail sync. Catch per-recipient and log.
- if (shouldSendSubscriptionConfirmation) {
- const teamUsers = await TeamService.getTeamUsers(team.id);
- await Promise.all(
- teamUsers
- .map((tu) => tu.user?.email)
- .filter((email): email is string => Boolean(email))
- .map((email) => sendSubscriptionConfirmationEmail(email))
- );
- }
+ if (shouldSendSubscriptionConfirmation) {
+ const teamUsers = await TeamService.getTeamUsers(team.id);
+ const emails = teamUsers
+ .map((tu) => tu.user?.email)
+ .filter((email): email is string => Boolean(email));
+ await Promise.all(
+ emails.map((email) =>
+ sendSubscriptionConfirmationEmail(email).catch((err) => {
+ // import logger from "~/server/logger/log"
+ logger.error({ err, email, teamId: team.id }, "Founder email send failed");
+ })
+ )
+ );
+ }Add this import near the top (outside the changed hunk):
import { logger } from "~/server/logger/log";🤖 Prompt for AI Agents
In apps/web/src/server/billing/payments.ts around lines 196 to 204, the current
Promise.all will reject the whole Stripe sync if any single
sendSubscriptionConfirmationEmail fails; change to handle each recipient send
with its own try/catch (or wrap each promise with .catch) so a single rejection
won't fail the overall sync, and log per-recipient failures using the logger;
also add the suggested import at the top: import { logger } from
"~/server/logger/log".
Summary
Testing
https://chatgpt.com/codex/tasks/task_e_68cdc1959db4832992629ea247d5aab2
Summary by CodeRabbit
New Features
Improvements
Chores