Full-stack app for participatory water pricing using Bezier curves. - Backend: FastAPI + SQLAlchemy + SQLite with JWT auth - Frontend: Nuxt 4 + TypeScript with interactive SVG editor - Math engine: cubic Bezier tarification with Cardano solver - Admin: commune management, household import, vote monitoring, CMS - Citizen: interactive curve editor, vote submission - Docker-compose deployment ready Includes fixes for: - Impact table snake_case/camelCase property mismatch - CMS content backend API + frontend editor (was stub) - Admin route protection middleware - Public content display on commune page - Vote confirmation page link fix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
206 lines
3.6 KiB
Python
206 lines
3.6 KiB
Python
"""Pydantic schemas for API request/response validation."""
|
|
|
|
from datetime import datetime
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
# ── Auth ──
|
|
|
|
class AdminLogin(BaseModel):
|
|
email: str
|
|
password: str
|
|
|
|
|
|
class CitizenVerify(BaseModel):
|
|
commune_slug: str
|
|
auth_code: str
|
|
|
|
|
|
class Token(BaseModel):
|
|
access_token: str
|
|
token_type: str = "bearer"
|
|
role: str
|
|
commune_slug: str | None = None
|
|
|
|
|
|
# ── Commune ──
|
|
|
|
class CommuneCreate(BaseModel):
|
|
name: str
|
|
slug: str
|
|
description: str = ""
|
|
|
|
|
|
class CommuneUpdate(BaseModel):
|
|
name: str | None = None
|
|
description: str | None = None
|
|
is_active: bool | None = None
|
|
|
|
|
|
class CommuneOut(BaseModel):
|
|
id: int
|
|
name: str
|
|
slug: str
|
|
description: str
|
|
is_active: bool
|
|
created_at: datetime
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
# ── TariffParams ──
|
|
|
|
class TariffParamsUpdate(BaseModel):
|
|
abop: float | None = None
|
|
abos: float | None = None
|
|
recettes: float | None = None
|
|
pmax: float | None = None
|
|
vmax: float | None = None
|
|
|
|
|
|
class TariffParamsOut(BaseModel):
|
|
abop: float
|
|
abos: float
|
|
recettes: float
|
|
pmax: float
|
|
vmax: float
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
# ── Household ──
|
|
|
|
class HouseholdOut(BaseModel):
|
|
id: int
|
|
identifier: str
|
|
status: str
|
|
volume_m3: float
|
|
price_paid_eur: float
|
|
auth_code: str
|
|
has_voted: bool
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class HouseholdStats(BaseModel):
|
|
total: int
|
|
rs_count: int
|
|
rp_count: int
|
|
pro_count: int
|
|
total_volume: float
|
|
avg_volume: float
|
|
median_volume: float
|
|
voted_count: int
|
|
|
|
|
|
class ImportPreview(BaseModel):
|
|
valid_rows: int
|
|
errors: list[str]
|
|
sample: list[dict]
|
|
|
|
|
|
class ImportResult(BaseModel):
|
|
created: int
|
|
errors: list[str]
|
|
|
|
|
|
# ── Tariff Compute ──
|
|
|
|
class TariffComputeRequest(BaseModel):
|
|
commune_slug: str
|
|
vinf: float = Field(ge=0)
|
|
a: float = Field(ge=0, le=1)
|
|
b: float = Field(ge=0, le=1)
|
|
c: float = Field(ge=0, le=1)
|
|
d: float = Field(ge=0, le=1)
|
|
e: float = Field(ge=0, le=1)
|
|
|
|
|
|
class ImpactRowOut(BaseModel):
|
|
volume: float
|
|
old_price: float
|
|
new_price_rp: float
|
|
new_price_rs: float
|
|
|
|
|
|
class TariffComputeResponse(BaseModel):
|
|
p0: float
|
|
curve_volumes: list[float]
|
|
curve_prices_m3: list[float]
|
|
curve_bills_rp: list[float]
|
|
curve_bills_rs: list[float]
|
|
impacts: list[ImpactRowOut]
|
|
|
|
|
|
# ── Vote ──
|
|
|
|
class VoteCreate(BaseModel):
|
|
vinf: float = Field(ge=0)
|
|
a: float = Field(ge=0, le=1)
|
|
b: float = Field(ge=0, le=1)
|
|
c: float = Field(ge=0, le=1)
|
|
d: float = Field(ge=0, le=1)
|
|
e: float = Field(ge=0, le=1)
|
|
|
|
|
|
class VoteOut(BaseModel):
|
|
id: int
|
|
household_id: int
|
|
vinf: float
|
|
a: float
|
|
b: float
|
|
c: float
|
|
d: float
|
|
e: float
|
|
computed_p0: float | None
|
|
submitted_at: datetime
|
|
is_active: bool
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class MedianOut(BaseModel):
|
|
vinf: float
|
|
a: float
|
|
b: float
|
|
c: float
|
|
d: float
|
|
e: float
|
|
computed_p0: float
|
|
vote_count: int
|
|
|
|
|
|
# ── Admin User ──
|
|
|
|
class AdminUserCreate(BaseModel):
|
|
email: str
|
|
password: str
|
|
full_name: str = ""
|
|
role: str = "commune_admin"
|
|
commune_slugs: list[str] = []
|
|
|
|
|
|
class AdminUserOut(BaseModel):
|
|
id: int
|
|
email: str
|
|
full_name: str
|
|
role: str
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
# ── Content ──
|
|
|
|
class ContentUpdate(BaseModel):
|
|
title: str
|
|
body_markdown: str
|
|
|
|
|
|
class ContentOut(BaseModel):
|
|
slug: str
|
|
title: str
|
|
body_markdown: str
|
|
updated_at: datetime
|
|
|
|
model_config = {"from_attributes": True}
|