Files
hotelsync/backend/src/jobs.ts
HotelSync 9be51e5d07 fix: jobs run every 1h, auto-checkout from hotel check_out_time
- Interval reduced from 10m to 1h (min period is 2h — hourly is enough)
- Auto-checkout threshold: check_out date + hotel.check_out_time + N hours
  (e.g. checkout Mar 23 + 12:00 + 2h = auto-checkout at 14:00)
- Auto-cancel threshold: check_in date + hotel.check_in_time + N hours

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 23:13:55 +03:00

169 lines
5.8 KiB
TypeScript

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<void> {
try {
await runAutoCancelNoShows()
await runAutoCheckouts()
} catch (err) {
console.error('[jobs] error running auto jobs:', err)
}
}
async function runAutoCancelNoShows(): Promise<void> {
// 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<void> {
// 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)
}