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
View File
+58
View File
@@ -0,0 +1,58 @@
"""Pydantic response schemas for analytics endpoints."""
from datetime import date
from uuid import UUID
from pydantic import BaseModel
from app.services.analytics_service import Age4Projection, WorkspaceOverview
class Age4ProjectionResponse(BaseModel):
personal_data_uuid: UUID
child_id: UUID
birth_date: date
fourth_birthday: date
total_days: int
days_used: int
planned_days_before_b4: int
projected_left_at_b4: int
savable_after_b4: int
expiring_at_b4: int
@classmethod
def from_entity(cls, p: Age4Projection) -> "Age4ProjectionResponse":
return cls(
personal_data_uuid=p.personal_data_uuid,
child_id=p.child_id,
birth_date=p.birth_date,
fourth_birthday=p.fourth_birthday,
total_days=p.total_days,
days_used=p.days_used,
planned_days_before_b4=p.planned_days_before_b4,
projected_left_at_b4=p.projected_left_at_b4,
savable_after_b4=p.savable_after_b4,
expiring_at_b4=p.expiring_at_b4,
)
class WorkspaceOverviewResponse(BaseModel):
workspace_id: UUID
child_id: UUID
parent_projections: list[Age4ProjectionResponse]
combined_days_used: int
combined_days_left: int
combined_expiring_at_b4: int
@classmethod
def from_entity(cls, overview: WorkspaceOverview) -> "WorkspaceOverviewResponse":
return cls(
workspace_id=overview.workspace_id,
child_id=overview.child_id,
parent_projections=[
Age4ProjectionResponse.from_entity(p) for p in overview.parent_projections
],
combined_days_used=overview.combined_days_used,
combined_days_left=overview.combined_days_left,
combined_expiring_at_b4=overview.combined_expiring_at_b4,
)
+27
View File
@@ -0,0 +1,27 @@
"""Pydantic schemas for auth endpoints."""
from pydantic import BaseModel, EmailStr, Field
class RegisterRequest(BaseModel):
email: EmailStr
password: str = Field(min_length=8)
display_name: str = Field(min_length=1, max_length=100)
class LoginRequest(BaseModel):
email: EmailStr
password: str
class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"
expires_in: int # seconds until expiry
class UserMeResponse(BaseModel):
id: str
email: str
display_name: str
workspace_id: str | None
+53
View File
@@ -0,0 +1,53 @@
"""API schemas (Pydantic) for BreakPeriod.
Schemas validate request bodies and shape responses. Separate from domain entities
so the wire format can evolve independently.
"""
from datetime import date, datetime
from uuid import UUID
from pydantic import BaseModel, Field
from app.entities.break_period import BreakPeriod
class BreakPeriodCreate(BaseModel):
workspace_id: UUID
name: str = Field(min_length=1, max_length=255)
start_date: date
end_date: date
target_weeks: int = Field(ge=1, le=52)
class BreakPeriodUpdate(BaseModel):
"""Full replacement — all fields required (PUT semantics). Partial updates are not supported."""
name: str = Field(min_length=1, max_length=255)
start_date: date
end_date: date
target_weeks: int = Field(ge=1, le=52)
class BreakPeriodResponse(BaseModel):
id: UUID
workspace_id: UUID
name: str
start_date: date
end_date: date
target_weeks: int
created_at: datetime | None
updated_at: datetime | None
@classmethod
def from_entity(cls, entity: BreakPeriod) -> "BreakPeriodResponse":
return cls(
id=entity.id,
workspace_id=entity.workspace_id,
name=entity.name,
start_date=entity.start_date,
end_date=entity.end_date,
target_weeks=entity.target_weeks,
created_at=entity.created_at,
updated_at=entity.updated_at,
)
+69
View File
@@ -0,0 +1,69 @@
"""API schemas (Pydantic) for Child.
Schemas validate request bodies and shape responses. Separate from domain entities
so the wire format can evolve independently.
"""
from datetime import date, datetime
from uuid import UUID
from pydantic import BaseModel, Field, field_validator
from app.entities.child import Child
class ChildCreate(BaseModel):
workspace_id: UUID
name: str = Field(min_length=1, max_length=255)
birth_date: date
preschool_start_date: date | None = None
@field_validator("preschool_start_date")
@classmethod
def validate_preschool_date(cls, v: date | None, info) -> date | None:
"""Preschool start date must be after birth date if both provided."""
if v is None:
return v
birth_date = info.data.get("birth_date")
if birth_date and v <= birth_date:
raise ValueError("preschool_start_date must be after birth_date")
return v
class ChildUpdate(BaseModel):
"""Partial update — all fields optional."""
name: str | None = Field(None, min_length=1, max_length=255)
birth_date: date | None = None
preschool_start_date: date | None = None
@field_validator("preschool_start_date")
@classmethod
def validate_preschool_date(cls, v: date | None, info) -> date | None:
"""Preschool start date must be after birth date if both provided."""
if v is None:
return v
birth_date = info.data.get("birth_date")
if birth_date and v <= birth_date:
raise ValueError("preschool_start_date must be after birth_date")
return v
class ChildResponse(BaseModel):
id: UUID
workspace_id: UUID
name: str
birth_date: date
preschool_start_date: date | None
created_at: datetime | None
@classmethod
def from_entity(cls, entity: Child) -> "ChildResponse":
return cls(
id=entity.id,
workspace_id=entity.workspace_id,
name=entity.name,
birth_date=entity.birth_date,
preschool_start_date=entity.preschool_start_date,
created_at=entity.created_at,
)
+84
View File
@@ -0,0 +1,84 @@
"""Pydantic schemas for parental leave fetch endpoints."""
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel, Field
from app.entities.parental_leave import ParentalLeaveFetch, TierData
class TierDataResponse(BaseModel):
sjukpenning_total: int
sjukpenning_used: int
sjukpenning_left: int
lagsta_niva_total: int
lagsta_niva_used: int
lagsta_niva_left: int
@classmethod
def from_entity(cls, td: TierData) -> "TierDataResponse":
return cls(
sjukpenning_total=td.sjukpenning_total,
sjukpenning_used=td.sjukpenning_used,
sjukpenning_left=td.sjukpenning_left,
lagsta_niva_total=td.lagsta_niva_total,
lagsta_niva_used=td.lagsta_niva_used,
lagsta_niva_left=td.lagsta_niva_left,
)
class ManualFetchRequest(BaseModel):
personal_data_uuid: UUID
workspace_id: UUID
child_id: UUID
total_days: int = Field(ge=0, le=480)
days_used: int = Field(ge=0, le=480)
reserved_days_total: int = Field(default=90, ge=0, le=240)
reserved_days_left: int = Field(ge=0, le=240)
transferable_days_left: int = Field(ge=0, le=150)
dubbeldagar_left: int = Field(default=60, ge=0, le=60)
sjukpenning_total: int = Field(default=390, ge=0, le=390)
sjukpenning_used: int = Field(ge=0, le=390)
lagsta_niva_total: int = Field(default=90, ge=0, le=90)
lagsta_niva_used: int = Field(ge=0, le=90)
class ParentalLeaveFetchResponse(BaseModel):
id: UUID
personal_data_uuid: UUID
workspace_id: UUID
child_id: UUID
source: str
fetched_at: datetime
total_days: int
days_used: int
days_left: int
reserved_days_total: int
reserved_days_left: int
transferable_days_left: int
dubbeldagar_left: int
tier_data: TierDataResponse
created_at: datetime | None
updated_at: datetime | None
@classmethod
def from_entity(cls, fetch: ParentalLeaveFetch) -> "ParentalLeaveFetchResponse":
return cls(
id=fetch.id,
personal_data_uuid=fetch.personal_data_uuid,
workspace_id=fetch.workspace_id,
child_id=fetch.child_id,
source=fetch.source,
fetched_at=fetch.fetched_at,
total_days=fetch.total_days,
days_used=fetch.days_used,
days_left=fetch.days_left,
reserved_days_total=fetch.reserved_days_total,
reserved_days_left=fetch.reserved_days_left,
transferable_days_left=fetch.transferable_days_left,
dubbeldagar_left=fetch.dubbeldagar_left,
tier_data=TierDataResponse.from_entity(fetch.tier_data),
created_at=fetch.created_at,
updated_at=fetch.updated_at,
)
+41
View File
@@ -0,0 +1,41 @@
"""API schemas (Pydantic) for PersonalData.
Schemas validate request bodies and shape responses. Separate from domain entities
so the wire format can evolve independently.
"""
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel, Field
from app.entities.personal_data import PersonalData
class PersonalDataUpdate(BaseModel):
"""Partial update — all fields optional."""
salary_type: str | None = Field(None, pattern="^(monthly|hourly)$")
employment_status: str | None = Field(None, pattern="^(employed|jobseeking|none)$")
class PersonalDataResponse(BaseModel):
uuid: UUID
workspace_id: UUID
name: str
salary_type: str
employment_status: str
created_at: datetime | None
deleted_at: datetime | None
@classmethod
def from_entity(cls, entity: PersonalData) -> "PersonalDataResponse":
return cls(
uuid=entity.uuid,
workspace_id=entity.workspace_id,
name=entity.name,
salary_type=entity.salary_type,
employment_status=entity.employment_status,
created_at=entity.created_at,
deleted_at=entity.deleted_at,
)
+54
View File
@@ -0,0 +1,54 @@
"""API schemas (Pydantic) for PlannedLeaveDay.
Schemas validate request bodies and shape responses. Separate from domain entities
so the wire format can evolve independently.
"""
from datetime import date, datetime
from uuid import UUID
from pydantic import BaseModel, Field
from app.entities.planned_leave import PlannedLeaveDay
class PlannedLeaveDayCreate(BaseModel):
workspace_id: UUID
child_id: UUID
parent_personal_data_uuid: UUID
date: date
day_type: str = Field(pattern="^(sjukpenning|lagsta|vacation)$")
is_dubbeldag: bool = False
class PlannedLeaveDayUpdate(BaseModel):
"""Partial update — all fields optional."""
day_type: str | None = Field(None, pattern="^(sjukpenning|lagsta|vacation)$")
is_dubbeldag: bool | None = None
class PlannedLeaveDayResponse(BaseModel):
id: UUID
workspace_id: UUID
child_id: UUID
parent_personal_data_uuid: UUID
date: date
day_type: str
is_dubbeldag: bool
created_at: datetime | None
updated_at: datetime | None
@classmethod
def from_entity(cls, entity: PlannedLeaveDay) -> "PlannedLeaveDayResponse":
return cls(
id=entity.id,
workspace_id=entity.workspace_id,
child_id=entity.child_id,
parent_personal_data_uuid=entity.parent_personal_data_uuid,
date=entity.date,
day_type=entity.day_type,
is_dubbeldag=entity.is_dubbeldag,
created_at=entity.created_at,
updated_at=entity.updated_at,
)
+50
View File
@@ -0,0 +1,50 @@
"""API schemas (Pydantic) for VacationDays.
Schemas are the API boundary — they validate request bodies and shape responses.
Keep them separate from domain entities so the wire format can evolve
independently. This is the REFERENCE for other entities' schemas.
"""
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel, Field
from app.entities.vacation_days import VacationDays
class VacationDaysCreate(BaseModel):
personal_data_uuid: UUID
workspace_id: UUID
year: int = Field(ge=1900, le=2100)
total_days: int = Field(ge=0, le=366)
saved_days_in: int = Field(default=0, ge=0, le=366)
class VacationDaysUpdate(BaseModel):
total_days: int = Field(ge=0, le=366)
saved_days_in: int = Field(default=0, ge=0, le=366)
class VacationDaysResponse(BaseModel):
id: UUID
personal_data_uuid: UUID
workspace_id: UUID
year: int
total_days: int
saved_days_in: int
created_at: datetime | None
updated_at: datetime | None
@classmethod
def from_entity(cls, entity: VacationDays) -> "VacationDaysResponse":
return cls(
id=entity.id,
personal_data_uuid=entity.personal_data_uuid,
workspace_id=entity.workspace_id,
year=entity.year,
total_days=entity.total_days,
saved_days_in=entity.saved_days_in,
created_at=entity.created_at,
updated_at=entity.updated_at,
)
+68
View File
@@ -0,0 +1,68 @@
"""Pydantic schemas for workspace and invitation endpoints."""
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel, Field
from app.entities.workspace import Workspace
from app.entities.workspace_invitation import WorkspaceInvitation
class WorkspaceCreate(BaseModel):
name: str = Field(min_length=1, max_length=100)
class WorkspaceUpdate(BaseModel):
name: str | None = Field(default=None, min_length=1, max_length=100)
planning_goal: str | None = None
class WorkspaceResponse(BaseModel):
id: UUID
name: str
planning_goal: str
created_at: datetime | None
@classmethod
def from_entity(cls, ws: Workspace) -> "WorkspaceResponse":
return cls(id=ws.id, name=ws.name, planning_goal=ws.planning_goal, created_at=ws.created_at)
class PersonalDataResponse(BaseModel):
uuid: UUID
workspace_id: UUID
name: str
salary_type: str
employment_status: str
created_at: datetime | None
class PersonalDataCreate(BaseModel):
display_name: str = Field(min_length=1, max_length=100)
class InviteResponse(BaseModel):
token: str # raw one-time token — share out-of-band; not stored on backend
class RedeemInviteRequest(BaseModel):
token: str
class InvitationResponse(BaseModel):
id: UUID
workspace_id: UUID
status: str
expires_at: datetime
created_at: datetime
@classmethod
def from_entity(cls, inv: WorkspaceInvitation) -> "InvitationResponse":
return cls(
id=inv.id,
workspace_id=inv.workspace_id,
status=inv.status,
expires_at=inv.expires_at,
created_at=inv.created_at,
)