TICKET-002: Targets CRUD with single-active-target invariant
- POST auto-closes previous target in one transaction - GET /targets (ordered), GET /targets/current (404 when none) - PUT with invariant enforcement (409 on double-active attempt) - Service-layer historical target lookup for TICKET-004 - Full suite green (62 passed)
This commit is contained in:
@@ -117,6 +117,36 @@ class LogEntryRead(BaseModel):
|
||||
# ── Targets ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TargetCreate(BaseModel):
|
||||
"""Schema for POST /api/targets. end_date is auto-managed by the service."""
|
||||
|
||||
start_date: date # client-supplied YYYY-MM-DD (spec §8.1 rule 8)
|
||||
calories: int = Field(gt=0)
|
||||
protein_g: float | None = Field(default=None, ge=0)
|
||||
carbs_g: float | None = Field(default=None, ge=0)
|
||||
fat_g: float | None = Field(default=None, ge=0)
|
||||
|
||||
|
||||
class TargetUpdate(BaseModel):
|
||||
"""Schema for PUT /api/targets/{id}. All fields optional — only supplied
|
||||
fields are updated. Setting end_date to null will reactivate a historical
|
||||
target only if no other active target exists."""
|
||||
|
||||
start_date: date | None = None
|
||||
end_date: date | None = None
|
||||
calories: int | None = Field(default=None, gt=0)
|
||||
protein_g: float | None = Field(default=None, ge=0)
|
||||
carbs_g: float | None = Field(default=None, ge=0)
|
||||
fat_g: float | None = Field(default=None, ge=0)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_date_range(self):
|
||||
if self.start_date is not None and self.end_date is not None:
|
||||
if self.end_date <= self.start_date:
|
||||
raise ValueError("end_date must be after start_date")
|
||||
return self
|
||||
|
||||
|
||||
class TargetRead(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user