refactor: remove obsolete member management API module
Some checks failed
Workflow / ʦ TypeScript (pull_request) Failing after 5m57s
Workflow / ⚫️ Test (pull_request) Has been skipped

This commit is contained in:
T. Zehetbauer
2026-04-03 14:08:31 +02:00
parent 124c6a632a
commit 5c5aaabae5
132 changed files with 10107 additions and 3442 deletions

View File

@@ -0,0 +1,157 @@
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();
}
},
};
}

View File

@@ -0,0 +1,57 @@
import 'server-only';
import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '@kit/supabase/database';
export function createGuestService(client: SupabaseClient<Database>) {
return {
async list(accountId: string, search?: string) {
let query = client
.from('guests')
.select('*')
.eq('account_id', accountId)
.order('last_name');
if (search)
query = query.or(
`last_name.ilike.%${search}%,first_name.ilike.%${search}%,email.ilike.%${search}%`,
);
const { data, error } = await query;
if (error) throw error;
return data ?? [];
},
async create(input: {
accountId: string;
firstName: string;
lastName: string;
email?: string;
phone?: string;
city?: string;
}) {
const { data, error } = await client
.from('guests')
.insert({
account_id: input.accountId,
first_name: input.firstName,
last_name: input.lastName,
email: input.email || null,
phone: input.phone || null,
city: input.city || null,
})
.select()
.single();
if (error) throw error;
return data;
},
async getHistory(guestId: string) {
const { data, error } = await client
.from('bookings')
.select('*')
.eq('guest_id', guestId)
.order('check_in', { ascending: false });
if (error) throw error;
return data ?? [];
},
};
}

View File

@@ -0,0 +1,18 @@
import 'server-only';
import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '@kit/supabase/database';
import { createBookingCrudService } from './booking-crud.service';
import { createGuestService } from './guest.service';
import { createRoomService } from './room.service';
export { createBookingCrudService, createGuestService, createRoomService };
export function createBookingServices(client: SupabaseClient<Database>) {
return {
rooms: createRoomService(client),
bookings: createBookingCrudService(client),
guests: createGuestService(client),
};
}

View File

@@ -0,0 +1,69 @@
import 'server-only';
import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '@kit/supabase/database';
export function createRoomService(client: SupabaseClient<Database>) {
return {
async list(accountId: string) {
const { data, error } = await client
.from('rooms')
.select('*')
.eq('account_id', accountId)
.eq('is_active', true)
.order('room_number');
if (error) throw error;
return data ?? [];
},
async getById(roomId: string) {
const { data, error } = await client
.from('rooms')
.select('*')
.eq('id', roomId)
.single();
if (error) throw error;
return data;
},
async checkAvailability(roomId: string, checkIn: string, checkOut: string) {
const { count, error } = await client
.from('bookings')
.select('*', { count: 'exact', head: true })
.eq('room_id', roomId)
.not('status', 'in', '("cancelled","no_show")')
.lt('check_in', checkOut)
.gt('check_out', checkIn);
if (error) throw error;
return (count ?? 0) === 0;
},
async create(input: {
accountId: string;
roomNumber: string;
name?: string;
roomType?: string;
capacity?: number;
floor?: number;
pricePerNight: number;
description?: string;
}) {
const { data, error } = await client
.from('rooms')
.insert({
account_id: input.accountId,
room_number: input.roomNumber,
name: input.name,
room_type: input.roomType ?? 'standard',
capacity: input.capacity ?? 2,
floor: input.floor,
price_per_night: input.pricePerNight,
description: input.description,
})
.select()
.single();
if (error) throw error;
return data;
},
};
}