Initial commit: Kalendartool (Next.js, Prisma, Docker)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
81
lib/reservations/conflicts.ts
Normal file
81
lib/reservations/conflicts.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Konfliktpruefung fuer Reservierungen (plan.md Abschnitt 6).
|
||||
*
|
||||
* Zwei Zeitraume kollidieren, wenn sie sich ueberlappen:
|
||||
* existing.start < new.end UND new.start < existing.end
|
||||
*
|
||||
* Die Pruefung MUSS serverseitig innerhalb einer Transaktion mit
|
||||
* konsistentem Isolation-Level erfolgen, damit gleichzeitige Buchungen
|
||||
* zweier Benutzer nicht zu einer Doppelbuchung fuehren koennen.
|
||||
*/
|
||||
import type { Reservation } from '@prisma/client';
|
||||
|
||||
/** Fehler, der bei einer Zeitkollision geworfen wird. */
|
||||
export class ReservationConflictError extends Error {
|
||||
constructor() {
|
||||
super('Der gewuenschte Zeitraum ist bereits belegt.');
|
||||
this.name = 'ReservationConflictError';
|
||||
}
|
||||
}
|
||||
|
||||
/** Fehler, wenn der Zeitraum ungueltig ist (Vergangenheit, Start>=Ende). */
|
||||
export class ReservationValidationError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'ReservationValidationError';
|
||||
}
|
||||
}
|
||||
|
||||
/** Prueft rein funktional, ob sich zwei Zeitraume ueberlappen. */
|
||||
export function intervalsOverlap(
|
||||
aStart: Date,
|
||||
aEnd: Date,
|
||||
bStart: Date,
|
||||
bEnd: Date,
|
||||
): boolean {
|
||||
return aStart < bEnd && bStart < aEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Findet kollidierende Reservierungen eines Kalenders im Zeitraum.
|
||||
* Rein funktional und damit gut testbar; die Datenbankabfrage wird
|
||||
* als Parameter injiziert.
|
||||
*/
|
||||
export function findConflicts(
|
||||
existing: Pick<Reservation, 'id' | 'startAt' | 'endAt'>[],
|
||||
newStart: Date,
|
||||
newEnd: Date,
|
||||
ignoreReservationId?: string,
|
||||
): Pick<Reservation, 'id' | 'startAt' | 'endAt'>[] {
|
||||
return existing.filter(
|
||||
(reservation) =>
|
||||
reservation.id !== ignoreReservationId &&
|
||||
intervalsOverlap(reservation.startAt, reservation.endAt, newStart, newEnd),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert, dass Start vor Ende liegt und der Zeitraum nicht in der
|
||||
* Vergangenheit beginnt. Wirft sonst eine aussagekraeftige Fehlermeldung.
|
||||
*/
|
||||
export function validateReservationPeriod(
|
||||
startAt: Date,
|
||||
endAt: Date,
|
||||
now: Date = new Date(),
|
||||
): void {
|
||||
if (Number.isNaN(startAt.getTime()) || Number.isNaN(endAt.getTime())) {
|
||||
throw new ReservationValidationError(
|
||||
'Start- und Endzeit muessen gueltige Zeitpunkte sein.',
|
||||
);
|
||||
}
|
||||
if (startAt >= endAt) {
|
||||
throw new ReservationValidationError(
|
||||
'Die Startzeit muss vor der Endzeit liegen.',
|
||||
);
|
||||
}
|
||||
if (startAt < now) {
|
||||
throw new ReservationValidationError(
|
||||
'Der Zeitraum liegt in der Vergangenheit.',
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user