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:
Craig
2026-07-26 13:08:23 +01:00
parent 3f2c765c84
commit 601c0d461e
4 changed files with 743 additions and 6 deletions
+30
View File
@@ -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)