BE-CAL-API-001: Implement single slot creation API

- Add TimeSlotCreate, TimeSlotResponse, TimeSlotCreateResponse schemas
- Add SlotConflictItem, SlotTypeEnum, EventTypeEnum, SlotStatusEnum to schemas
- Add POST /calendar/slots endpoint with overlap detection and workload warnings
- Add _slot_to_response helper for ORM -> schema conversion
This commit is contained in:
zhi
2026-03-31 05:45:58 +00:00
parent 4f0e933de3
commit 751b3bc574
2 changed files with 191 additions and 3 deletions

View File

@@ -1,12 +1,15 @@
"""Calendar-related Pydantic schemas.
BE-CAL-004: MinimumWorkload read/write schemas.
BE-CAL-API-001: TimeSlot create / response schemas.
"""
from __future__ import annotations
from pydantic import BaseModel, Field, model_validator
from typing import Optional
from datetime import date, time, datetime
from enum import Enum
from pydantic import BaseModel, Field, model_validator, field_validator
from typing import Any, Optional
# ---------------------------------------------------------------------------
@@ -61,3 +64,91 @@ class WorkloadWarningItem(BaseModel):
minimum_minutes: int = Field(..., ge=0, description="Configured minimum threshold")
shortfall_minutes: int = Field(..., ge=0, description="How many minutes below threshold")
message: str = Field(..., description="Human-readable warning")
# ---------------------------------------------------------------------------
# TimeSlot enums (mirror DB enums for schema layer)
# ---------------------------------------------------------------------------
class SlotTypeEnum(str, Enum):
WORK = "work"
ON_CALL = "on_call"
ENTERTAINMENT = "entertainment"
SYSTEM = "system"
class EventTypeEnum(str, Enum):
JOB = "job"
ENTERTAINMENT = "entertainment"
SYSTEM_EVENT = "system_event"
class SlotStatusEnum(str, Enum):
NOT_STARTED = "not_started"
ONGOING = "ongoing"
DEFERRED = "deferred"
SKIPPED = "skipped"
PAUSED = "paused"
FINISHED = "finished"
ABORTED = "aborted"
# ---------------------------------------------------------------------------
# TimeSlot create / response (BE-CAL-API-001)
# ---------------------------------------------------------------------------
class TimeSlotCreate(BaseModel):
"""Request body for creating a single calendar slot."""
date: Optional[date] = Field(None, description="Target date (defaults to today)")
slot_type: SlotTypeEnum = Field(..., description="work | on_call | entertainment | system")
scheduled_at: time = Field(..., description="Planned start time HH:MM (00:00-23:00)")
estimated_duration: int = Field(..., ge=1, le=50, description="Duration in minutes (1-50)")
event_type: Optional[EventTypeEnum] = Field(None, description="job | entertainment | system_event")
event_data: Optional[dict[str, Any]] = Field(None, description="Event details JSON")
priority: int = Field(0, ge=0, le=99, description="Priority 0-99")
@field_validator("scheduled_at")
@classmethod
def _validate_scheduled_at(cls, v: time) -> time:
if v.hour > 23:
raise ValueError("scheduled_at hour must be between 00 and 23")
return v
class SlotConflictItem(BaseModel):
"""Describes a single overlap conflict."""
conflicting_slot_id: Optional[int] = None
conflicting_virtual_id: Optional[str] = None
scheduled_at: str
estimated_duration: int
slot_type: str
message: str
class TimeSlotResponse(BaseModel):
"""Response for a single TimeSlot."""
id: int
user_id: int
date: date
slot_type: str
estimated_duration: int
scheduled_at: str # HH:MM:SS ISO format
started_at: Optional[str] = None
attended: bool
actual_duration: Optional[int] = None
event_type: Optional[str] = None
event_data: Optional[dict[str, Any]] = None
priority: int
status: str
plan_id: Optional[int] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
class TimeSlotCreateResponse(BaseModel):
"""Response after creating a slot — includes the slot and any warnings."""
slot: TimeSlotResponse
warnings: list[WorkloadWarningItem] = Field(default_factory=list)