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)
This commit is contained in:
Matt Ciaccio
2026-04-24 12:15:22 +02:00
parent 15a79e7990
commit b1133c4e87
6 changed files with 995 additions and 0 deletions

View File

@@ -8,6 +8,7 @@ import { createFieldSchema, updateFieldSchema } from '@/lib/validators/custom-fi
import { createYachtSchema, transferOwnershipSchema } from '@/lib/validators/yachts';
import { createCompanySchema } from '@/lib/validators/companies';
import { addMembershipSchema } from '@/lib/validators/company-memberships';
import { createPendingSchema } from '@/lib/validators/reservations';
// ─── Client schemas ───────────────────────────────────────────────────────────
@@ -483,3 +484,49 @@ describe('addMembershipSchema', () => {
expect(result.success).toBe(true);
});
});
// ─── Reservation schemas ─────────────────────────────────────────────────────
describe('createPendingSchema', () => {
const validInput = {
berthId: 'berth-1',
clientId: 'client-1',
yachtId: 'yacht-1',
startDate: '2026-05-01',
};
it('rejects missing berthId', () => {
const result = createPendingSchema.safeParse({
clientId: validInput.clientId,
yachtId: validInput.yachtId,
startDate: validInput.startDate,
});
expect(result.success).toBe(false);
});
it('rejects missing clientId', () => {
const result = createPendingSchema.safeParse({
berthId: validInput.berthId,
yachtId: validInput.yachtId,
startDate: validInput.startDate,
});
expect(result.success).toBe(false);
});
it('rejects missing yachtId', () => {
const result = createPendingSchema.safeParse({
berthId: validInput.berthId,
clientId: validInput.clientId,
startDate: validInput.startDate,
});
expect(result.success).toBe(false);
});
it('accepts minimal valid input with default tenureType', () => {
const result = createPendingSchema.safeParse(validInput);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.tenureType).toBe('permanent');
}
});
});