2026-05-20 21:33:20 +00:00
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 } ;
Address code review feedback on email notification feature
- 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
2026-05-20 22:23:30 +00:00
/ * *
* 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 .
* /
2026-05-20 21:33:20 +00:00
export async function sendOnTheClockEmail ( params : {
seasonId : string ;
totalTeams : number ;
draftSlots : DraftSlot [ ] ;
db : ReturnType < typeof database > ;
} ) : Promise < void > {
Address code review feedback on email notification feature
- 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
2026-05-20 22:23:30 +00:00
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 ;
2026-05-20 21:33:20 +00:00
if ( currentPickNumber > totalPicks ) return ;
Address code review feedback on email notification feature
- 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
2026-05-20 22:23:30 +00:00
const { pickInRound , round } = calculatePickInfo ( currentPickNumber , totalTeams ) ;
2026-05-20 21:33:20 +00:00
const currentSlot = draftSlots . find ( ( s ) = > s . draftOrder === pickInRound ) ;
if ( ! currentSlot ) return ;
Address code review feedback on email notification feature
- 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
2026-05-20 22:23:30 +00:00
// 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 ) } ) ,
] ) ;
2026-05-20 21:33:20 +00:00
Address code review feedback on email notification feature
- 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
2026-05-20 22:23:30 +00:00
// 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 ;
2026-05-20 21:33:20 +00:00
const owner = await db . query . users . findFirst ( {
Address code review feedback on email notification feature
- 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
2026-05-20 22:23:30 +00:00
where : eq ( schema . users . id , ownerId ) ,
2026-05-20 21:33:20 +00:00
} ) ;
if ( ! owner ? . email || ! owner . draftEmailNotificationsEnabled ) return ;
Address code review feedback on email notification feature
- 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
2026-05-20 22:23:30 +00:00
// Detect consecutive picks: same team picks the very next slot too
2026-05-20 21:33:20 +00:00
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 ) ;
}
}