import 'server-only'; import type { SupabaseClient } from '@supabase/supabase-js'; import { getLogger } from '@kit/shared/logger'; import type { Database } from '@kit/supabase/database'; import { getValidTransitions, validateTransition, } from '../../lib/booking-status-machine'; import { BookingConcurrencyConflictError, InvalidBookingStatusTransitionError, } from '../../lib/errors'; import type { CreateBookingInput } from '../../schema/booking.schema'; const NAMESPACE = 'booking-crud'; export function createBookingCrudService(client: SupabaseClient) { return { async list( accountId: string, opts?: { status?: string; from?: string; to?: string; page?: number; pageSize?: number; }, ) { let query = client .from('bookings') .select('*', { count: 'exact' }) .eq('account_id', accountId) .order('check_in', { ascending: false }); if (opts?.status) query = query.eq('status', opts.status); if (opts?.from) query = query.gte('check_in', opts.from); if (opts?.to) query = query.lte('check_out', opts.to); const page = opts?.page ?? 1; const pageSize = opts?.pageSize ?? 25; query = query.range((page - 1) * pageSize, page * pageSize - 1); const { data, error, count } = await query; if (error) throw error; const total = count ?? 0; return { data: data ?? [], total, page, pageSize, totalPages: Math.max(1, Math.ceil(total / pageSize)), }; }, async create(input: CreateBookingInput) { const logger = await getLogger(); logger.info({ name: NAMESPACE }, 'Creating booking...'); const { data, error } = await (client.rpc as CallableFunction)( 'create_booking_atomic', { p_account_id: input.accountId, p_room_id: input.roomId, p_guest_id: input.guestId ?? null, p_check_in: input.checkIn, p_check_out: input.checkOut, p_adults: input.adults ?? 1, p_children: input.children ?? 0, p_status: input.status ?? 'confirmed', p_total_price: input.totalPrice ?? null, p_notes: input.notes ?? null, }, ); if (error) throw error; // RPC returns the booking UUID; fetch the full row const bookingId = data as unknown as string; const { data: booking, error: fetchError } = await client .from('bookings') .select('*') .eq('id', bookingId) .single(); if (fetchError) throw fetchError; return booking; }, async updateStatus( bookingId: string, status: string, version?: number, userId?: string, ) { const logger = await getLogger(); logger.info( { name: NAMESPACE, bookingId, status }, 'Updating booking status...', ); // Fetch current booking to get current status // eslint-disable-next-line @typescript-eslint/no-explicit-any -- version column added via migration, not yet in generated types const { data: current, error: fetchError } = await ( client.from('bookings').select('id, status, version') as any ) .eq('id', bookingId) .single(); if (fetchError) throw fetchError; const currentStatus = (current as Record) .status as string; // Validate status transition using the state machine try { validateTransition( currentStatus as Parameters[0], status as Parameters[1], ); } catch { const validTargets = getValidTransitions( currentStatus as Parameters[0], ); throw new InvalidBookingStatusTransitionError( currentStatus, status, validTargets, ); } // Build the update query // eslint-disable-next-line @typescript-eslint/no-explicit-any -- updated_by column added via migration let query = client .from('bookings') .update({ status, ...(userId ? { updated_by: userId } : {}), } as any) .eq('id', bookingId); // Optimistic concurrency control via version column (added by migration, not yet in generated types) if (version !== undefined) { query = (query as any).eq('version', version); } const { data, error } = await query.select('id').single(); if (error) { // If no rows matched, it's a concurrency conflict if (error.code === 'PGRST116') { throw new BookingConcurrencyConflictError(); } throw error; } if (!data) { throw new BookingConcurrencyConflictError(); } }, }; }