feat(reservations): service + validators + exclusivity tests
Adds the berth_reservations service covering the full lifecycle
(pending -> active -> ended/cancelled) with tenant scoping, DB-enforced
exclusivity on the idx_br_active partial unique index, and
client-or-company-member cross-checks for yacht ownership.
- validators: createPending / activate / end / cancel / list schemas
- service: createPending, activate, endReservation, cancel, getById,
listReservations — with narrow 23505/idx_br_active catch that
re-queries the conflicting active reservation
- socket events: berth_reservation:{created,activated,ended,cancelled}
- tests: unit (lifecycle, tenant, membership cross-check),
integration (concurrent-activate ConflictError + re-activate after end)
2026-04-24 12:15:22 +02:00
|
|
|
import { z } from 'zod';
|
2026-05-06 14:56:59 +02:00
|
|
|
import { baseListQuerySchema } from '@/lib/api/list-query';
|
feat(reservations): service + validators + exclusivity tests
Adds the berth_reservations service covering the full lifecycle
(pending -> active -> ended/cancelled) with tenant scoping, DB-enforced
exclusivity on the idx_br_active partial unique index, and
client-or-company-member cross-checks for yacht ownership.
- validators: createPending / activate / end / cancel / list schemas
- service: createPending, activate, endReservation, cancel, getById,
listReservations — with narrow 23505/idx_br_active catch that
re-queries the conflicting active reservation
- socket events: berth_reservation:{created,activated,ended,cancelled}
- tests: unit (lifecycle, tenant, membership cross-check),
integration (concurrent-activate ConflictError + re-activate after end)
2026-04-24 12:15:22 +02:00
|
|
|
|
|
|
|
|
export const RESERVATION_STATUSES = ['pending', 'active', 'ended', 'cancelled'] as const;
|
|
|
|
|
export const TENURE_TYPES = ['permanent', 'fixed_term', 'seasonal'] as const;
|
|
|
|
|
|
|
|
|
|
export const createPendingSchema = z.object({
|
|
|
|
|
berthId: z.string().min(1),
|
|
|
|
|
clientId: z.string().min(1),
|
|
|
|
|
yachtId: z.string().min(1),
|
|
|
|
|
interestId: z.string().optional(),
|
|
|
|
|
startDate: z.coerce.date(),
|
|
|
|
|
tenureType: z.enum(TENURE_TYPES).optional().default('permanent'),
|
|
|
|
|
notes: z.string().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const activateSchema = z.object({
|
|
|
|
|
contractFileId: z.string().optional(),
|
|
|
|
|
effectiveDate: z.coerce.date().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const endReservationSchema = z.object({
|
|
|
|
|
endDate: z.coerce.date(),
|
|
|
|
|
notes: z.string().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const cancelSchema = z.object({
|
|
|
|
|
reason: z.string().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const listReservationsSchema = baseListQuerySchema.extend({
|
|
|
|
|
status: z.enum(RESERVATION_STATUSES).optional(),
|
|
|
|
|
berthId: z.string().optional(),
|
|
|
|
|
clientId: z.string().optional(),
|
|
|
|
|
yachtId: z.string().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export type CreatePendingInput = z.infer<typeof createPendingSchema>;
|
|
|
|
|
export type ActivateInput = z.infer<typeof activateSchema>;
|
|
|
|
|
export type EndReservationInput = z.infer<typeof endReservationSchema>;
|
|
|
|
|
export type CancelInput = z.infer<typeof cancelSchema>;
|
|
|
|
|
export type ListReservationsInput = z.infer<typeof listReservationsSchema>;
|