Skip to content

fix: send founder email on first paid subscription#247

Merged
KMKoushik merged 2 commits intomainfrom
codex/send-plain-text-email-on-subscription
Sep 20, 2025
Merged

fix: send founder email on first paid subscription#247
KMKoushik merged 2 commits intomainfrom
codex/send-plain-text-email-on-subscription

Conversation

@KMKoushik
Copy link
Copy Markdown
Member

@KMKoushik KMKoushik commented Sep 20, 2025

Summary

  • send the founder subscription confirmation email when a team transitions from free to an active paid plan during Stripe sync
  • introduce a mailer helper that emits the plain-text founder message via the existing sendMail override

Testing

  • pnpm lint (fails: existing lint warnings in packages/ui)
  • pnpm --filter web lint (fails: existing lint warnings across apps/web)

https://chatgpt.com/codex/tasks/task_e_68cdc1959db4832992629ea247d5aab2

Summary by CodeRabbit

  • New Features

    • Automatically sends a subscription confirmation email when a workspace upgrades to a paid plan.
    • Sends confirmations to relevant team members with valid email addresses.
  • Improvements

    • Smarter detection of free-to-paid transitions ensures confirmation emails are only sent when appropriate, reducing noise.
  • Chores

    • Introduced internal email workflow to support subscription confirmations.

@vercel
Copy link
Copy Markdown

vercel bot commented Sep 20, 2025

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

Project Deployment Preview Comments Updated (UTC)
unsend-marketing Ready Ready Preview Comment Sep 20, 2025 8:19pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Sep 20, 2025

Walkthrough

The 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)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly and accurately describes the PR's primary change—sending the founder subscription confirmation when a team transitions from free to an active paid plan—and aligns with the changes in payments.ts and the new mailer helper, making it clear and actionable for reviewers.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/send-plain-text-email-on-subscription

Comment @coderabbitai help to get the list of available commands and usage tips.

@KMKoushik KMKoushik merged commit 5780177 into main Sep 20, 2025
2 of 3 checks passed
@KMKoushik KMKoushik deleted the codex/send-plain-text-email-on-subscription branch September 20, 2025 20:18
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 sync

The 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 emails

Other 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 guideline

Replace 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 acceptable

Setting 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 admins

If 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1226e89 and b945692.

📒 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.ts
  • apps/web/src/server/billing/payments.ts
  • apps/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.ts
  • apps/web/src/server/billing/payments.ts
  • apps/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.ts
  • apps/web/src/server/billing/payments.ts
  • apps/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.ts
  • apps/web/src/server/billing/payments.ts
  • apps/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-hosted

In 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 change

getLimitReachedEmail 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 assumptions

wasPaid 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 detection

nextPlan/isNowPaid/shouldSendSubscriptionConfirmation logic is sound for active, non-FREE transitions.

Comment on lines +196 to +204
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))
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant