Files
myeasycms-v2/packages/features/booking-management/src/server/services/booking-crud.service.ts
T. Zehetbauer 5c5aaabae5
Some checks failed
Workflow / ʦ TypeScript (pull_request) Failing after 5m57s
Workflow / ⚫️ Test (pull_request) Has been skipped
refactor: remove obsolete member management API module
2026-04-03 14:08:31 +02:00

158 lines
4.8 KiB
TypeScript

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<Database>) {
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<string, unknown>)
.status as string;
// Validate status transition using the state machine
try {
validateTransition(
currentStatus as Parameters<typeof validateTransition>[0],
status as Parameters<typeof validateTransition>[1],
);
} catch {
const validTargets = getValidTransitions(
currentStatus as Parameters<typeof getValidTransitions>[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();
}
},
};
}