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
88 lines
2.5 KiB
Python
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, ...] = ()
|