add contact to users on waitlist removal (#276)

This commit is contained in:
KM Koushik
2025-10-19 15:08:33 +11:00
committed by GitHub
parent 78db758512
commit 77b0239b92
2 changed files with 73 additions and 19 deletions
+4 -2
View File
@@ -13,7 +13,7 @@ export const env = createEnv({
.url() .url()
.refine( .refine(
(str) => !str.includes("YOUR_MYSQL_URL_HERE"), (str) => !str.includes("YOUR_MYSQL_URL_HERE"),
"You forgot to change the default URL" "You forgot to change the default URL",
), ),
NODE_ENV: z NODE_ENV: z
.enum(["development", "test", "production"]) .enum(["development", "test", "production"])
@@ -27,7 +27,7 @@ export const env = createEnv({
// Since NextAuth.js automatically uses the VERCEL_URL if present. // Since NextAuth.js automatically uses the VERCEL_URL if present.
(str) => process.env.VERCEL_URL ?? str, (str) => process.env.VERCEL_URL ?? str,
// VERCEL_URL doesn't include `https` so it cant be validated as a URL // VERCEL_URL doesn't include `https` so it cant be validated as a URL
process.env.VERCEL ? z.string() : z.string().url() process.env.VERCEL ? z.string() : z.string().url(),
), ),
GITHUB_ID: z.string().optional(), GITHUB_ID: z.string().optional(),
GITHUB_SECRET: z.string().optional(), GITHUB_SECRET: z.string().optional(),
@@ -65,6 +65,7 @@ export const env = createEnv({
STRIPE_WEBHOOK_SECRET: z.string().optional(), STRIPE_WEBHOOK_SECRET: z.string().optional(),
SMTP_HOST: z.string().default("smtp.usesend.com"), SMTP_HOST: z.string().default("smtp.usesend.com"),
SMTP_USER: z.string().default("usesend"), SMTP_USER: z.string().default("usesend"),
CONTACT_BOOK_ID: z.string().optional(),
}, },
/** /**
@@ -120,6 +121,7 @@ export const env = createEnv({
STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET, STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
SMTP_HOST: process.env.SMTP_HOST, SMTP_HOST: process.env.SMTP_HOST,
SMTP_USER: process.env.SMTP_USER, SMTP_USER: process.env.SMTP_USER,
CONTACT_BOOK_ID: process.env.CONTACT_BOOK_ID,
}, },
/** /**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
+69 -17
View File
@@ -8,6 +8,8 @@ import { getAccount } from "~/server/aws/ses";
import { db } from "~/server/db"; import { db } from "~/server/db";
import { sendMail } from "~/server/mailer"; import { sendMail } from "~/server/mailer";
import { logger } from "~/server/logger/log"; import { logger } from "~/server/logger/log";
import { UseSend } from "usesend-js";
import { isCloud } from "~/utils/common";
const waitlistUserSelection = { const waitlistUserSelection = {
id: true, id: true,
@@ -79,7 +81,7 @@ export const adminRouter = createTRPCRouter({
.input( .input(
z.object({ z.object({
region: z.string(), region: z.string(),
}) }),
) )
.query(async ({ input }) => { .query(async ({ input }) => {
const acc = await getAccount(input.region); const acc = await getAccount(input.region);
@@ -93,7 +95,7 @@ export const adminRouter = createTRPCRouter({
usesendUrl: z.string().url(), usesendUrl: z.string().url(),
sendRate: z.number(), sendRate: z.number(),
transactionalQuota: z.number(), transactionalQuota: z.number(),
}) }),
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
return SesSettingsService.createSesSetting({ return SesSettingsService.createSesSetting({
@@ -110,7 +112,7 @@ export const adminRouter = createTRPCRouter({
settingsId: z.string(), settingsId: z.string(),
sendRate: z.number(), sendRate: z.number(),
transactionalQuota: z.number(), transactionalQuota: z.number(),
}) }),
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
return SesSettingsService.updateSesSetting({ return SesSettingsService.updateSesSetting({
@@ -124,11 +126,11 @@ export const adminRouter = createTRPCRouter({
.input( .input(
z.object({ z.object({
region: z.string().optional().nullable(), region: z.string().optional().nullable(),
}) }),
) )
.query(async ({ input }) => { .query(async ({ input }) => {
return SesSettingsService.getSetting( return SesSettingsService.getSetting(
input.region ?? env.AWS_DEFAULT_REGION input.region ?? env.AWS_DEFAULT_REGION,
); );
}), }),
@@ -139,7 +141,7 @@ export const adminRouter = createTRPCRouter({
.string() .string()
.email() .email()
.transform((value) => value.toLowerCase()), .transform((value) => value.toLowerCase()),
}) }),
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const user = await db.user.findUnique({ const user = await db.user.findUnique({
@@ -155,7 +157,7 @@ export const adminRouter = createTRPCRouter({
z.object({ z.object({
userId: z.number(), userId: z.number(),
isWaitlisted: z.boolean(), isWaitlisted: z.boolean(),
}) }),
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const existingUser = await db.user.findUnique({ const existingUser = await db.user.findUnique({
@@ -182,6 +184,56 @@ export const adminRouter = createTRPCRouter({
Boolean(updatedUser.email) && Boolean(updatedUser.email) &&
(founderEmail || fallbackFrom); (founderEmail || fallbackFrom);
// Add user to contact book when removed from waitlist (cloud only)
if (
existingUser.isWaitlisted &&
!input.isWaitlisted &&
isCloud() &&
env.CONTACT_BOOK_ID &&
updatedUser.email
) {
try {
const client = new UseSend(env.USESEND_API_KEY);
// Split name into first and last name if available
const firstName = updatedUser.name || "";
const result = await client.contacts.create(env.CONTACT_BOOK_ID, {
email: updatedUser.email,
firstName: firstName,
});
if (result.error) {
logger.error(
{
userId: updatedUser.id,
email: updatedUser.email,
error: result.error,
},
"Failed to add user to contact book",
);
} else {
logger.info(
{
userId: updatedUser.id,
email: updatedUser.email,
contactId: result.data?.contactId,
},
"Successfully added user to contact book",
);
}
} catch (error) {
logger.error(
{
userId: updatedUser.id,
email: updatedUser.email,
error,
},
"Error adding user to contact book",
);
}
}
if (shouldSendAcceptanceEmail) { if (shouldSendAcceptanceEmail) {
const recipient = updatedUser.email as string; const recipient = updatedUser.email as string;
const replyTo = founderEmail ?? fallbackFrom; const replyTo = founderEmail ?? fallbackFrom;
@@ -201,12 +253,12 @@ export const adminRouter = createTRPCRouter({
text, text,
toPlainHtml(text), toPlainHtml(text),
replyTo, replyTo,
fromOverride fromOverride,
); );
} catch (error) { } catch (error) {
logger.error( logger.error(
{ userId: updatedUser.id, error }, { userId: updatedUser.id, error },
"Failed to send waitlist acceptance email" "Failed to send waitlist acceptance email",
); );
} }
} }
@@ -218,7 +270,7 @@ export const adminRouter = createTRPCRouter({
.input( .input(
z.object({ z.object({
userId: z.number(), userId: z.number(),
}) }),
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const user = await db.user.findUnique({ const user = await db.user.findUnique({
@@ -262,12 +314,12 @@ export const adminRouter = createTRPCRouter({
text, text,
toPlainHtml(text), toPlainHtml(text),
replyTo, replyTo,
fromOverride fromOverride,
); );
} catch (error) { } catch (error) {
logger.error( logger.error(
{ userId: user.id, error }, { userId: user.id, error },
"Failed to send waitlist rejection email" "Failed to send waitlist rejection email",
); );
throw new Error("Failed to send waitlist rejection email"); throw new Error("Failed to send waitlist rejection email");
} }
@@ -282,7 +334,7 @@ export const adminRouter = createTRPCRouter({
.string({ required_error: "Search query is required" }) .string({ required_error: "Search query is required" })
.trim() .trim()
.min(1, "Search query is required"), .min(1, "Search query is required"),
}) }),
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const query = input.query.trim(); const query = input.query.trim();
@@ -338,7 +390,7 @@ export const adminRouter = createTRPCRouter({
dailyEmailLimit: z.number().int().min(0).max(10_000_000), dailyEmailLimit: z.number().int().min(0).max(10_000_000),
isBlocked: z.boolean(), isBlocked: z.boolean(),
plan: z.enum(["FREE", "BASIC"]), plan: z.enum(["FREE", "BASIC"]),
}) }),
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const { teamId, ...data } = input; const { teamId, ...data } = input;
@@ -357,7 +409,7 @@ export const adminRouter = createTRPCRouter({
z.object({ z.object({
timeframe: z.enum(["today", "thisMonth"]), timeframe: z.enum(["today", "thisMonth"]),
paidOnly: z.boolean().optional(), paidOnly: z.boolean().optional(),
}) }),
) )
.query(async ({ input }) => { .query(async ({ input }) => {
const timeframe = input.timeframe; const timeframe = input.timeframe;
@@ -366,7 +418,7 @@ export const adminRouter = createTRPCRouter({
const now = new Date(); const now = new Date();
const today = now.toISOString().slice(0, 10); const today = now.toISOString().slice(0, 10);
const monthStartDate = new Date( const monthStartDate = new Date(
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1) Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1),
); );
const monthStart = monthStartDate.toISOString().slice(0, 10); const monthStart = monthStartDate.toISOString().slice(0, 10);
@@ -427,7 +479,7 @@ export const adminRouter = createTRPCRouter({
bounced: 0, bounced: 0,
complained: 0, complained: 0,
hardBounced: 0, hardBounced: 0,
} },
); );
return { return {