- Fix reminder duplicate bug: use slot-only keys to prevent multiple reminders when settings change - Add New Jackpot announcement delay setting for groups (default 5 min) - Cancel unpaid purchases after draw completes (prevents payments for past rounds) - Add BotFather commands template file - Update README documentation
688 lines
22 KiB
TypeScript
688 lines
22 KiB
TypeScript
import TelegramBot from 'node-telegram-bot-api';
|
|
import { groupStateManager } from './groupState';
|
|
import { stateManager } from './state';
|
|
import { apiClient } from './api';
|
|
import { logger } from './logger';
|
|
import { messages } from '../messages';
|
|
import { GroupSettings, reminderTimeToMinutes, formatReminderTime, ReminderTime, DEFAULT_GROUP_REMINDER_SLOTS } from '../types/groups';
|
|
import { TelegramUser } from '../types';
|
|
|
|
interface CycleInfo {
|
|
id: string;
|
|
scheduled_at: string;
|
|
status: string;
|
|
pot_total_sats: number;
|
|
}
|
|
|
|
interface ScheduledReminder {
|
|
groupId?: number;
|
|
telegramId?: number;
|
|
cycleId: string;
|
|
reminderKey: string;
|
|
scheduledFor: Date;
|
|
timeout: NodeJS.Timeout;
|
|
}
|
|
|
|
class NotificationScheduler {
|
|
private bot: TelegramBot | null = null;
|
|
private pollInterval: NodeJS.Timeout | null = null;
|
|
private scheduledReminders: Map<string, ScheduledReminder> = new Map();
|
|
private lastCycleId: string | null = null;
|
|
private lastCycleStatus: string | null = null;
|
|
private isRunning = false;
|
|
private announcedCycles: Set<string> = new Set(); // Track announced cycles
|
|
|
|
/**
|
|
* Initialize the scheduler with the bot instance
|
|
*/
|
|
init(bot: TelegramBot): void {
|
|
this.bot = bot;
|
|
logger.info('Notification scheduler initialized');
|
|
}
|
|
|
|
/**
|
|
* Start the scheduler
|
|
*/
|
|
start(): void {
|
|
if (this.isRunning || !this.bot) {
|
|
return;
|
|
}
|
|
|
|
this.isRunning = true;
|
|
logger.info('Starting notification scheduler');
|
|
|
|
// Poll every 30 seconds
|
|
this.pollInterval = setInterval(() => this.poll(), 30 * 1000);
|
|
|
|
// Run immediately
|
|
this.poll();
|
|
}
|
|
|
|
/**
|
|
* Stop the scheduler
|
|
*/
|
|
stop(): void {
|
|
if (this.pollInterval) {
|
|
clearInterval(this.pollInterval);
|
|
this.pollInterval = null;
|
|
}
|
|
|
|
// Clear all scheduled reminders
|
|
for (const reminder of this.scheduledReminders.values()) {
|
|
clearTimeout(reminder.timeout);
|
|
}
|
|
this.scheduledReminders.clear();
|
|
|
|
this.isRunning = false;
|
|
logger.info('Notification scheduler stopped');
|
|
}
|
|
|
|
/**
|
|
* Main poll loop
|
|
*/
|
|
private async poll(): Promise<void> {
|
|
try {
|
|
const jackpot = await apiClient.getNextJackpot();
|
|
if (!jackpot?.cycle) {
|
|
return;
|
|
}
|
|
|
|
const cycle = jackpot.cycle;
|
|
const lottery = jackpot.lottery;
|
|
|
|
// Check for draw completion (same cycle, status changed to completed)
|
|
if (this.lastCycleId === cycle.id &&
|
|
this.lastCycleStatus !== 'completed' &&
|
|
cycle.status === 'completed') {
|
|
await this.handleDrawCompleted(cycle);
|
|
}
|
|
|
|
// Check for new cycle (new jackpot started)
|
|
// IMPORTANT: When we detect a new cycle, the old one is completed
|
|
// Send draw completion for the old cycle BEFORE new cycle announcement
|
|
if (this.lastCycleId && this.lastCycleId !== cycle.id) {
|
|
// The previous cycle has completed - announce the draw result first
|
|
await this.handlePreviousCycleCompleted(this.lastCycleId);
|
|
|
|
// Then announce the new cycle
|
|
await this.handleNewCycle(cycle, lottery);
|
|
}
|
|
|
|
// Schedule reminders for current cycle
|
|
await this.scheduleGroupReminders(cycle);
|
|
await this.scheduleUserReminders(cycle);
|
|
|
|
this.lastCycleId = cycle.id;
|
|
this.lastCycleStatus = cycle.status;
|
|
} catch (error) {
|
|
logger.error('Error in notification scheduler poll', { error });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle previous cycle completion when we detect a new cycle
|
|
*/
|
|
private async handlePreviousCycleCompleted(previousCycleId: string): Promise<void> {
|
|
if (!this.bot) return;
|
|
|
|
// Check if we've already announced this draw
|
|
if (this.announcedCycles.has(`draw:${previousCycleId}`)) {
|
|
return;
|
|
}
|
|
this.announcedCycles.add(`draw:${previousCycleId}`);
|
|
|
|
// Clear reminders for the old cycle
|
|
this.clearRemindersForCycle(previousCycleId);
|
|
|
|
// Get participants for the previous cycle
|
|
const participants = await stateManager.getCycleParticipants(previousCycleId);
|
|
const hasParticipants = participants.length > 0;
|
|
|
|
logger.info('Processing previous cycle completion', {
|
|
cycleId: previousCycleId,
|
|
participantCount: participants.length
|
|
});
|
|
|
|
// Get draw result details for winner announcement
|
|
let winnerDisplayName = 'Anon';
|
|
let winnerTicketNumber = '0000';
|
|
let potSats = 0;
|
|
|
|
// Notify each participant about their result
|
|
for (const participant of participants) {
|
|
try {
|
|
const user = await stateManager.getUser(participant.telegramId);
|
|
if (!user) continue;
|
|
|
|
// Check if they won
|
|
const status = await apiClient.getTicketStatus(participant.purchaseId);
|
|
if (!status) continue;
|
|
|
|
potSats = status.cycle.pot_total_sats || 0;
|
|
const isWinner = status.result.is_winner;
|
|
|
|
if (isWinner) {
|
|
// Store winner info for group announcement
|
|
winnerDisplayName = user.displayName || 'Anon';
|
|
const winningTicket = status.tickets.find(t => t.is_winning_ticket);
|
|
if (winningTicket) {
|
|
winnerTicketNumber = winningTicket.serial_number.toString().padStart(4, '0');
|
|
}
|
|
|
|
// Send winner notification if user has drawResults enabled
|
|
if (user.notifications?.drawResults !== false) {
|
|
const prizeSats = status.result.payout?.amount_sats || potSats;
|
|
const payoutStatus = status.result.payout?.status || 'processing';
|
|
await this.bot.sendMessage(
|
|
participant.telegramId,
|
|
messages.notifications.winner(
|
|
prizeSats.toLocaleString(),
|
|
winnerTicketNumber,
|
|
payoutStatus
|
|
),
|
|
{ parse_mode: 'Markdown' }
|
|
);
|
|
logger.info('Sent winner notification', { telegramId: participant.telegramId });
|
|
}
|
|
} else {
|
|
// Send loser notification if user has drawResults enabled
|
|
if (user.notifications?.drawResults !== false) {
|
|
await this.bot.sendMessage(
|
|
participant.telegramId,
|
|
messages.notifications.loser(
|
|
winnerTicketNumber,
|
|
potSats.toLocaleString()
|
|
),
|
|
{ parse_mode: 'Markdown' }
|
|
);
|
|
logger.debug('Sent draw result to participant', { telegramId: participant.telegramId });
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logger.error('Failed to notify participant', {
|
|
telegramId: participant.telegramId,
|
|
error
|
|
});
|
|
}
|
|
}
|
|
|
|
// Send group announcements (even if no participants - groups might want to know)
|
|
if (hasParticipants) {
|
|
await this.sendGroupDrawAnnouncementsImmediate(
|
|
previousCycleId,
|
|
winnerDisplayName,
|
|
winnerTicketNumber,
|
|
potSats,
|
|
participants.length
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send draw announcements to groups immediately (no delay - for cycle transition)
|
|
*/
|
|
private async sendGroupDrawAnnouncementsImmediate(
|
|
cycleId: string,
|
|
winnerDisplayName: string,
|
|
winnerTicketNumber: string,
|
|
potSats: number,
|
|
totalParticipants: number
|
|
): Promise<void> {
|
|
if (!this.bot) return;
|
|
|
|
const groups = await groupStateManager.getGroupsWithFeature('drawAnnouncements');
|
|
|
|
for (const group of groups) {
|
|
try {
|
|
const message = messages.notifications.drawAnnouncement(
|
|
winnerDisplayName,
|
|
`#${winnerTicketNumber}`,
|
|
potSats.toLocaleString(),
|
|
totalParticipants
|
|
);
|
|
|
|
await this.bot.sendMessage(group.groupId, message, { parse_mode: 'Markdown' });
|
|
logger.debug('Sent draw announcement to group', { groupId: group.groupId });
|
|
} catch (error) {
|
|
this.handleSendError(error, group.groupId);
|
|
}
|
|
}
|
|
|
|
logger.info('Draw announcements sent', {
|
|
cycleId,
|
|
groupCount: groups.length
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle new cycle announcement
|
|
*/
|
|
private async handleNewCycle(
|
|
cycle: CycleInfo,
|
|
lottery: { name: string; ticket_price_sats: number }
|
|
): Promise<void> {
|
|
if (!this.bot) return;
|
|
|
|
// Check if we've already announced this cycle
|
|
if (this.announcedCycles.has(`new:${cycle.id}`)) {
|
|
return;
|
|
}
|
|
this.announcedCycles.add(`new:${cycle.id}`);
|
|
|
|
const drawTime = new Date(cycle.scheduled_at);
|
|
const bot = this.bot;
|
|
|
|
// Send to groups (with configurable delay)
|
|
const groups = await groupStateManager.getGroupsWithFeature('enabled');
|
|
for (const group of groups) {
|
|
if (group.newJackpotAnnouncement === false) continue;
|
|
|
|
const delayMs = (group.newJackpotDelayMinutes ?? 5) * 60 * 1000;
|
|
|
|
setTimeout(async () => {
|
|
try {
|
|
const message = messages.notifications.newJackpot(
|
|
lottery.name,
|
|
lottery.ticket_price_sats,
|
|
drawTime
|
|
);
|
|
await bot.sendMessage(group.groupId, message, { parse_mode: 'Markdown' });
|
|
logger.debug('Sent new jackpot announcement to group', {
|
|
groupId: group.groupId,
|
|
delayMinutes: group.newJackpotDelayMinutes ?? 5
|
|
});
|
|
} catch (error) {
|
|
this.handleSendError(error, group.groupId);
|
|
}
|
|
}, delayMs);
|
|
}
|
|
|
|
// Send to users with new jackpot alerts enabled (immediate)
|
|
const users = await stateManager.getUsersWithNotification('newJackpotAlerts');
|
|
for (const user of users) {
|
|
try {
|
|
const message = messages.notifications.newJackpot(
|
|
lottery.name,
|
|
lottery.ticket_price_sats,
|
|
drawTime
|
|
);
|
|
await this.bot.sendMessage(user.telegramId, message, { parse_mode: 'Markdown' });
|
|
logger.debug('Sent new jackpot alert to user', { telegramId: user.telegramId });
|
|
} catch (error) {
|
|
logger.error('Failed to send new jackpot alert', { telegramId: user.telegramId, error });
|
|
}
|
|
}
|
|
|
|
logger.info('New jackpot announcements scheduled/sent', { cycleId: cycle.id });
|
|
}
|
|
|
|
/**
|
|
* Handle draw completed - send notifications to participants and groups
|
|
*/
|
|
private async handleDrawCompleted(cycle: CycleInfo): Promise<void> {
|
|
if (!this.bot) return;
|
|
|
|
// Check if we've already announced this draw
|
|
if (this.announcedCycles.has(`draw:${cycle.id}`)) {
|
|
return;
|
|
}
|
|
this.announcedCycles.add(`draw:${cycle.id}`);
|
|
|
|
// Clear reminders for this cycle
|
|
this.clearRemindersForCycle(cycle.id);
|
|
|
|
// Get participants for this cycle
|
|
const participants = await stateManager.getCycleParticipants(cycle.id);
|
|
const hasParticipants = participants.length > 0;
|
|
|
|
logger.info('Processing draw completion', {
|
|
cycleId: cycle.id,
|
|
participantCount: participants.length,
|
|
potSats: cycle.pot_total_sats
|
|
});
|
|
|
|
// Only proceed if there were participants
|
|
if (!hasParticipants) {
|
|
logger.info('No participants in cycle, skipping notifications', { cycleId: cycle.id });
|
|
return;
|
|
}
|
|
|
|
// Get draw result details for winner announcement
|
|
let winnerDisplayName = 'Anon';
|
|
let winnerTicketNumber = '0000';
|
|
|
|
// Notify each participant about their result
|
|
for (const participant of participants) {
|
|
try {
|
|
const user = await stateManager.getUser(participant.telegramId);
|
|
if (!user || !user.notifications?.drawResults) continue;
|
|
|
|
// Check if they won
|
|
const status = await apiClient.getTicketStatus(participant.purchaseId);
|
|
if (!status) continue;
|
|
|
|
const isWinner = status.result.is_winner;
|
|
|
|
if (isWinner) {
|
|
// Store winner info for group announcement
|
|
winnerDisplayName = user.displayName || 'Anon';
|
|
const winningTicket = status.tickets.find(t => t.is_winning_ticket);
|
|
if (winningTicket) {
|
|
winnerTicketNumber = winningTicket.serial_number.toString().padStart(4, '0');
|
|
}
|
|
|
|
// Send winner notification
|
|
const prizeSats = status.result.payout?.amount_sats || cycle.pot_total_sats;
|
|
const payoutStatus = status.result.payout?.status || 'processing';
|
|
await this.bot.sendMessage(
|
|
participant.telegramId,
|
|
messages.notifications.winner(
|
|
prizeSats.toLocaleString(),
|
|
winnerTicketNumber,
|
|
payoutStatus
|
|
),
|
|
{ parse_mode: 'Markdown' }
|
|
);
|
|
logger.info('Sent winner notification', { telegramId: participant.telegramId });
|
|
} else {
|
|
// Send loser notification
|
|
await this.bot.sendMessage(
|
|
participant.telegramId,
|
|
messages.notifications.loser(
|
|
winnerTicketNumber,
|
|
cycle.pot_total_sats.toLocaleString()
|
|
),
|
|
{ parse_mode: 'Markdown' }
|
|
);
|
|
logger.debug('Sent draw result to participant', { telegramId: participant.telegramId });
|
|
}
|
|
} catch (error) {
|
|
logger.error('Failed to notify participant', {
|
|
telegramId: participant.telegramId,
|
|
error
|
|
});
|
|
}
|
|
}
|
|
|
|
// Send group announcements (only if there were participants)
|
|
await this.sendGroupDrawAnnouncements(cycle, winnerDisplayName, winnerTicketNumber, participants.length);
|
|
}
|
|
|
|
/**
|
|
* Send draw announcements to groups
|
|
*/
|
|
private async sendGroupDrawAnnouncements(
|
|
cycle: CycleInfo,
|
|
winnerDisplayName: string,
|
|
winnerTicketNumber: string,
|
|
totalParticipants: number
|
|
): Promise<void> {
|
|
if (!this.bot) return;
|
|
|
|
const groups = await groupStateManager.getGroupsWithFeature('drawAnnouncements');
|
|
|
|
for (const group of groups) {
|
|
const delay = (group.announcementDelaySeconds || 0) * 1000;
|
|
|
|
setTimeout(async () => {
|
|
try {
|
|
const message = messages.notifications.drawAnnouncement(
|
|
winnerDisplayName,
|
|
`#${winnerTicketNumber}`,
|
|
cycle.pot_total_sats.toLocaleString(),
|
|
totalParticipants
|
|
);
|
|
|
|
if (this.bot) {
|
|
await this.bot.sendMessage(group.groupId, message, { parse_mode: 'Markdown' });
|
|
logger.debug('Sent draw announcement to group', { groupId: group.groupId });
|
|
}
|
|
} catch (error) {
|
|
this.handleSendError(error, group.groupId);
|
|
}
|
|
}, delay);
|
|
}
|
|
|
|
logger.info('Draw announcements scheduled', {
|
|
cycleId: cycle.id,
|
|
groupCount: groups.length
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Schedule reminders for groups (3-tier system with custom times)
|
|
*/
|
|
private async scheduleGroupReminders(cycle: CycleInfo): Promise<void> {
|
|
if (!this.bot || cycle.status === 'completed' || cycle.status === 'cancelled') {
|
|
return;
|
|
}
|
|
|
|
const drawTime = new Date(cycle.scheduled_at);
|
|
const now = new Date();
|
|
|
|
const groups = await groupStateManager.getGroupsWithFeature('reminders');
|
|
|
|
for (const group of groups) {
|
|
// Build list of enabled reminders from 3-tier system with custom times
|
|
const enabledReminders: { slot: number; time: ReminderTime }[] = [];
|
|
|
|
// Check each of the 3 reminder slots with their custom times
|
|
if (group.reminder1Enabled !== false) {
|
|
const time = group.reminder1Time || DEFAULT_GROUP_REMINDER_SLOTS[0];
|
|
enabledReminders.push({ slot: 1, time });
|
|
}
|
|
if (group.reminder2Enabled === true) {
|
|
const time = group.reminder2Time || DEFAULT_GROUP_REMINDER_SLOTS[1];
|
|
enabledReminders.push({ slot: 2, time });
|
|
}
|
|
if (group.reminder3Enabled === true) {
|
|
const time = group.reminder3Time || DEFAULT_GROUP_REMINDER_SLOTS[2];
|
|
enabledReminders.push({ slot: 3, time });
|
|
}
|
|
|
|
if (enabledReminders.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
for (const { slot, time: reminderTime } of enabledReminders) {
|
|
// Use slot-only key to prevent duplicate reminders when settings change
|
|
const uniqueKey = `group:${group.groupId}:${cycle.id}:slot${slot}`;
|
|
|
|
// Check if we already have a reminder for this slot
|
|
const existingReminder = this.scheduledReminders.get(uniqueKey);
|
|
if (existingReminder) {
|
|
// If settings changed (different time), cancel old and reschedule
|
|
const newMinutesBefore = reminderTimeToMinutes(reminderTime);
|
|
const newReminderDate = new Date(drawTime.getTime() - newMinutesBefore * 60 * 1000);
|
|
|
|
// Only reschedule if the time actually changed
|
|
if (existingReminder.scheduledFor.getTime() !== newReminderDate.getTime()) {
|
|
clearTimeout(existingReminder.timeout);
|
|
this.scheduledReminders.delete(uniqueKey);
|
|
logger.debug('Cleared old reminder due to settings change', {
|
|
groupId: group.groupId,
|
|
slot,
|
|
oldTime: existingReminder.scheduledFor.toISOString(),
|
|
newTime: newReminderDate.toISOString(),
|
|
});
|
|
} else {
|
|
continue; // Same time, skip
|
|
}
|
|
}
|
|
|
|
const minutesBefore = reminderTimeToMinutes(reminderTime);
|
|
const reminderDate = new Date(drawTime.getTime() - minutesBefore * 60 * 1000);
|
|
|
|
if (reminderDate <= now) {
|
|
continue;
|
|
}
|
|
|
|
const delay = reminderDate.getTime() - now.getTime();
|
|
|
|
const timeout = setTimeout(async () => {
|
|
await this.sendGroupReminder(group, cycle, reminderTime, drawTime);
|
|
this.scheduledReminders.delete(uniqueKey);
|
|
}, delay);
|
|
|
|
this.scheduledReminders.set(uniqueKey, {
|
|
groupId: group.groupId,
|
|
cycleId: cycle.id,
|
|
reminderKey: `slot${slot}_${formatReminderTime(reminderTime)}`,
|
|
scheduledFor: reminderDate,
|
|
timeout,
|
|
});
|
|
|
|
logger.debug('Scheduled group reminder', {
|
|
groupId: group.groupId,
|
|
cycleId: cycle.id,
|
|
slot,
|
|
time: formatReminderTime(reminderTime),
|
|
scheduledFor: reminderDate.toISOString(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Schedule reminders for individual users with drawReminders enabled
|
|
*/
|
|
private async scheduleUserReminders(cycle: CycleInfo): Promise<void> {
|
|
if (!this.bot || cycle.status === 'completed' || cycle.status === 'cancelled') {
|
|
return;
|
|
}
|
|
|
|
const drawTime = new Date(cycle.scheduled_at);
|
|
const now = new Date();
|
|
|
|
// Get users with draw reminders enabled
|
|
const users = await stateManager.getUsersWithNotification('drawReminders');
|
|
|
|
// Default reminder: 15 minutes before
|
|
const defaultReminder: ReminderTime = { value: 15, unit: 'minutes' };
|
|
const reminderKey = formatReminderTime(defaultReminder);
|
|
|
|
for (const user of users) {
|
|
const uniqueKey = `user:${user.telegramId}:${cycle.id}:${reminderKey}`;
|
|
|
|
if (this.scheduledReminders.has(uniqueKey)) {
|
|
continue;
|
|
}
|
|
|
|
const minutesBefore = reminderTimeToMinutes(defaultReminder);
|
|
const reminderDate = new Date(drawTime.getTime() - minutesBefore * 60 * 1000);
|
|
|
|
if (reminderDate <= now) {
|
|
continue;
|
|
}
|
|
|
|
const delay = reminderDate.getTime() - now.getTime();
|
|
|
|
const timeout = setTimeout(async () => {
|
|
await this.sendUserReminder(user, cycle, defaultReminder, drawTime);
|
|
this.scheduledReminders.delete(uniqueKey);
|
|
}, delay);
|
|
|
|
this.scheduledReminders.set(uniqueKey, {
|
|
telegramId: user.telegramId,
|
|
cycleId: cycle.id,
|
|
reminderKey,
|
|
scheduledFor: reminderDate,
|
|
timeout,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send a reminder to a group
|
|
*/
|
|
private async sendGroupReminder(
|
|
group: GroupSettings,
|
|
cycle: CycleInfo,
|
|
reminderTime: ReminderTime,
|
|
drawTime: Date
|
|
): Promise<void> {
|
|
if (!this.bot) return;
|
|
|
|
try {
|
|
const message = messages.notifications.drawReminder(
|
|
reminderTime.value,
|
|
reminderTime.unit,
|
|
drawTime,
|
|
cycle.pot_total_sats
|
|
);
|
|
|
|
await this.bot.sendMessage(group.groupId, message, { parse_mode: 'Markdown' });
|
|
logger.info('Sent draw reminder to group', {
|
|
groupId: group.groupId,
|
|
reminderKey: formatReminderTime(reminderTime)
|
|
});
|
|
} catch (error) {
|
|
this.handleSendError(error, group.groupId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send a reminder to a user
|
|
*/
|
|
private async sendUserReminder(
|
|
user: TelegramUser,
|
|
cycle: CycleInfo,
|
|
reminderTime: ReminderTime,
|
|
drawTime: Date
|
|
): Promise<void> {
|
|
if (!this.bot) return;
|
|
|
|
try {
|
|
const message = messages.notifications.drawReminder(
|
|
reminderTime.value,
|
|
reminderTime.unit,
|
|
drawTime,
|
|
cycle.pot_total_sats
|
|
);
|
|
|
|
await this.bot.sendMessage(user.telegramId, message, { parse_mode: 'Markdown' });
|
|
logger.info('Sent draw reminder to user', { telegramId: user.telegramId });
|
|
} catch (error) {
|
|
logger.error('Failed to send reminder to user', { telegramId: user.telegramId, error });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle send errors (remove group if bot was kicked)
|
|
*/
|
|
private handleSendError(error: any, groupId: number): void {
|
|
logger.error('Failed to send message to group', { groupId, error });
|
|
if (error?.response?.statusCode === 403) {
|
|
groupStateManager.removeGroup(groupId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear all scheduled reminders for a cycle
|
|
*/
|
|
private clearRemindersForCycle(cycleId: string): void {
|
|
for (const [key, reminder] of this.scheduledReminders.entries()) {
|
|
if (reminder.cycleId === cycleId) {
|
|
clearTimeout(reminder.timeout);
|
|
this.scheduledReminders.delete(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get status info for debugging
|
|
*/
|
|
getStatus(): object {
|
|
return {
|
|
isRunning: this.isRunning,
|
|
lastCycleId: this.lastCycleId,
|
|
lastCycleStatus: this.lastCycleStatus,
|
|
scheduledReminders: this.scheduledReminders.size,
|
|
announcedCycles: this.announcedCycles.size,
|
|
};
|
|
}
|
|
}
|
|
|
|
export const notificationScheduler = new NotificationScheduler();
|
|
export default notificationScheduler;
|