139 lines
6.1 KiB
TypeScript
139 lines
6.1 KiB
TypeScript
/**
|
|
* 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.' }],
|
|
])
|