import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { resolve } from "node:path"; import { and, eq, gte, lte } from "drizzle-orm"; import nodemailer from "nodemailer"; import { db } from "../db/client.js"; import { getDataDir } from "../db/db-utils.js"; import { doseTracking, medications } from "../db/schema.js"; import { getDateLocale, getFooterHtml, getFooterPlain, getTranslations, type Language, t, } from "../i18n/translations.js"; import { getAllUserSettings, sendShoutrrrNotification, type UserSettings } from "../routes/settings.js"; import type { ServiceLogger } from "../utils/logger.js"; // Import shared utilities import { cleanOldIntakeReminders, createDefaultIntakeReminderState, getTimezone, getTodaysIntakes, getUpcomingIntakes, type Intake, type IntakeReminderState, parseIntakeReminderState, parseIntakesJson, parseTakenByJson, type UpcomingIntake, } from "../utils/scheduler-utils.js"; import { updateReminderSentTime, updateUserReminderSentTime } from "./reminder-scheduler.js"; const REMINDER_MINUTES_BEFORE = parseInt(process.env.REMINDER_MINUTES_BEFORE ?? "15", 10); const CHECK_INTERVAL_MS = 60 * 1000; // Check every 1 minute const intakeReminderStateFile = resolve(getDataDir(), "intake-reminder-state.json"); function loadIntakeReminderState(): IntakeReminderState { try { if (existsSync(intakeReminderStateFile)) { return parseIntakeReminderState(readFileSync(intakeReminderStateFile, "utf-8")); } } catch { // ignore } return createDefaultIntakeReminderState(); } function saveIntakeReminderState(state: IntakeReminderState): void { writeFileSync(intakeReminderStateFile, JSON.stringify(state, null, 2)); } async function sendIntakeReminderEmail( email: string, intakes: UpcomingIntake[], language: Language, isRepeat: boolean = false, repeatIntervalMinutes?: number, currentCount?: number, maxCount?: number ): Promise<{ success: boolean; error?: string }> { const smtpHost = process.env.SMTP_HOST; const smtpUser = process.env.SMTP_USER; const smtpPass = process.env.SMTP_TOKEN || process.env.SMTP_PASS; // Token takes precedence const smtpPort = parseInt(process.env.SMTP_PORT ?? "587", 10); const smtpSecure = process.env.SMTP_SECURE === "true"; const smtpFrom = process.env.SMTP_FROM ?? smtpUser; if (!smtpHost || !smtpUser) { return { success: false, error: "SMTP not configured" }; } const tr = getTranslations(language); // Helper to format dosage with weight const formatDosage = (intake: UpcomingIntake): string => { const pillText = `${intake.usage} ${intake.usage === 1 ? tr.common.pill : tr.intakeReminder.pills}`; if (intake.pillWeightMg) { const totalMg = intake.usage * intake.pillWeightMg; const weightStr = totalMg >= 1000 ? `${(totalMg / 1000).toFixed(1)} g` : `${totalMg} mg`; return `${pillText} (${weightStr})`; } return pillText; }; // Helper to format medication name with takenBy (single person or null) const formatMedName = (intake: UpcomingIntake): string => { if (intake.takenBy) { return `${intake.medName} ${t(tr.intakeReminder.takenBy, { name: intake.takenBy })}`; } return intake.medName; }; const tableRows = intakes .map( (intake) => ` ${formatMedName(intake)} ${formatDosage(intake)} ${intake.intakeTimeStr} ` ) .join(""); const alertText = intakes.length === 1 ? tr.intakeReminder.alertSingle : t(tr.intakeReminder.alertMultiple, { count: intakes.length }); // Different description for repeat reminders let description: string; if (isRepeat && repeatIntervalMinutes && currentCount !== undefined && maxCount !== undefined) { const remainingReminders = maxCount - currentCount; if (remainingReminders <= 0) { description = language === "de" ? "⚠️ Dies ist die letzte Erinnerung." : "⚠️ This is the last reminder."; } else if (remainingReminders === 1) { description = language === "de" ? `ℹ️ Eine weitere Erinnerung wird in ${repeatIntervalMinutes} Minuten gesendet.` : `ℹ️ One more reminder will be sent in ${repeatIntervalMinutes} minutes.`; } else { description = language === "de" ? `ℹ️ ${remainingReminders} weitere Erinnerungen werden alle ${repeatIntervalMinutes} Minuten gesendet.` : `ℹ️ ${remainingReminders} more reminders will be sent every ${repeatIntervalMinutes} minutes.`; } } else { description = t(tr.intakeReminder.description, { minutes: REMINDER_MINUTES_BEFORE }); } const html = `

