Files
benevoles/backend/app/validation/models.py
T
AdamThomasandClaude Sonnet 5 464635656a Étape 9 : suppression du statut d'affectation (plus de confirmation)
Une affectation est valide dès sa création ; la retirer = supprimer la ligne.

- Modèle : suppression de AssignmentStatus, ACTIVE_ASSIGNMENT_STATUSES et de
  Assignment.status ; migration 0003 (drop_column, alembic check vert)
- Loader de validation : toutes les affectations du bénévole comptent, plus de
  filtre par statut
- API : AssignmentCreate/Update/Read sans status ; création → validation
  toujours exécutée (blocage si violation bloquante) ; PATCH ne porte plus que
  source/locked
- Matrice : MatrixShift.{confirmed, proposed} → {assigned} (compte unique)
- Seed : affectations sans statut, résumé « Affectations » (260)
- Tests mis à jour (matrice, loader, modèle, API) ; 128 tests au total
- CLAUDE.md : glossaire, modèle, sémantique de validation, périmètre

Frontend : les références au statut (propositions, badges confirmé/proposé,
écran de confirmation mobile, mock, schema.ts) restent à retirer séparément.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-10 07:36:06 +02:00

88 lines
2.5 KiB
Python

"""Types d'entrée et de sortie du moteur de validation.
Découplé de la base : le moteur ne connaît que ces structures simples, jamais
les modèles SQLModel. La couche appelante construit l'entrée depuis la base.
"""
import datetime as dt
from dataclasses import dataclass
from enum import StrEnum
from pydantic import BaseModel, Field
from app.models import AvailabilityStatus
class Severity(StrEnum):
BLOCKING = "BLOCKING"
WARNING = "WARNING"
class ViolationCode(StrEnum):
SHIFT_OVERLAP = "SHIFT_OVERLAP"
OUTSIDE_AVAILABILITY = "OUTSIDE_AVAILABILITY"
DAILY_HOURS_EXCEEDED = "DAILY_HOURS_EXCEEDED"
INSUFFICIENT_REST = "INSUFFICIENT_REST"
MISSING_REQUIRED_SKILL = "MISSING_REQUIRED_SKILL"
SEVERITY_BY_CODE: dict[ViolationCode, Severity] = {
ViolationCode.SHIFT_OVERLAP: Severity.BLOCKING,
ViolationCode.OUTSIDE_AVAILABILITY: Severity.WARNING,
ViolationCode.DAILY_HOURS_EXCEEDED: Severity.WARNING,
ViolationCode.INSUFFICIENT_REST: Severity.WARNING,
ViolationCode.MISSING_REQUIRED_SKILL: Severity.WARNING,
}
class Violation(BaseModel):
"""Une violation : un **code** + des paramètres structurés, jamais de phrase
(traduction via `locales/fr.json`, clés `validation.<CODE>`)."""
code: ViolationCode
severity: Severity
params: dict[str, str | int] = Field(default_factory=dict)
@classmethod
def of(cls, code: ViolationCode, **params: str | int) -> "Violation":
return cls(code=code, severity=SEVERITY_BY_CODE[code], params=params)
@dataclass(frozen=True)
class ShiftInfo:
id: str
starts_at: dt.datetime
ends_at: dt.datetime
required_skill_codes: frozenset[str] = frozenset()
@dataclass(frozen=True)
class DayPartWindow:
code: str
start_time: dt.time
# end_time <= start_time ⇒ la demi-journée passe minuit.
end_time: dt.time
order: int
@dataclass(frozen=True)
class AvailabilityDeclaration:
date: dt.date
day_part_code: str
status: AvailabilityStatus
@dataclass(frozen=True)
class ValidationInput:
"""Tout ce qu'il faut pour valider une affectation candidate.
`existing_shifts` = les créneaux des autres affectations du même bénévole
(une affectation = une décision, il n'y a pas d'étape de confirmation).
"""
candidate_shift: ShiftInfo
volunteer_skill_codes: frozenset[str] = frozenset()
existing_shifts: tuple[ShiftInfo, ...] = ()
availabilities: tuple[AvailabilityDeclaration, ...] = ()
day_parts: tuple[DayPartWindow, ...] = ()