Initial commit

This commit is contained in:
2026-06-01 16:48:21 +02:00
commit 53d157535a
159 changed files with 13641 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
"""Abstract repository contracts.
`Repository[T]` is the plain CRUD base. Use it for simple entities like
VacationDays that have no GDPR deletion requirements.
`SoftDeleteRepository[T]` extends it for entities that need an audit trail
(soft-delete) plus a GDPR hard delete. Use it for PersonalData and any other
entity that directly holds PII. The `list()` contract on soft-deletable
entities should always filter `WHERE deleted_at IS NULL` by default.
Dependency direction: routers/services depend on these interfaces only —
never on a concrete DB class.
"""
from abc import ABC, abstractmethod
from typing import Generic, TypeVar
from uuid import UUID
T = TypeVar("T")
class Repository(ABC, Generic[T]):
@abstractmethod
async def create(self, item: T) -> T:
"""Persist a new item and return it."""
@abstractmethod
async def get_by_id(self, id: UUID) -> T | None:
"""Return the item or None if it does not exist."""
@abstractmethod
async def update(self, id: UUID, item: T) -> T:
"""Update an existing item. Raise NotFoundError if missing."""
@abstractmethod
async def delete(self, id: UUID) -> None:
"""Remove an item. Raise NotFoundError if missing."""
@abstractmethod
async def list(self, **filters: object) -> list[T]:
"""Return items matching the given filters."""
class SoftDeleteRepository(Repository[T], Generic[T]):
"""Extended contract for GDPR-sensitive entities.
`delete()` on these entities sets `deleted_at` (soft delete for audit trail);
`hard_delete()` removes the row entirely (GDPR erasure). Implementations of
`list()` must filter `WHERE deleted_at IS NULL` unless `include_deleted=True`
is passed.
"""
@abstractmethod
async def soft_delete(self, id: UUID) -> None:
"""Set deleted_at to now. Raise NotFoundError if missing."""
@abstractmethod
async def hard_delete(self, id: UUID) -> None:
"""Permanently remove the row (GDPR erasure). Raise NotFoundError if missing."""
async def delete(self, id: UUID) -> None:
"""Default: calls soft_delete. Override only if hard delete is the right default."""
await self.soft_delete(id)
@@ -0,0 +1,17 @@
"""Abstract repository interface for BreakPeriod.
Adds entity-specific queries on top of the generic CRUD contract. Concrete
implementations live in app/repositories_impl/<backend>/.
"""
from abc import abstractmethod
from uuid import UUID
from app.entities.break_period import BreakPeriod
from app.repositories.base import Repository
class BreakPeriodRepository(Repository[BreakPeriod]):
@abstractmethod
async def get_by_workspace(self, workspace_id: UUID) -> list[BreakPeriod]:
"""Return all break periods for a workspace."""
@@ -0,0 +1,13 @@
"""Abstract repository for Child entities."""
from abc import abstractmethod
from uuid import UUID
from app.entities.child import Child
from app.repositories.base import Repository
class ChildRepository(Repository[Child]):
@abstractmethod
async def list_by_workspace(self, workspace_id: UUID) -> list[Child]:
"""Return all children in a workspace."""
@@ -0,0 +1,21 @@
"""Abstract repository for ParentalLeaveFetch snapshots."""
from abc import abstractmethod
from uuid import UUID
from app.entities.parental_leave import ParentalLeaveFetch
from app.repositories.base import Repository
class ParentalLeaveRepository(Repository[ParentalLeaveFetch]):
@abstractmethod
async def get_latest_by_parent(
self, personal_data_uuid: UUID, child_id: UUID
) -> ParentalLeaveFetch | None:
"""Return the most recently fetched snapshot for a parent+child pair."""
@abstractmethod
async def list_history(
self, personal_data_uuid: UUID, child_id: UUID
) -> list[ParentalLeaveFetch]:
"""Return all snapshots for a parent+child pair, newest first."""
@@ -0,0 +1,17 @@
"""Abstract repository for PersonalData.
Extends SoftDeleteRepository because PersonalData directly holds PII and must
support GDPR erasure.
"""
from abc import abstractmethod
from uuid import UUID
from app.entities.personal_data import PersonalData
from app.repositories.base import SoftDeleteRepository
class PersonalDataRepository(SoftDeleteRepository[PersonalData]):
@abstractmethod
async def list_by_workspace(self, workspace_id: UUID) -> list[PersonalData]:
"""Return all non-deleted profiles in a workspace (up to 2)."""
@@ -0,0 +1,35 @@
"""Abstract repository for PlannedLeaveDay calendar events."""
from abc import abstractmethod
from datetime import date
from uuid import UUID
from app.entities.planned_leave import PlannedLeaveDay
from app.repositories.base import Repository
class PlannedLeaveRepository(Repository[PlannedLeaveDay]):
@abstractmethod
async def list_by_workspace_child(
self,
workspace_id: UUID,
child_id: UUID,
from_date: date | None = None,
to_date: date | None = None,
) -> list[PlannedLeaveDay]:
"""Return planned days for a workspace+child, optionally within a date range."""
@abstractmethod
async def count_by_parent_type(
self,
personal_data_uuid: UUID,
child_id: UUID,
day_type: str,
from_date: date | None = None,
to_date: date | None = None,
) -> int:
"""Count planned days of a given type for a parent+child pair."""
@abstractmethod
async def count_dubbeldagar(self, child_id: UUID, from_date: date | None = None) -> int:
"""Count dubbeldagar already planned for a child (after from_date)."""
@@ -0,0 +1,12 @@
"""Abstract repository for User (login accounts)."""
from abc import abstractmethod
from app.entities.user import User
from app.repositories.base import Repository
class UserRepository(Repository[User]):
@abstractmethod
async def get_by_email(self, email: str) -> User | None:
"""Return the user with this email, or None if not found."""
@@ -0,0 +1,17 @@
"""Abstract repository interface for VacationDays.
Adds entity-specific queries on top of the generic CRUD contract. Concrete
implementations live in app/repositories_impl/<backend>/.
"""
from abc import abstractmethod
from uuid import UUID
from app.entities.vacation_days import VacationDays
from app.repositories.base import Repository
class VacationDaysRepository(Repository[VacationDays]):
@abstractmethod
async def get_by_parent_year(self, personal_data_uuid: UUID, year: int) -> VacationDays | None:
"""Return the entry for a given parent + year (UNIQUE), or None."""
@@ -0,0 +1,24 @@
"""Abstract repository for Workspace and WorkspaceInvitation."""
from abc import abstractmethod
from uuid import UUID
from app.entities.workspace import Workspace
from app.entities.workspace_invitation import WorkspaceInvitation
from app.repositories.base import Repository
class WorkspaceRepository(Repository[Workspace]):
pass # CRUD is sufficient; no entity-specific queries needed yet
class WorkspaceInvitationRepository(Repository[WorkspaceInvitation]):
@abstractmethod
async def get_by_token_hash(self, token_hash: str) -> WorkspaceInvitation | None:
"""Look up a pending invitation by the hash of the raw token."""
@abstractmethod
async def list_by_workspace(
self, workspace_id: UUID, status: str | None = None
) -> list[WorkspaceInvitation]:
"""Return invitations for a workspace, optionally filtered by status."""