${tr.intakeReminder.title}

${description}

${alertText}

${tableRows}
${tr.intakeReminder.tableHeaders.medication} ${tr.intakeReminder.tableHeaders.dosage} ${tr.intakeReminder.tableHeaders.time}

${getFooterHtml(language)}

`; // Helper for plain text dosage const formatDosagePlain = (intake: UpcomingIntake): string => { const pillText = `${intake.usage} ${intake.usage === 1 ? tr.common.pill : tr.intakeReminder.pills}`; if (intake.pillWeightMg) { const totalMg = intake.usage * intake.pillWeightMg; const weightStr = totalMg >= 1000 ? `${(totalMg / 1000).toFixed(1)} g` : `${totalMg} mg`; return `${pillText} (${weightStr})`; } return pillText; }; const plainText = `${tr.intakeReminder.title} ${description} ${intakes .map((i) => { const takenByStr = i.takenBy ? ` ${t(tr.intakeReminder.takenBy, { name: i.takenBy })}` : ""; return `${i.medName}${takenByStr}: ${formatDosagePlain(i)} - ${i.intakeTimeStr}`; }) .join("\n")} --- ${getFooterPlain(language)}`; const subject = isRepeat ? `[Reminder] ${t(tr.intakeReminder.subject, { medications: intakes.map((i) => i.medName).join(", ") })}` : t(tr.intakeReminder.subject, { medications: intakes.map((i) => i.medName).join(", ") }); try { const transporter = nodemailer.createTransport({ host: smtpHost, port: smtpPort, secure: smtpSecure, auth: { user: smtpUser, pass: smtpPass ?? "", }, }); await transporter.sendMail({ from: smtpFrom, to: email, subject: `💊 ${subject}`, text: plainText, html, }); return { success: true }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; return { success: false, error: errorMessage }; } } async function checkAndSendIntakeReminders(logger: ServiceLogger): Promise { logger.debug(`[IntakeReminder] Checking for intake reminders...`); // Get all user settings to iterate over each user const allUserSettings = await getAllUserSettings(); if (allUserSettings.length === 0) { logger.debug(`[IntakeReminder] No users with settings found`); return; // No users with settings } logger.debug(`[IntakeReminder] Found ${allUserSettings.length} users to check`); for (const userSettings of allUserSettings) { await checkAndSendIntakeRemindersForUser(userSettings, logger); } } async function checkAndSendIntakeRemindersForUser( settings: UserSettings & { userId: number }, logger: ServiceLogger ): Promise { const language = settings.language; const tr = getTranslations(language); logger.debug( `[IntakeReminder] Checking user ${settings.userId} - repeat:${settings.repeatRemindersEnabled} skip:${settings.skipRemindersForTakenDoses}` ); // Check if any intake reminder notifications are enabled (granular check) const emailEnabled = settings.emailEnabled && settings.notificationEmail && settings.emailIntakeReminders; const shoutrrrEnabled = settings.shoutrrrEnabled && settings.shoutrrrUrl && settings.shoutrrrIntakeReminders; if (!emailEnabled && !shoutrrrEnabled) { logger.debug( `[IntakeReminder] User ${settings.userId}: No intake notifications enabled (email:${emailEnabled}, shoutrrr:${shoutrrrEnabled})` ); return; // No intake reminder notifications enabled for this user } logger.debug( `[IntakeReminder] User ${settings.userId}: Notifications enabled (email:${emailEnabled}, shoutrrr:${shoutrrrEnabled})` ); // Get all medications with intake reminders enabled for this user const rows = await db .select() .from(medications) .where(eq(medications.userId, settings.userId)) .orderBy(medications.id); const medsWithReminders = rows.filter((row) => row.intakeRemindersEnabled); if (medsWithReminders.length === 0) { logger.debug(`[IntakeReminder] User ${settings.userId}: No medications have reminders enabled`); return; // No medications have reminders enabled for this user } logger.debug( `[IntakeReminder] User ${settings.userId}: Found ${medsWithReminders.length} medications with reminders` ); const state = loadIntakeReminderState(); const allUpcoming: (UpcomingIntake & { medicationId: number; blisterIndex: number })[] = []; const locale = getDateLocale(language); const tz = getTimezone(); // Get start and end of today in user's timezone (for filtering today's doses only) const now = new Date(); const todayStart = new Date(now.toLocaleString("en-US", { timeZone: tz })); todayStart.setHours(0, 0, 0, 0); const todayEnd = new Date(now.toLocaleString("en-US", { timeZone: tz })); todayEnd.setHours(23, 59, 59, 999); logger.debug( `[IntakeReminder] User ${settings.userId}: Today range: ${todayStart.toISOString()} to ${todayEnd.toISOString()}` ); // Find intakes: upcoming ones in reminder window + past ones for repeat reminders for (const med of medsWithReminders) { // Parse intakes using new format (with per-intake takenBy), falling back to legacy const intakes = parseIntakesJson( med.intakesJson, { usageJson: med.usageJson, everyJson: med.everyJson, startJson: med.startJson }, med.intakeRemindersEnabled ?? false ); // Medication-level takenBy (for fallback/display purposes) const medicationTakenBy = parseTakenByJson(med.takenByJson); logger.debug( `[IntakeReminder] User ${settings.userId}: Processing medication "${med.name}" with ${intakes.length} intakes` ); // Filter intakes that have reminders enabled (per-intake setting or medication-level) const intakesWithReminders = intakes.filter((intake, idx) => { const hasReminder = intake.intakeRemindersEnabled || med.intakeRemindersEnabled; if (!hasReminder) { logger.debug(`[IntakeReminder] User ${settings.userId}: Intake ${idx} has reminders disabled, skipping`); } return hasReminder; }); // Process each intake separately to track blisterIndex intakesWithReminders.forEach((intake, blisterIndex) => { const actualIndex = intakes.indexOf(intake); // Get the actual index in original array logger.debug( `[IntakeReminder] User ${settings.userId}: Intake ${actualIndex} - start: ${intake.start}, every: ${intake.every} days, usage: ${intake.usage}, takenBy: ${intake.takenBy || "(none)"}` ); // Always get upcoming intakes (15 min before) for first reminders const upcomingIntakes = getUpcomingIntakes( med.name, [intake], REMINDER_MINUTES_BEFORE, medicationTakenBy, med.pillWeightMg, locale, tz, undefined, // nowOverride med.id, med.doseUnit ?? "mg" ); logger.debug( `[IntakeReminder] User ${settings.userId}: Intake ${actualIndex} found ${upcomingIntakes.length} upcoming intakes (reminder window)` ); // Add upcoming intakes for first reminders allUpcoming.push( ...upcomingIntakes.map((upcomingIntake) => ({ ...upcomingIntake, medicationId: med.id, blisterIndex: actualIndex, })) ); // If repeat reminders enabled, also check for missed intakes (past the intake time) if (settings.repeatRemindersEnabled) { const allTodaysIntakes = getTodaysIntakes( med.name, [intake], medicationTakenBy, med.pillWeightMg, locale, tz, med.id, med.doseUnit ?? "mg" ); logger.debug( `[IntakeReminder] User ${settings.userId}: Intake ${actualIndex} - all today's intakes: ${allTodaysIntakes.length}, times: ${allTodaysIntakes.map((i) => i.intakeTime.toISOString()).join(", ")}` ); const missedIntakes = allTodaysIntakes.filter( (todayIntake) => todayIntake.intakeTime.getTime() < now.getTime() ); logger.debug( `[IntakeReminder] User ${settings.userId}: Intake ${actualIndex} found ${missedIntakes.length} missed intakes (past intake time)` ); // Add missed intakes for repeat reminders (only if not already in upcoming list) const upcomingTimes = new Set(upcomingIntakes.map((i) => i.intakeTime.getTime())); allUpcoming.push( ...missedIntakes .filter((missed) => !upcomingTimes.has(missed.intakeTime.getTime())) .map((missed) => ({ ...missed, medicationId: med.id, blisterIndex: actualIndex, })) ); } }); } logger.debug(`[IntakeReminder] User ${settings.userId}: Total ${allUpcoming.length} intakes for today`); if (allUpcoming.length === 0) { logger.debug(`[IntakeReminder] User ${settings.userId}: No intakes for today`); return; // No upcoming intakes for today } // Determine which doses need reminders (new or repeated) const nowMs = Date.now(); const maxReminders = settings.maxNaggingReminders ?? 5; type ReminderWithCount = (typeof allUpcoming)[number] & { currentSendCount: number; // 0 = advance reminder (no counter), 1+ = nagging count maxReminders: number; isAdvanceReminder: boolean; // true if this is the 15-min-before reminder }; let remindersToSend: ReminderWithCount[] = []; for (const intake of allUpcoming) { const key = `user_${settings.userId}:${intake.medName}:${intake.intakeTime.getTime()}`; const existingEntry = state.reminders[key]; const intakeTimeMs = intake.intakeTime.getTime(); const isIntakePast = intakeTimeMs < nowMs; if (!existingEntry) { // New dose - send first reminder if (isIntakePast) { // Intake time already passed and we have no state entry. Check how recently it was missed. const minutesSinceIntake = (nowMs - intakeTimeMs) / 60000; const gracePeriodMinutes = (settings.reminderRepeatIntervalMinutes ?? 30) + REMINDER_MINUTES_BEFORE; if (minutesSinceIntake <= gracePeriodMinutes) { // Recently missed — scheduler likely recovered from sleep/restart. // Send a catch-up reminder (counts as first nagging reminder). remindersToSend.push({ ...intake, currentSendCount: 1, maxReminders, isAdvanceReminder: false }); logger.info( `[IntakeReminder] User ${settings.userId}: Catch-up reminder for recently missed "${intake.medName}" at ${intake.intakeTimeStr} (${Math.round(minutesSinceIntake)} min ago)` ); } else { // Long ago — seed state without notification (user likely already noticed) state.reminders[key] = { firstSentAt: nowMs, lastSentAt: nowMs, sendCount: 0, advanceSent: false, }; logger.debug( `[IntakeReminder] User ${settings.userId}: Seeding state for old past "${intake.medName}" at ${intake.intakeTimeStr} (no notification — ${Math.round(minutesSinceIntake)} min ago)` ); } } else { // Upcoming - this is advance reminder (no counter) remindersToSend.push({ ...intake, currentSendCount: 0, maxReminders, isAdvanceReminder: true }); logger.debug( `[IntakeReminder] User ${settings.userId}: Advance reminder for "${intake.medName}" at ${intake.intakeTimeStr}` ); } } else if (settings.repeatRemindersEnabled && isIntakePast) { // Intake time passed - check if we need to send nagging reminder const intervalMs = settings.reminderRepeatIntervalMinutes * 60 * 1000; const timeSinceLastReminder = nowMs - existingEntry.lastSentAt; // If only advance reminder was sent (sendCount=0), first nagging has count=1 // Otherwise increment from current sendCount const currentNaggingCount = existingEntry.sendCount; if (currentNaggingCount >= maxReminders) { // Max nagging reminders reached - stop logger.debug( `[IntakeReminder] User ${settings.userId}: Max nagging (${maxReminders}) reached for "${intake.medName}" at ${intake.intakeTimeStr}` ); } else if (timeSinceLastReminder >= intervalMs) { const nextSendCount = currentNaggingCount + 1; remindersToSend.push({ ...intake, currentSendCount: nextSendCount, maxReminders, isAdvanceReminder: false }); logger.debug( `[IntakeReminder] User ${settings.userId}: Nagging reminder for "${intake.medName}" at ${intake.intakeTimeStr} (${nextSendCount}/${maxReminders})` ); } } // Else: Already sent and either repeats disabled or intake not yet past - skip } if (remindersToSend.length === 0) { return; // All reminders already sent and no repeats needed } // If skipRemindersForTakenDoses is enabled, filter out doses that were already taken today if (settings.skipRemindersForTakenDoses) { // Query doses marked as taken today (takenAt is timestamp, stored as seconds since epoch) const takenToday = await db .select() .from(doseTracking) .where( and( eq(doseTracking.userId, settings.userId), gte(doseTracking.takenAt, todayStart), lte(doseTracking.takenAt, todayEnd) ) ); const takenDoseIds = new Set(takenToday.map((d) => d.doseId)); // Filter out reminders for doses that were already taken remindersToSend = remindersToSend.filter((intake) => { // Convert to date-only timestamp (midnight) to match frontend dose ID format const intakeDate = intake.intakeTime; const dateOnlyMs = new Date(intakeDate.getFullYear(), intakeDate.getMonth(), intakeDate.getDate()).getTime(); // Check both with and without person suffix if (intake.takenBy) { // For person-specific intake, check if that person has taken it const doseId = `${intake.medicationId}-${intake.blisterIndex}-${dateOnlyMs}-${intake.takenBy}`; const isTaken = takenDoseIds.has(doseId); if (isTaken) { logger.debug( `[IntakeReminder] User ${settings.userId}: Skipping "${intake.medName}" - dose ${doseId} already taken` ); } return !isTaken; } else { // For non-person-specific intakes const doseId = `${intake.medicationId}-${intake.blisterIndex}-${dateOnlyMs}`; const isTaken = takenDoseIds.has(doseId); if (isTaken) { logger.debug( `[IntakeReminder] User ${settings.userId}: Skipping "${intake.medName}" - dose ${doseId} already taken` ); } return !isTaken; } }); if (remindersToSend.length === 0) { logger.debug(`[IntakeReminder] User ${settings.userId}: All doses taken, skipping reminders`); return; } } logger.info(`[IntakeReminder] User ${settings.userId}: Sending reminder for ${remindersToSend.length} intakes...`); // Determine if this is a repeat reminder: // - Any intake already has a state entry AND is past (repeat after first reminder) // - OR intake is past even without state entry (missed the 15-min window) const isRepeatReminder = remindersToSend.some((intake) => { const intakeTimeMs = intake.intakeTime.getTime(); const isIntakePast = intakeTimeMs < nowMs; return isIntakePast; // Use repeat message for ANY missed intake }); let emailSuccess = false; let shoutrrrSuccess = false; // Send email if enabled for intake reminders if (emailEnabled) { // Calculate counts for repeat reminder text const hasNaggingReminder = remindersToSend.some((r) => !r.isAdvanceReminder); const highestSendCount = Math.max(...remindersToSend.map((r) => r.currentSendCount)); const maxReminderCount = remindersToSend[0]?.maxReminders ?? 5; const result = await sendIntakeReminderEmail( settings.notificationEmail!, remindersToSend, language, isRepeatReminder, settings.reminderRepeatIntervalMinutes, hasNaggingReminder ? highestSendCount : undefined, hasNaggingReminder ? maxReminderCount : undefined ); emailSuccess = result.success; if (result.success) { logger.info(`[IntakeReminder] User ${settings.userId}: Email sent successfully`); } else { logger.error(`[IntakeReminder] User ${settings.userId}: Failed to send email: ${result.error}`); } } // Send Shoutrrr notification if enabled for intake reminders if (shoutrrrEnabled) { // Check if any reminder is a nagging reminder (not advance) const hasNaggingReminder = remindersToSend.some((r) => !r.isAdvanceReminder); const highestSendCount = Math.max(...remindersToSend.map((r) => r.currentSendCount)); const maxReminderCount = remindersToSend[0]?.maxReminders ?? 5; let title: string; if (hasNaggingReminder && highestSendCount > 0) { // Nagging reminder - show counter const counterStr = `(${highestSendCount}/${maxReminderCount})`; title = language === "de" ? `⚠️ Erinnerung: Medikamenteneinnahme ${counterStr}` : `⚠️ Reminder: Medication intake ${counterStr}`; } else { // Advance reminder - no counter title = t(tr.push.intakeTitle, { minutes: REMINDER_MINUTES_BEFORE }); } // Only show repeat note for nagging reminders, not for advance reminders let repeatNote = ""; if (hasNaggingReminder && settings.reminderRepeatIntervalMinutes) { const remainingReminders = maxReminderCount - highestSendCount; if (remainingReminders <= 0) { // Last reminder repeatNote = language === "de" ? "\n\n⚠️ Dies ist die letzte Erinnerung." : "\n\n⚠️ This is the last reminder."; } else if (remainingReminders === 1) { // One more reminder repeatNote = language === "de" ? `\n\nℹ️ Eine weitere Erinnerung wird in ${settings.reminderRepeatIntervalMinutes} Minuten gesendet.` : `\n\nℹ️ One more reminder will be sent in ${settings.reminderRepeatIntervalMinutes} minutes.`; } else { // Multiple reminders remaining repeatNote = language === "de" ? `\n\nℹ️ ${remainingReminders} weitere Erinnerungen werden alle ${settings.reminderRepeatIntervalMinutes} Minuten gesendet.` : `\n\nℹ️ ${remainingReminders} more reminders will be sent every ${settings.reminderRepeatIntervalMinutes} minutes.`; } } const message = remindersToSend .map((i) => { const takenByStr = i.takenBy ? ` ${t(tr.intakeReminder.takenBy, { name: i.takenBy })}` : ""; let dosage = `${i.usage} ${i.usage === 1 ? tr.common.pill : tr.common.pills}`; if (i.pillWeightMg) { const totalMg = i.usage * i.pillWeightMg; dosage += totalMg >= 1000 ? ` (${(totalMg / 1000).toFixed(1)} g)` : ` (${totalMg} mg)`; } return `• ${i.medName}${takenByStr}: ${dosage} @ ${i.intakeTimeStr}`; }) .join("\n") + repeatNote + `\n\n---\n${getFooterPlain(language)}`; const result = await sendShoutrrrNotification(settings.shoutrrrUrl!, title, message); shoutrrrSuccess = result.success; if (result.success) { logger.info(`[IntakeReminder] User ${settings.userId}: Push notification sent successfully`); } else { logger.error(`[IntakeReminder] User ${settings.userId}: Failed to send push: ${result.error}`); } } // Update state if any notification was sent successfully if (emailSuccess || shoutrrrSuccess) { // Update or create entries for sent reminders for (const intake of remindersToSend) { const key = `user_${settings.userId}:${intake.medName}:${intake.intakeTime.getTime()}`; const existing = state.reminders[key]; if (existing) { // Update existing entry if (intake.isAdvanceReminder) { // Advance reminder - don't increment nagging count state.reminders[key] = { ...existing, lastSentAt: nowMs, advanceSent: true, }; } else { // Nagging reminder - increment count state.reminders[key] = { firstSentAt: existing.firstSentAt, lastSentAt: nowMs, sendCount: existing.sendCount + 1, advanceSent: existing.advanceSent, }; } } else { // Create new entry if (intake.isAdvanceReminder) { // Advance reminder - sendCount stays 0 state.reminders[key] = { firstSentAt: nowMs, lastSentAt: nowMs, sendCount: 0, advanceSent: true, }; } else { // First nagging reminder - sendCount starts at 1 state.reminders[key] = { firstSentAt: nowMs, lastSentAt: nowMs, sendCount: 1, advanceSent: false, }; } } } // Clean up old entries (remove doses from past days) state.reminders = cleanOldIntakeReminders(state.reminders, tz); saveIntakeReminderState(state); // Update global reminder state for UI display const channel = emailSuccess && shoutrrrSuccess ? "both" : emailSuccess ? "email" : "push"; updateReminderSentTime("intake", channel); // Also update user settings in database so frontend can display the info // Get the first reminder's medication name and taken by for display const firstReminder = remindersToSend[0]; const medName = firstReminder?.medName; const takenBy = firstReminder?.takenBy || undefined; await updateUserReminderSentTime(settings.userId, "intake", channel, medName, takenBy); } } let intakeCheckInterval: NodeJS.Timeout | null = null; export function startIntakeReminderScheduler(logger: ServiceLogger): void { logger.info(`[IntakeReminder] Starting intake reminder scheduler (checks every minute)...`); // Run immediately on start checkAndSendIntakeReminders(logger).catch((err) => logger.error(`[IntakeReminder] Error: ${err}`)); // Then run every minute intakeCheckInterval = setInterval(() => { checkAndSendIntakeReminders(logger).catch((err) => logger.error(`[IntakeReminder] Error: ${err}`)); }, CHECK_INTERVAL_MS); logger.info(`[IntakeReminder] Scheduler started - checking every minute for upcoming intakes`); } export function stopIntakeReminderScheduler(): void { if (intakeCheckInterval) { clearInterval(intakeCheckInterval); intakeCheckInterval = null; } }