@@ -25,20 +25,29 @@ import { getConfig } from "../../config/index.js";
2525
2626const logger = loggers . scheduler . child ( "elections" ) ;
2727
28- function getCurrentTimeInTimezone ( timezone : string ) : { hours : number ; minutes : number } {
28+ function getCurrentTimeInTimezone ( timezone : string ) : { hours : number ; minutes : number ; dayOfWeek : number } {
2929 const now = new Date ( ) ;
30- const formatter = new Intl . DateTimeFormat ( "en-US" , {
30+ const timeFormatter = new Intl . DateTimeFormat ( "en-US" , {
3131 timeZone : timezone ,
3232 hour : "2-digit" ,
3333 minute : "2-digit" ,
3434 hour12 : false ,
3535 } ) ;
36+ const dayFormatter = new Intl . DateTimeFormat ( "en-US" , {
37+ timeZone : timezone ,
38+ weekday : "short" ,
39+ } ) ;
3640
37- const parts = formatter . formatToParts ( now ) ;
38- const hours = Number . parseInt ( parts . find ( ( p ) => p . type === "hour" ) ?. value ?? "0" , 10 ) ;
39- const minutes = Number . parseInt ( parts . find ( ( p ) => p . type === "minute" ) ?. value ?? "0" , 10 ) ;
41+ const timeParts = timeFormatter . formatToParts ( now ) ;
42+ const hours = Number . parseInt ( timeParts . find ( ( p ) => p . type === "hour" ) ?. value ?? "0" , 10 ) ;
43+ const minutes = Number . parseInt ( timeParts . find ( ( p ) => p . type === "minute" ) ?. value ?? "0" , 10 ) ;
4044
41- return { hours, minutes } ;
45+ // Get day of week (0=Sunday, 6=Saturday)
46+ const dayStr = dayFormatter . format ( now ) ;
47+ const dayMap : Record < string , number > = { Sun : 0 , Mon : 1 , Tue : 2 , Wed : 3 , Thu : 4 , Fri : 5 , Sat : 6 } ;
48+ const dayOfWeek = dayMap [ dayStr ] ?? 0 ;
49+
50+ return { hours, minutes, dayOfWeek } ;
4251}
4352
4453function isElectionStartTime ( ) : boolean {
@@ -48,26 +57,38 @@ function isElectionStartTime(): boolean {
4857 const [ targetHour , targetMinute ] = config . elections . startTime . split ( ":" ) . map ( Number ) ;
4958 const current = getCurrentTimeInTimezone ( config . elections . timezone ) ;
5059
60+ // Check if it's the configured day of the week (default: Wednesday = 3)
61+ if ( current . dayOfWeek !== config . elections . dayOfWeek ) return false ;
62+
5163 // Allow a 5-minute window for the scheduler
5264 const currentMinutes = current . hours * 60 + current . minutes ;
5365 const targetMinutes = ( targetHour ?? 0 ) * 60 + ( targetMinute ?? 0 ) ;
5466
5567 return currentMinutes >= targetMinutes && currentMinutes < targetMinutes + 5 ;
5668}
5769
58- async function hasElectionToday ( guildId : string ) : Promise < boolean > {
59- const todayStart = new Date ( ) ;
60- todayStart . setHours ( 0 , 0 , 0 , 0 ) ;
61- const todayEnd = new Date ( ) ;
62- todayEnd . setHours ( 23 , 59 , 59 , 999 ) ;
70+ async function hasElectionThisWeek ( guildId : string ) : Promise < boolean > {
71+ const config = getConfig ( ) ;
72+ const current = getCurrentTimeInTimezone ( config . elections . timezone ) ;
73+ const now = new Date ( ) ;
74+
75+ // Calculate start of the current week (Sunday at 00:00:00) using configured timezone's day
76+ const weekStart = new Date ( now ) ;
77+ weekStart . setDate ( now . getDate ( ) - current . dayOfWeek ) ;
78+ weekStart . setHours ( 0 , 0 , 0 , 0 ) ;
79+
80+ // Calculate end of the current week (Saturday at 23:59:59)
81+ const weekEnd = new Date ( weekStart ) ;
82+ weekEnd . setDate ( weekStart . getDate ( ) + 6 ) ;
83+ weekEnd . setHours ( 23 , 59 , 59 , 999 ) ;
6384
6485 const election = await prisma . electionPoll . findFirst ( {
6586 where : {
6687 guildId,
6788 pollType : "election" ,
6889 createdAt : {
69- gte : todayStart ,
70- lte : todayEnd ,
90+ gte : weekStart ,
91+ lte : weekEnd ,
7192 } ,
7293 } ,
7394 } ) ;
@@ -90,9 +111,9 @@ export async function checkAndStartElections(): Promise<void> {
90111
91112 for ( const [ guildId , oauthGuild ] of guilds ) {
92113 try {
93- // Check if election already exists for today
94- if ( await hasElectionToday ( guildId ) ) {
95- logger . debug ( "Election already exists for today " , { guildId } ) ;
114+ // Check if election already exists for this week
115+ if ( await hasElectionThisWeek ( guildId ) ) {
116+ logger . debug ( "Election already exists for this week " , { guildId } ) ;
96117 continue ;
97118 }
98119
@@ -158,7 +179,7 @@ export async function checkAndStartElections(): Promise<void> {
158179 } ) ;
159180 }
160181
161- logger . info ( "Daily election created and started" , {
182+ logger . info ( "Weekly election created and started" , {
162183 guildId,
163184 channelId,
164185 candidates : candidates . length ,
@@ -230,29 +251,27 @@ export async function processElectionResults(): Promise<void> {
230251
231252 const results = determineWinner ( pollResults . data . answers ) ;
232253
233- // Handle tie - create runoff
254+ // Handle tie - announce and use random winner
234255 if ( results . isTie ) {
235- await updateElectionStatus ( election . id , "completed" , {
236- actualEnd : new Date ( ) ,
237- voteCounts : JSON . stringify ( results . voteCounts ) ,
238- } ) ;
239-
240- // Create runoff poll
241- const runoffEnd = new Date ( Date . now ( ) + 2 * 60 * 60 * 1000 ) ;
242- await createElectionPoll ( {
243- guildId : election . guildId ,
244- channelId : election . channelId ,
245- pollType : "runoff" ,
246- scheduledStart : new Date ( ) ,
247- scheduledEnd : runoffEnd ,
248- candidates : results . tiedCandidates ,
249- } ) ;
250-
251- logger . info ( "Runoff created" , {
256+ const client = getDiscordClient ( ) ;
257+ const channel = await client . channels . fetch ( election . channelId ) ;
258+ if ( channel ?. isTextBased ( ) && "send" in channel ) {
259+ const tiedNames = results . tiedCandidates
260+ . map ( ( name ) => name . charAt ( 0 ) . toUpperCase ( ) + name . slice ( 1 ) )
261+ . join ( ", " ) ;
262+ const winnerName = ( results . winner ?? "jerred" ) . charAt ( 0 ) . toUpperCase ( ) + ( results . winner ?? "jerred" ) . slice ( 1 ) ;
263+ const voteCount = results . voteCounts [ results . tiedCandidates [ 0 ] ?? "" ] ?? 0 ;
264+ await channel . send (
265+ `🎲 **It's a tie!** ${ tiedNames } each received ${ String ( voteCount ) } votes. ` +
266+ `A random winner has been selected: **${ winnerName } **! 🎉`
267+ ) ;
268+ }
269+
270+ logger . info ( "Tie resolved with random winner" , {
252271 guildId : election . guildId ,
253272 tiedCandidates : results . tiedCandidates ,
273+ randomWinner : results . winner ,
254274 } ) ;
255- continue ;
256275 }
257276
258277 // Update guild owner
0 commit comments