import { db } from './db' import { broadcast } from './routes/ws' import { createNotification } from './routes/notifications' import { getHkSettings } from './routes/housekeeping-settings' const JOB_INTERVAL_MS = 60 * 60 * 1000 // every 1 hour async function runAutoJobs(): Promise { try { await runAutoCancelNoShows() await runAutoCheckouts() } catch (err) { console.error('[jobs] error running auto jobs:', err) } } async function runAutoCancelNoShows(): Promise { // Find hotels where auto_cancel_noshow_enabled = true // Also fetch check_in_time to calculate from the correct arrival time const { rows: hotels } = await db.query<{ id: string; slug: string; hours: number; checkInTime: string }>(` SELECT h.id, h.slug, COALESCE(h.check_in_time, '14:00') AS "checkInTime", COALESCE( (SELECT (value::text)::int FROM hotel_settings WHERE hotel_id = h.id AND key = 'auto_cancel_noshow_hours'), 24 ) AS hours FROM hotels h WHERE EXISTS ( SELECT 1 FROM hotel_settings WHERE hotel_id = h.id AND key = 'auto_cancel_noshow_enabled' AND value = 'true'::jsonb ) `) for (const hotel of hotels) { // Threshold = check_in date + hotel check-in time + N hours // e.g. check-in date 2025-03-23, check_in_time=14:00, hours=24 → cancel after 14:00 next day const { rows: bookings } = await db.query<{ id: string; guest_name: string | null; room_id: string }>(` SELECT b.id, b.guest_name, b.room_id FROM bookings b WHERE b.hotel_id = $1 AND b.status = 'confirmed' AND ( b.check_in::date + $2::time + ($3 || ' hours')::interval ) < NOW() `, [hotel.id, hotel.checkInTime, hotel.hours]) for (const b of bookings) { const { rows } = await db.query( `UPDATE bookings SET status = 'no_show', updated_at = NOW() WHERE id = $1 RETURNING *`, [b.id], ) if (rows[0]) { broadcast(hotel.slug, { type: 'booking:updated', booking: rows[0] }) } await createNotification(hotel.id, hotel.slug, { type: 'booking_cancelled', title: 'Гость не заехал — бронь отменена', body: `${b.guest_name ?? 'Гость'} не заехал вовремя. Бронирование автоматически отмечено как неявка.`, bookingId: b.id, link: `/${hotel.slug}/bookings`, }).catch(() => {}) } } } async function runAutoCheckouts(): Promise { // Find hotels where auto_checkout_enabled = true // Also fetch check_out_time so we calculate from the correct departure time const { rows: hotels } = await db.query<{ id: string; slug: string; hours: number; checkOutTime: string }>(` SELECT h.id, h.slug, COALESCE(h.check_out_time, '12:00') AS "checkOutTime", COALESCE( (SELECT (value::text)::int FROM hotel_settings WHERE hotel_id = h.id AND key = 'auto_checkout_hours'), 12 ) AS hours FROM hotels h WHERE EXISTS ( SELECT 1 FROM hotel_settings WHERE hotel_id = h.id AND key = 'auto_checkout_enabled' AND value = 'true'::jsonb ) `) for (const hotel of hotels) { // Threshold = check_out date + hotel checkout time + N hours // e.g. checkout date 2025-03-23, check_out_time=12:00, hours=2 → auto-checkout after 14:00 const { rows: bookings } = await db.query<{ id: string; guest_name: string | null; room_id: string }>(` SELECT b.id, b.guest_name, b.room_id FROM bookings b WHERE b.hotel_id = $1 AND b.status = 'checked_in' AND ( b.check_out::date + $2::time + ($3 || ' hours')::interval ) < NOW() `, [hotel.id, hotel.checkOutTime, hotel.hours]) for (const b of bookings) { // Update booking to checked_out const { rows } = await db.query( `UPDATE bookings SET status = 'checked_out', updated_at = NOW() WHERE id = $1 RETURNING *`, [b.id], ) if (rows[0]) { broadcast(hotel.slug, { type: 'booking:updated', booking: rows[0] }) } // Auto-create housekeeping task if enabled const hkSettings = await getHkSettings(hotel.id).catch(() => null) if (hkSettings?.checkout_auto && b.room_id) { const today = new Date().toISOString().slice(0, 10) const { rows: taskRows } = await db.query( `INSERT INTO housekeeping_tasks (hotel_id, room_id, type, priority, notes, due_date) VALUES ($1,$2,'turnover',$3,$4,$5) RETURNING *`, [hotel.id, b.room_id, hkSettings.checkout_priority, `Автоматическая уборка после выезда${b.guest_name ? ': ' + b.guest_name : ''}`, today], ) await db.query( `UPDATE rooms SET housekeeping_status = 'dirty' WHERE id = $1`, [b.room_id], ) if (taskRows[0]) { broadcast(hotel.slug, { type: 'housekeeping_task_created', task: taskRows[0] }) } } await createNotification(hotel.id, hotel.slug, { type: 'booking_checkout', title: 'Автоматическое выселение', body: `${b.guest_name ?? 'Гость'} выселен автоматически по истечении времени проживания.`, bookingId: b.id, link: `/${hotel.slug}/calendar`, }).catch(() => {}) } } } export function startJobs(): void { // Small delay to let DB migrations finish on startup setTimeout(() => { runAutoJobs().catch(console.error) }, 15_000) setInterval(() => { runAutoJobs().catch(console.error) }, JOB_INTERVAL_MS) }