Initial commit: SejeteralO water tarification platform
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>
This commit is contained in:
1
backend/app/schemas/__init__.py
Normal file
1
backend/app/schemas/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from app.schemas.schemas import * # noqa: F401, F403
|
||||
205
backend/app/schemas/schemas.py
Normal file
205
backend/app/schemas/schemas.py
Normal file
@@ -0,0 +1,205 @@
|
||||
"""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}
|
||||
Reference in New Issue
Block a user