/** * Domain-specific error classes for the Seminarhof API. * * Convention (see /CLAUDE.md): caught / expected exceptions must throw one of * these defined errors — never a raw `new Error('string')`. Each extends a * Pikku core error and is registered with `addErrors` below so it maps to the * right HTTP status and a stable, client-facing message. * * This module registers its errors as an import side-effect; importing it from * any function that throws is enough to wire the mapping at startup. */ import { BadRequestError, ConflictError, UnauthorizedError, addErrors, } from '@pikku/core/errors' // ─── Auth ──────────────────────────────────────────────────────────── export class InvalidCredentialsError extends UnauthorizedError { constructor(message = 'Invalid email or password.') { super(message) } } // ─── Bookings / scheduling ─────────────────────────────────────────── export class InvalidDateRangeError extends BadRequestError { constructor(message = 'Start date must be on or before the end date.') { super(message) } } export class InvalidBookingTransitionError extends ConflictError { constructor(from: string, to: string) { super(`Cannot move a booking from "${from}" to "${to}".`) } } // ─── Contract lifecycle ────────────────────────────────────────────── export class ContractRequiresReservedError extends ConflictError { constructor(status: string) { super(`A contract can only be sent for a reserved booking (was "${status}").`) } } export class ContractAlreadySentError extends ConflictError { constructor(message = 'A contract has already been sent for this booking.') { super(message) } } export class ContractNotSentError extends ConflictError { constructor(message = 'No contract has been sent for this booking yet.') { super(message) } } export class ContractResponseRequiresReservedError extends ConflictError { constructor(status: string) { super(`A contract response can only be recorded for a reserved booking (was "${status}").`) } } export class DepositRequiresReservedError extends ConflictError { constructor(status: string) { super(`A deposit can only be recorded for a reserved booking (was "${status}").`) } } // ─── Enquiries ─────────────────────────────────────────────────────── export class EnquiryNotPendingError extends ConflictError { constructor(status: string) { super(`This enquiry is "${status}" and can no longer be approved, waitlisted or declined.`) } } export class EnquiryDatesRequiredError extends BadRequestError { constructor(message = 'Start and end dates are required for this action.') { super(message) } } export class ClientWithEmailExistsError extends ConflictError { constructor(email: string) { super(`A client with the email "${email}" already exists — select it instead of creating a new one.`) } } // ─── Booking form / magic link ─────────────────────────────────────── export class InvalidFormLinkError extends UnauthorizedError { constructor(message = 'This booking-form link is invalid or has expired.') { super(message) } } export class BookingFormUnavailableError extends ConflictError { constructor(status: string) { super(`This booking can no longer be completed via the form (status "${status}").`) } } export class ConsentRequiredError extends BadRequestError { constructor(message = 'You must accept the terms and the privacy policy to submit.') { super(message) } } // ─── Invoicing ─────────────────────────────────────────────────────── export class InvoiceAlreadyPaidError extends ConflictError { constructor(message = 'This invoice has already been paid.') { super(message) } } export class InvoiceCancelledError extends ConflictError { constructor(message = 'This invoice has been cancelled.') { super(message) } } addErrors([ [InvalidCredentialsError, { status: 401, message: 'Invalid email or password.' }], [InvalidDateRangeError, { status: 400, message: 'Start date must be on or before the end date.' }], [InvalidBookingTransitionError, { status: 409, message: 'Illegal booking status transition.' }], [ContractRequiresReservedError, { status: 409, message: 'A contract can only be sent for a reserved booking.' }], [ContractAlreadySentError, { status: 409, message: 'A contract has already been sent for this booking.' }], [ContractNotSentError, { status: 409, message: 'No contract has been sent for this booking yet.' }], [ContractResponseRequiresReservedError, { status: 409, message: 'A contract response can only be recorded for a reserved booking.' }], [DepositRequiresReservedError, { status: 409, message: 'A deposit can only be recorded for a reserved booking.' }], [EnquiryNotPendingError, { status: 409, message: 'This enquiry can no longer be acted on.' }], [EnquiryDatesRequiredError, { status: 400, message: 'Start and end dates are required for this action.' }], [ClientWithEmailExistsError, { status: 409, message: 'A client with this email already exists.' }], [InvalidFormLinkError, { status: 401, message: 'This booking-form link is invalid or has expired.' }], [BookingFormUnavailableError, { status: 409, message: 'This booking can no longer be completed via the form.' }], [ConsentRequiredError, { status: 400, message: 'You must accept the terms and the privacy policy to submit.' }], [InvoiceAlreadyPaidError, { status: 409, message: 'This invoice has already been paid.' }], [InvoiceCancelledError, { status: 409, message: 'This invoice has been cancelled.' }], ])