M7-T05: add meter CRUD API + retroactive recompute + OpenAPI regen
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
"""Pydantic schemas for the Meter CRUD + swap declaration API (M7-T05).
|
||||
|
||||
Schema hierarchy
|
||||
----------------
|
||||
MeterResponse — single meter row (id/label/commodity/started_at/ended_at/reason/note/created_at)
|
||||
MeterListResponse — ordered list of MeterResponse items
|
||||
MeterDeclareRequest — POST /api/energy/meters body (declare a swap or initial meter)
|
||||
MeterPatchRequest — PATCH /api/energy/meters/{id} body (all fields optional)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Enums
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class MeterReason(str, Enum):
|
||||
"""Allowed values for the meter epoch creation reason."""
|
||||
|
||||
initial = "initial"
|
||||
meter_swap = "meter_swap"
|
||||
home_move = "home_move"
|
||||
other = "other"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Response schemas
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class MeterResponse(BaseModel):
|
||||
"""Response schema for a single Meter epoch row.
|
||||
|
||||
``ended_at`` is ``null`` for the currently active meter.
|
||||
"""
|
||||
|
||||
id: int
|
||||
label: str
|
||||
commodity: str
|
||||
started_at: datetime
|
||||
ended_at: datetime | None
|
||||
reason: str
|
||||
note: str | None
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class MeterListResponse(BaseModel):
|
||||
"""Response schema for GET /api/energy/meters.
|
||||
|
||||
Meters are returned in ascending ``started_at`` order so the caller sees
|
||||
the historical installation sequence.
|
||||
"""
|
||||
|
||||
items: list[MeterResponse]
|
||||
total: int
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Request schemas
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_VALID_REASONS = ", ".join(r.value for r in MeterReason)
|
||||
|
||||
|
||||
class MeterDeclareRequest(BaseModel):
|
||||
"""Request body for POST /api/energy/meters.
|
||||
|
||||
Declares a new meter epoch (swap, home move, or initial declaration). The
|
||||
service layer closes the current active meter for the given commodity at
|
||||
``started_at`` and opens a new one.
|
||||
|
||||
``started_at`` follows the Principle-A localisation convention: a
|
||||
timezone-naive value is interpreted as the **server's local wall-clock time**
|
||||
(e.g. CEST midnight → stored as UTC the night before); a timezone-aware
|
||||
value is converted to UTC as-is. Omitting ``started_at`` is not allowed —
|
||||
every meter declaration must carry an explicit start timestamp.
|
||||
|
||||
``commodity`` defaults to ``"electricity"``; the field is available for
|
||||
future use with ``gas`` or ``heating``.
|
||||
"""
|
||||
|
||||
label: str = Field(..., min_length=1, max_length=255)
|
||||
started_at: datetime = Field(
|
||||
...,
|
||||
description=(
|
||||
"UTC (or server-local naive) datetime from which this meter epoch starts. "
|
||||
"May be in the past (retroactive declaration)."
|
||||
),
|
||||
)
|
||||
reason: MeterReason = Field(
|
||||
...,
|
||||
description=f"Why this epoch was created. One of: {_VALID_REASONS}.",
|
||||
)
|
||||
note: str | None = Field(default=None, max_length=1024)
|
||||
commodity: str = Field(
|
||||
default="electricity",
|
||||
min_length=1,
|
||||
max_length=32,
|
||||
description="Energy commodity this meter measures. Defaults to 'electricity'.",
|
||||
)
|
||||
|
||||
|
||||
class MeterPatchRequest(BaseModel):
|
||||
"""Request body for PATCH /api/energy/meters/{id}.
|
||||
|
||||
All fields are optional. Only non-``None`` values are applied.
|
||||
|
||||
Updating ``started_at`` is a **retroactive correction**: the service layer
|
||||
maintains timeline continuity (adjusting the preceding meter's ``ended_at``)
|
||||
and the API layer triggers ``recompute_range`` over the affected window so
|
||||
that billing attribution is re-judged.
|
||||
"""
|
||||
|
||||
label: str | None = Field(default=None, min_length=1, max_length=255)
|
||||
note: str | None = Field(default=None, max_length=1024)
|
||||
started_at: datetime | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Retroactive correction of the meter epoch start timestamp. "
|
||||
"Triggers billing recompute over the affected window."
|
||||
),
|
||||
)
|
||||
Reference in New Issue
Block a user