BE-CAL-API-006: implement plan-edit and plan-cancel API endpoints
- PATCH /calendar/plans/{plan_id}: edit a recurring schedule plan
- Validates period-parameter hierarchy after merge
- Rejects edits to inactive (cancelled) plans
- Detaches future materialized slots so they keep old data
- Past materialized slots remain untouched
- POST /calendar/plans/{plan_id}/cancel: cancel (soft-delete) a plan
- Sets is_active=False
- Detaches future materialized slots (plan_id -> NULL)
- Preserves past materialized slots, returns their IDs
- Added SchedulePlanEdit and SchedulePlanCancelResponse schemas
This commit is contained in:
@@ -337,3 +337,57 @@ class SchedulePlanResponse(BaseModel):
|
||||
class SchedulePlanListResponse(BaseModel):
|
||||
"""Response for listing schedule plans."""
|
||||
plans: list[SchedulePlanResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SchedulePlan edit / cancel (BE-CAL-API-006)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class SchedulePlanEdit(BaseModel):
|
||||
"""Request body for editing a recurring schedule plan.
|
||||
|
||||
All fields are optional — only provided fields are updated.
|
||||
Period-parameter hierarchy (on_month → on_week → on_day) is
|
||||
validated after merging with existing plan values.
|
||||
"""
|
||||
slot_type: Optional[SlotTypeEnum] = Field(None, description="New slot type")
|
||||
estimated_duration: Optional[int] = Field(None, ge=1, le=50, description="New duration in minutes (1-50)")
|
||||
at_time: Optional[time] = Field(None, description="New daily time (HH:MM)")
|
||||
on_day: Optional[DayOfWeekEnum] = Field(None, description="New day of week (sun-sat), use 'clear' param to remove")
|
||||
on_week: Optional[int] = Field(None, ge=1, le=4, description="New week of month (1-4), use 'clear' param to remove")
|
||||
on_month: Optional[MonthOfYearEnum] = Field(None, description="New month (jan-dec), use 'clear' param to remove")
|
||||
event_type: Optional[EventTypeEnum] = Field(None, description="New event type")
|
||||
event_data: Optional[dict[str, Any]] = Field(None, description="New event details JSON")
|
||||
clear_on_day: bool = Field(False, description="Clear on_day (set to NULL)")
|
||||
clear_on_week: bool = Field(False, description="Clear on_week (set to NULL)")
|
||||
clear_on_month: bool = Field(False, description="Clear on_month (set to NULL)")
|
||||
|
||||
@field_validator("at_time")
|
||||
@classmethod
|
||||
def _validate_at_time(cls, v: Optional[time]) -> Optional[time]:
|
||||
if v is not None and v.hour > 23:
|
||||
raise ValueError("at_time hour must be between 00 and 23")
|
||||
return v
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _at_least_one_field(self) -> "SchedulePlanEdit":
|
||||
"""Ensure at least one editable field or clear flag is provided."""
|
||||
has_value = any(
|
||||
getattr(self, f) is not None
|
||||
for f in ("slot_type", "estimated_duration", "at_time", "on_day",
|
||||
"on_week", "on_month", "event_type", "event_data")
|
||||
)
|
||||
has_clear = self.clear_on_day or self.clear_on_week or self.clear_on_month
|
||||
if not has_value and not has_clear:
|
||||
raise ValueError("At least one field must be provided for edit")
|
||||
return self
|
||||
|
||||
|
||||
class SchedulePlanCancelResponse(BaseModel):
|
||||
"""Response after cancelling a plan."""
|
||||
plan: SchedulePlanResponse
|
||||
message: str = Field("Plan cancelled successfully", description="Human-readable result")
|
||||
preserved_past_slot_ids: list[int] = Field(
|
||||
default_factory=list,
|
||||
description="IDs of past materialized slots that were NOT affected",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user