/** * NetUP PMS pull-integration endpoint. * * NetUP polls this URL every N minutes (configured as "TravelLine" integration type in NetUP). * We log every request so we can see exactly what NetUP sends, and respond with active bookings. * * Configure in NetUP: * Integration type: TravelLine * API URL: https://api.hotelsync.ru/netup-pms * Token: */ import type { FastifyPluginAsync } from 'fastify' import { db } from '../db' // In-memory ring buffer — last 100 captured requests (shared with netup.ts via export) const MAX_CAPTURE = 100 export const captured: { ts: string method: string url: string headers: Record query: Record body: unknown }[] = [] function recordRequest(request: { method: string url: string headers: Record query: unknown body: unknown }) { captured.push({ ts: new Date().toISOString(), method: request.method, url: request.url, headers: request.headers, query: request.query as Record, body: request.body, }) if (captured.length > MAX_CAPTURE) captured.shift() } const netupPms: FastifyPluginAsync = async (fastify) => { // Catch /netup-pms and /netup-pms/* — everything NetUP might call for (const pattern of ['/netup-pms', '/netup-pms/*']) { fastify.all(pattern, async (request, reply) => { recordRequest(request as Parameters[0]) fastify.log.info({ method: request.method, url: request.url }, 'NetUP PMS poll') const token = extractToken(request) if (!token) return reply.code(401).send({ error: 'Unauthorized' }) const { rows: [row] } = await db.query( `SELECT hs.hotel_id FROM netup_settings hs WHERE hs.tl_token = $1`, [token], ).catch(() => ({ rows: [] as { hotel_id: string }[] })) if (!row) return reply.send(buildEmptyResponse()) return reply.send(await buildResponse(row.hotel_id)) }) } } // ── Helpers ─────────────────────────────────────────────────────────────────── function extractToken(request: { headers: Record; query: unknown }): string | null { const auth = request.headers['authorization'] as string | undefined if (auth?.startsWith('Bearer ')) return auth.slice(7) if (auth?.startsWith('Token ')) return auth.slice(6) const q = request.query as Record return q?.token ?? q?.api_key ?? null } function buildEmptyResponse() { return { success: true, reservations: [], rooms: [] } } async function buildResponse(hotelId: string) { const { rows: bookings } = await db.query( `SELECT b.id, b.guest_name, b.guest_email, b.guest_phone, b.check_in, b.check_out, b.status, b.adults, b.children, r.number as room_number, r.id as room_id, m.netup_room_number FROM bookings b JOIN rooms r ON r.id = b.room_id LEFT JOIN netup_room_mapping m ON m.pms_room_id = r.id AND m.hotel_id = $1 WHERE b.hotel_id = $1 AND b.status IN ('confirmed','checked_in') AND b.check_out >= CURRENT_DATE ORDER BY b.check_in`, [hotelId], ) const reservations = bookings.map((b: Record) => ({ id: b.id, status: b.status === 'checked_in' ? 'CheckedIn' : 'Confirmed', guestName: b.guest_name, guestEmail: b.guest_email ?? '', guestPhone: b.guest_phone ?? '', roomNumber: b.netup_room_number ?? b.room_number, pmsRoomId: b.room_id, checkIn: b.check_in, checkOut: b.check_out, adults: b.adults, children: b.children ?? 0, })) return { success: true, reservations } } export default netupPms