diff --git a/telegram_bot/src/services/api.ts b/telegram_bot/src/services/api.ts index 467eb98..e08fcfb 100644 --- a/telegram_bot/src/services/api.ts +++ b/telegram_bot/src/services/api.ts @@ -157,6 +157,32 @@ class ApiClient { return { enabled: false, pending: false, message: null }; } } + + /** + * Get past wins (for group announcements) + */ + async getPastWins(limit: number = 1, offset: number = 0): Promise { + try { + const response = await this.client.get>( + `/jackpot/past-wins?limit=${limit}&offset=${offset}` + ); + return response.data.data.wins || []; + } catch (error) { + logger.error('Failed to get past wins', { error }); + return []; + } + } +} + +export interface PastWin { + cycle_id: string; + cycle_type: string; + scheduled_at: string; + pot_total_sats: number; + pot_after_fee_sats: number | null; + winner_name: string; + winner_address: string | null; + winning_ticket_serial: number | null; } export const apiClient = new ApiClient(); diff --git a/telegram_bot/src/services/notificationScheduler.ts b/telegram_bot/src/services/notificationScheduler.ts index c73d811..00a767b 100644 --- a/telegram_bot/src/services/notificationScheduler.ts +++ b/telegram_bot/src/services/notificationScheduler.ts @@ -122,6 +122,7 @@ class NotificationScheduler { /** * Handle previous cycle completion when we detect a new cycle + * Always sends group announcements, even if no participants tracked locally */ private async handlePreviousCycleCompleted(previousCycleId: string): Promise { if (!this.bot) return; @@ -135,19 +136,41 @@ class NotificationScheduler { // Clear reminders for the old cycle this.clearRemindersForCycle(previousCycleId); - // Get participants for the previous cycle - const participants = await stateManager.getCycleParticipants(previousCycleId); + logger.info('Processing previous cycle completion', { cycleId: previousCycleId }); - logger.info('Processing previous cycle completion', { - cycleId: previousCycleId, - participantCount: participants.length - }); + // Fetch winner info from API (works even if no participants tracked locally) + let winnerDisplayName = 'Anon'; + let winnerTicketNumber = '0000'; + let winnerLightningAddress = ''; + let potSats = 0; + let totalTickets = 0; - if (participants.length === 0) { - return; + try { + const pastWins = await apiClient.getPastWins(1, 0); + // Try to find this specific cycle, or use the latest + const latestWin = pastWins.find(w => w.cycle_id === previousCycleId) || pastWins[0]; + + if (latestWin) { + winnerDisplayName = latestWin.winner_name || 'Anon'; + winnerLightningAddress = latestWin.winner_address || ''; + potSats = latestWin.pot_total_sats || 0; + if (latestWin.winning_ticket_serial !== null) { + winnerTicketNumber = latestWin.winning_ticket_serial.toString().padStart(4, '0'); + } + logger.info('Got winner info from API', { + cycleId: previousCycleId, + winnerName: winnerDisplayName, + potSats + }); + } + } catch (error) { + logger.error('Failed to fetch past wins for announcement', { error, cycleId: previousCycleId }); } - // Group participants by telegramId to avoid spamming users with multiple purchases + // Get participants for DM notifications + const participants = await stateManager.getCycleParticipants(previousCycleId); + + // Group participants by telegramId const userPurchases = new Map(); for (const participant of participants) { const existing = userPurchases.get(participant.telegramId) || []; @@ -155,13 +178,11 @@ class NotificationScheduler { userPurchases.set(participant.telegramId, existing); } - // First pass: Find the winning ticket info and pot amount - let winnerDisplayName = 'Anon'; - let winnerTicketNumber = '0000'; + totalTickets = userPurchases.size; + + // Find winner among tracked participants for DM notifications let winnerTelegramId: number | null = null; - let winnerLightningAddress = ''; - let potSats = 0; - let prizeSats = 0; + let prizeSats = potSats; let payoutStatus = 'processing'; for (const [telegramId, purchaseIds] of userPurchases) { @@ -170,13 +191,17 @@ class NotificationScheduler { const status = await apiClient.getTicketStatus(purchaseId); if (!status) continue; - potSats = status.cycle.pot_total_sats || 0; + if (status.cycle.pot_total_sats) { + potSats = status.cycle.pot_total_sats; + } if (status.result.is_winner) { const user = await stateManager.getUser(telegramId); winnerTelegramId = telegramId; - winnerDisplayName = user ? stateManager.getDisplayName(user) : 'Anon'; - winnerLightningAddress = status.purchase.lightning_address || ''; + if (user) { + winnerDisplayName = stateManager.getDisplayName(user); + } + winnerLightningAddress = status.purchase.lightning_address || winnerLightningAddress; const winningTicket = status.tickets.find(t => t.is_winning_ticket); if (winningTicket) { @@ -185,16 +210,16 @@ class NotificationScheduler { prizeSats = status.result.payout?.amount_sats || potSats; payoutStatus = status.result.payout?.status || 'processing'; - break; // Found winner, stop searching + break; } } catch (error) { logger.error('Error checking purchase status', { purchaseId, error }); } } - if (winnerTelegramId) break; // Found winner, stop searching + if (winnerTelegramId) break; } - // Second pass: Send ONE notification per user + // Send DM notifications to tracked participants const notifiedUsers = new Set(); for (const [telegramId, purchaseIds] of userPurchases) { @@ -208,7 +233,6 @@ class NotificationScheduler { const isWinner = telegramId === winnerTelegramId; if (isWinner) { - // Send winner notification await this.bot.sendMessage( telegramId, messages.notifications.winner( @@ -220,7 +244,6 @@ class NotificationScheduler { ); logger.info('Sent winner notification', { telegramId }); } else { - // Send loser notification with actual winning ticket number await this.bot.sendMessage( telegramId, messages.notifications.loser( @@ -236,14 +259,13 @@ class NotificationScheduler { } } - // Send group announcements - const uniqueUserCount = userPurchases.size; + // ALWAYS send group announcements (regardless of tracked participants) await this.sendGroupDrawAnnouncementsImmediate( previousCycleId, winnerDisplayName, winnerTicketNumber, potSats, - uniqueUserCount, + totalTickets, winnerLightningAddress ); } @@ -351,6 +373,7 @@ class NotificationScheduler { /** * Handle draw completed - send notifications to participants and groups + * Always sends group announcements, even if no participants tracked locally */ private async handleDrawCompleted(cycle: CycleInfo): Promise { if (!this.bot) return; @@ -364,22 +387,43 @@ class NotificationScheduler { // Clear reminders for this cycle this.clearRemindersForCycle(cycle.id); - // Get participants for this cycle - const participants = await stateManager.getCycleParticipants(cycle.id); - logger.info('Processing draw completion', { cycleId: cycle.id, - participantCount: participants.length, potSats: cycle.pot_total_sats }); - // Only proceed if there were participants - if (participants.length === 0) { - logger.info('No participants in cycle, skipping notifications', { cycleId: cycle.id }); - return; + // Fetch winner info from API (works even if no participants tracked locally) + let winnerDisplayName = 'Anon'; + let winnerTicketNumber = '0000'; + let winnerLightningAddress = ''; + let potSats = cycle.pot_total_sats; + let totalTickets = 0; + + try { + const pastWins = await apiClient.getPastWins(1, 0); + const latestWin = pastWins.find(w => w.cycle_id === cycle.id) || pastWins[0]; + + if (latestWin) { + winnerDisplayName = latestWin.winner_name || 'Anon'; + winnerLightningAddress = latestWin.winner_address || ''; + potSats = latestWin.pot_total_sats || cycle.pot_total_sats; + if (latestWin.winning_ticket_serial !== null) { + winnerTicketNumber = latestWin.winning_ticket_serial.toString().padStart(4, '0'); + } + logger.info('Got winner info from API', { + cycleId: cycle.id, + winnerName: winnerDisplayName, + potSats + }); + } + } catch (error) { + logger.error('Failed to fetch past wins for announcement', { error, cycleId: cycle.id }); } - // Group participants by telegramId to avoid spamming users with multiple purchases + // Get participants for DM notifications + const participants = await stateManager.getCycleParticipants(cycle.id); + + // Group participants by telegramId const userPurchases = new Map(); for (const participant of participants) { const existing = userPurchases.get(participant.telegramId) || []; @@ -387,12 +431,11 @@ class NotificationScheduler { userPurchases.set(participant.telegramId, existing); } - // First pass: Find the winning ticket info - let winnerDisplayName = 'Anon'; - let winnerTicketNumber = '0000'; + totalTickets = userPurchases.size; + + // Find winner among tracked participants for DM notifications let winnerTelegramId: number | null = null; - let winnerLightningAddress = ''; - let prizeSats = 0; + let prizeSats = potSats; let payoutStatus = 'processing'; for (const [telegramId, purchaseIds] of userPurchases) { @@ -404,26 +447,28 @@ class NotificationScheduler { if (status.result.is_winner) { const user = await stateManager.getUser(telegramId); winnerTelegramId = telegramId; - winnerDisplayName = user ? stateManager.getDisplayName(user) : 'Anon'; - winnerLightningAddress = status.purchase.lightning_address || ''; + if (user) { + winnerDisplayName = stateManager.getDisplayName(user); + } + winnerLightningAddress = status.purchase.lightning_address || winnerLightningAddress; const winningTicket = status.tickets.find(t => t.is_winning_ticket); if (winningTicket) { winnerTicketNumber = winningTicket.serial_number.toString().padStart(4, '0'); } - prizeSats = status.result.payout?.amount_sats || cycle.pot_total_sats; + prizeSats = status.result.payout?.amount_sats || potSats; payoutStatus = status.result.payout?.status || 'processing'; - break; // Found winner, stop searching + break; } } catch (error) { logger.error('Error checking purchase status', { purchaseId, error }); } } - if (winnerTelegramId) break; // Found winner, stop searching + if (winnerTelegramId) break; } - // Second pass: Send ONE notification per user + // Send DM notifications to tracked participants const notifiedUsers = new Set(); for (const [telegramId] of userPurchases) { @@ -437,7 +482,6 @@ class NotificationScheduler { const isWinner = telegramId === winnerTelegramId; if (isWinner) { - // Send winner notification await this.bot.sendMessage( telegramId, messages.notifications.winner( @@ -449,12 +493,11 @@ class NotificationScheduler { ); logger.info('Sent winner notification', { telegramId }); } else { - // Send loser notification with actual winning ticket number await this.bot.sendMessage( telegramId, messages.notifications.loser( winnerTicketNumber, - cycle.pot_total_sats.toLocaleString() + potSats.toLocaleString() ), { parse_mode: 'Markdown' } ); @@ -465,9 +508,8 @@ class NotificationScheduler { } } - // Send group announcements - const uniqueUserCount = userPurchases.size; - await this.sendGroupDrawAnnouncements(cycle, winnerDisplayName, winnerTicketNumber, uniqueUserCount, winnerLightningAddress); + // ALWAYS send group announcements (regardless of tracked participants) + await this.sendGroupDrawAnnouncements(cycle, winnerDisplayName, winnerTicketNumber, totalTickets, winnerLightningAddress); } /**