chore: seminarhof customer project

This commit is contained in:
e2e
2026-07-11 07:59:25 +02:00
commit 465dd7a41a
188 changed files with 20779 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
/**
* 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.' }],
])