8ebfcb9462
Settings is gaining a partner display, which hides the add-form once a
partner exists. Without these routes a typo'd partner name would be
permanently unfixable, since no endpoint could change or drop one.
PATCH /workspace/{id}/parents/{uuid} renames; DELETE soft-deletes.
Soft rather than hard delete because PersonalData holds PII and the
repository is a SoftDeleteRepository specifically so erasure stays
auditable.
Removal refuses (422) when the member still has leave wishes, planned
leave, parental-leave rows or vacation days, naming the blockers, so a
removal can never orphan plan data. It also refuses for the caller's own
profile, and for a member who has their own login -- the invite/redeem
flow re-points a real user's personal_data into the inviter's workspace,
so soft-deleting that row would silently break their login.
Errors carry message_key + data rather than prose, per the no-Swedish-on
-the-wire rule; NotFoundError/ValidationError gained the same optional
shape DubbeldagError already had, defaulting empty so existing call
sites are unaffected.
Also fixes seed_demo.py constructing WorkspaceService with a stale
3-arg signature, which would have crashed at runtime -- invisible to
pytest since it is a standalone script.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
93 lines
2.9 KiB
Python
93 lines
2.9 KiB
Python
"""Domain error types.
|
|
|
|
Use these instead of bare exceptions so services/routers can map them to
|
|
meaningful HTTP responses. Never swallow errors silently (see ARCHITECTURE.md §10).
|
|
"""
|
|
|
|
|
|
class DomainError(Exception):
|
|
"""Base class for all application-domain errors."""
|
|
|
|
|
|
class NotFoundError(DomainError):
|
|
"""Raised when an entity cannot be found by its identifier.
|
|
|
|
May optionally carry a `message_key` + `data` pair (§3.5) so a router can
|
|
surface a structured, i18n-ready refusal instead of a hardcoded string —
|
|
see `DubbeldagError` for the established shape. Existing call sites that
|
|
only pass `message` are unaffected: `message_key` defaults to `""`.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
message_key: str = "",
|
|
data: dict[str, object] | None = None,
|
|
) -> None:
|
|
super().__init__(message)
|
|
self.message_key = message_key
|
|
self.data: dict[str, object] = data if data is not None else {}
|
|
|
|
|
|
class ValidationError(DomainError):
|
|
"""Raised when input fails a business rule (not just schema validation).
|
|
|
|
May optionally carry a `message_key` + `data` pair (§3.5); see
|
|
`NotFoundError` above for the same shape and rationale.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
message_key: str = "",
|
|
data: dict[str, object] | None = None,
|
|
) -> None:
|
|
super().__init__(message)
|
|
self.message_key = message_key
|
|
self.data: dict[str, object] = data if data is not None else {}
|
|
|
|
|
|
class InsufficientDaysError(DomainError):
|
|
"""Raised when a parent doesn't have enough remaining days to plan."""
|
|
|
|
|
|
class InvalidDayTypeError(ValidationError):
|
|
"""Raised when a planned-day type is not recognized."""
|
|
|
|
|
|
class DubbeldagError(ValidationError):
|
|
"""Raised when a dubbeldag breaks the 15-month window or the 60-day quota.
|
|
|
|
Carries an optional `message_key` + `data` pair (§3.5) alongside the
|
|
human-readable `message` (kept for anything that only logs/prints the
|
|
exception) so callers that need to surface this to the UI — see
|
|
`PlanningService._validate_dubbeldagar_batch` — can build an i18n-ready
|
|
`Rejection` instead of leaking this English string past the backend.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
message_key: str = "",
|
|
data: dict[str, object] | None = None,
|
|
) -> None:
|
|
super().__init__(message)
|
|
self.message_key = message_key
|
|
self.data: dict[str, object] = data if data is not None else {}
|
|
|
|
|
|
class AuthError(DomainError):
|
|
"""Raised when credentials are invalid or an auth operation fails."""
|
|
|
|
|
|
class UnauthorizedError(DomainError):
|
|
"""Raised when the caller lacks permission to perform an action."""
|
|
|
|
|
|
class ConflictError(DomainError):
|
|
"""Raised when a resource already exists (e.g. duplicate email on register)."""
|
|
|
|
|
|
class InvitationError(ValidationError):
|
|
"""Raised when an invitation token is invalid, expired, or already redeemed."""
|