- sendOnTheClockEmail now fetches season (and currentPickNumber) internally,
removing the blocking postChainSeason pre-fetch from both make-pick.ts and
executeAutoPick; callers are now true fire-and-forget with no IIFE needed
- Parallel Promise.all for team, autodraftSettings, and league queries reduces
sequential DB round-trips from 5 to 3
- Remove team.ownerId type cast; assign to local after the null guard
- Combine the two calculatePickInfo calls for the same pick into one
- Fix popoverOpen/enabled divergence: remove the {enabled && ...} guard on
PopoverContent so popoverOpen is the sole visibility control
- Unify NotificationsSection feedback pattern: both Discord and email sections
now read success/error directly from their respective fetcher data via a
shared feedbackFromFetcher helper; removes the success/error props and the
verbose cast that was used for email
- Add 13 unit tests for sendOnTheClockEmail covering: early-exit paths
(no season, past totalPicks, no owner, notifications disabled, autodraft
enabled, no league), single vs back-to-back subject selection, draft room
link presence, error logging without throwing, and parallel query execution
https://claude.ai/code/session_01LMGxgYvtE3CF8Jf3u2pXgA
112 lines
4.7 KiB
TypeScript
112 lines
4.7 KiB
TypeScript
import { eq, and } from "drizzle-orm";
|
|
import * as schema from "~/database/schema";
|
|
import { sendEmail, wrapInEmailTemplate, emailParagraph, emailButton, escapeHtml } from "~/lib/email.server";
|
|
import { calculatePickInfo } from "~/models/draft-utils";
|
|
import { logger } from "~/lib/logger";
|
|
import type { database } from "~/database/context";
|
|
|
|
type DraftSlot = { teamId: string; draftOrder: number };
|
|
|
|
/**
|
|
* Sends an "on the clock" email to the team owner whose turn it is.
|
|
* Reads the current pick number directly from the DB so callers never need
|
|
* to await a pre-fetch — just fire and forget this after the autodraft chain
|
|
* has settled.
|
|
*/
|
|
export async function sendOnTheClockEmail(params: {
|
|
seasonId: string;
|
|
totalTeams: number;
|
|
draftSlots: DraftSlot[];
|
|
db: ReturnType<typeof database>;
|
|
}): Promise<void> {
|
|
const { seasonId, totalTeams, draftSlots, db } = params;
|
|
|
|
// Season is fetched first: we need currentPickNumber + draftRounds + leagueId
|
|
// all in one round-trip, and the pick number must be read after the autodraft
|
|
// chain has committed its updates to the DB.
|
|
const season = await db.query.seasons.findFirst({
|
|
where: eq(schema.seasons.id, seasonId),
|
|
});
|
|
if (!season) return;
|
|
|
|
const currentPickNumber = season.currentPickNumber ?? 1;
|
|
const totalPicks = totalTeams * season.draftRounds;
|
|
|
|
if (currentPickNumber > totalPicks) return;
|
|
|
|
const { pickInRound, round } = calculatePickInfo(currentPickNumber, totalTeams);
|
|
const currentSlot = draftSlots.find((s) => s.draftOrder === pickInRound);
|
|
if (!currentSlot) return;
|
|
|
|
// Parallel: team, autodraft settings, and league are all independent reads
|
|
const [team, autodraft, league] = await Promise.all([
|
|
db.query.teams.findFirst({ where: eq(schema.teams.id, currentSlot.teamId) }),
|
|
db.query.autodraftSettings.findFirst({
|
|
where: and(
|
|
eq(schema.autodraftSettings.seasonId, seasonId),
|
|
eq(schema.autodraftSettings.teamId, currentSlot.teamId)
|
|
),
|
|
}),
|
|
db.query.leagues.findFirst({ where: eq(schema.leagues.id, season.leagueId) }),
|
|
]);
|
|
|
|
// Safety net: skip if autodraft is enabled (pick will be made automatically)
|
|
if (autodraft?.isEnabled) return;
|
|
if (!team?.ownerId || !league) return;
|
|
|
|
const ownerId = team.ownerId;
|
|
const owner = await db.query.users.findFirst({
|
|
where: eq(schema.users.id, ownerId),
|
|
});
|
|
if (!owner?.email || !owner.draftEmailNotificationsEnabled) return;
|
|
|
|
// Detect consecutive picks: same team picks the very next slot too
|
|
let isConsecutive = false;
|
|
if (currentPickNumber + 1 <= totalPicks) {
|
|
const { pickInRound: nextPickInRound } = calculatePickInfo(currentPickNumber + 1, totalTeams);
|
|
const nextSlot = draftSlots.find((s) => s.draftOrder === nextPickInRound);
|
|
if (nextSlot?.teamId === currentSlot.teamId) {
|
|
isConsecutive = true;
|
|
}
|
|
}
|
|
|
|
const appUrl = process.env.APP_URL ?? "https://brackt.com";
|
|
const draftUrl = `${appUrl}/leagues/${season.leagueId}/draft/${seasonId}`;
|
|
const leagueName = escapeHtml(league.name);
|
|
const teamName = escapeHtml(team.name);
|
|
|
|
let subject: string;
|
|
let preheader: string;
|
|
let bodyHtml: string;
|
|
|
|
if (isConsecutive) {
|
|
const { round: round2 } = calculatePickInfo(currentPickNumber + 1, totalTeams);
|
|
subject = `You have back-to-back picks in ${league.name}`;
|
|
preheader = `${team.name} picks twice in a row — head to the draft room!`;
|
|
bodyHtml = `
|
|
${emailParagraph(`<strong>You're up twice in a row!</strong>`)}
|
|
${emailParagraph(`<strong>${teamName}</strong> has back-to-back picks (Round ${round} and Round ${round2}) in <strong>${leagueName}</strong>. Head to the draft room to make both picks before time runs out.`)}
|
|
${emailButton(draftUrl, "Go to Draft Room")}
|
|
${emailParagraph(`<span style="font-size:13px;color:rgba(255,255,255,0.6);">You're receiving this because you enabled draft email notifications in your account settings.</span>`)}
|
|
`;
|
|
} else {
|
|
subject = `You're on the clock in ${league.name}`;
|
|
preheader = `${team.name} is on the clock — make your pick!`;
|
|
bodyHtml = `
|
|
${emailParagraph(`<strong>You're on the clock!</strong>`)}
|
|
${emailParagraph(`<strong>${teamName}</strong> is now on the clock in Round ${round} of <strong>${leagueName}</strong>. Head to the draft room to make your pick before time runs out.`)}
|
|
${emailButton(draftUrl, "Go to Draft Room")}
|
|
${emailParagraph(`<span style="font-size:13px;color:rgba(255,255,255,0.6);">You're receiving this because you enabled draft email notifications in your account settings.</span>`)}
|
|
`;
|
|
}
|
|
|
|
const { error } = await sendEmail({
|
|
to: owner.email,
|
|
subject,
|
|
html: wrapInEmailTemplate(bodyHtml, preheader),
|
|
});
|
|
|
|
if (error) {
|
|
logger.error(`[DraftEmail] Failed to send on-the-clock email to ${owner.email}:`, error);
|
|
}
|
|
}
|