import { FastifyPluginAsync } from 'fastify' import { db } from '../db' import { broadcast } from './ws' type SlugParam = { Params: { slug: string } } type SlugIdParam = { Params: { slug: string; id: string } } export interface NotificationRow { id: string hotel_id: string type: string title: string body: string booking_id: string | null room_id: string | null link: string | null is_read: boolean created_at: string } export async function createNotification( hotelId: string, hotelSlug: string, data: { type: string title: string body: string bookingId?: string | null roomId?: string | null link?: string | null }, ): Promise { const { rows } = await db.query( `INSERT INTO notifications (hotel_id, type, title, body, booking_id, room_id, link) VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING *`, [hotelId, data.type, data.title, data.body, data.bookingId ?? null, data.roomId ?? null, data.link ?? null], ) const notif = rows[0] broadcast(hotelSlug, { type: 'notification:new', notification: notif }) return notif } const notifications: FastifyPluginAsync = async (fastify) => { const getHotelId = async (slug: string): Promise => { const { rows } = await db.query('SELECT id FROM hotels WHERE slug = $1', [slug]) return rows[0]?.id ?? null } const canAccess = (userSlug: string | null, role: string, slug: string) => role === 'super_admin' || userSlug === slug // GET /api/hotels/:slug/notifications fastify.get( '/api/hotels/:slug/notifications', { onRequest: [fastify.authenticate] }, async (request, reply) => { const { slug } = request.params if (!canAccess(request.user.hotelSlug, request.user.role, slug)) return reply.code(403).send({ error: 'Forbidden' }) const hotelId = await getHotelId(slug) if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' }) const { rows } = await db.query( `SELECT * FROM notifications WHERE hotel_id = $1 ORDER BY created_at DESC LIMIT 100`, [hotelId], ) return rows }, ) // PATCH /api/hotels/:slug/notifications/:id — mark read fastify.patch( '/api/hotels/:slug/notifications/:id', { onRequest: [fastify.authenticate] }, async (request, reply) => { const { slug, id } = request.params if (!canAccess(request.user.hotelSlug, request.user.role, slug)) return reply.code(403).send({ error: 'Forbidden' }) const hotelId = await getHotelId(slug) if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' }) const { rows } = await db.query( `UPDATE notifications SET is_read = true WHERE id = $1 AND hotel_id = $2 RETURNING *`, [id, hotelId], ) if (!rows[0]) return reply.code(404).send({ error: 'Not found' }) return rows[0] }, ) // POST /api/hotels/:slug/notifications/read-all fastify.post( '/api/hotels/:slug/notifications/read-all', { onRequest: [fastify.authenticate] }, async (request, reply) => { const { slug } = request.params if (!canAccess(request.user.hotelSlug, request.user.role, slug)) return reply.code(403).send({ error: 'Forbidden' }) const hotelId = await getHotelId(slug) if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' }) await db.query( `UPDATE notifications SET is_read = true WHERE hotel_id = $1 AND is_read = false`, [hotelId], ) return { ok: true } }, ) // DELETE /api/hotels/:slug/notifications/:id fastify.delete( '/api/hotels/:slug/notifications/:id', { onRequest: [fastify.authenticate] }, async (request, reply) => { const { slug, id } = request.params if (!canAccess(request.user.hotelSlug, request.user.role, slug)) return reply.code(403).send({ error: 'Forbidden' }) const hotelId = await getHotelId(slug) if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' }) const { rowCount } = await db.query( `DELETE FROM notifications WHERE id = $1 AND hotel_id = $2`, [id, hotelId], ) if (!rowCount) return reply.code(404).send({ error: 'Not found' }) return reply.code(204).send() }, ) } export default notifications