feat: persister les événements de présence et activer les clés étrangères SQLite
Ajoute le modèle WorkplacePresenceEvent pour stocker les événements de présence reçus de Home Assistant, avec clé d'idempotence unique, lien vers une journée et, facultativement, une plage horaire. Active les contraintes de clés étrangères sur chaque connexion SQLite et documente le schéma dans l'onboarding. Couvre le tout par des tests de modèle et de factory. Co-authored-by: OpenAI/GPT-5.6-Luna-Pro <vibecoder@antoineve.me>
This commit is contained in:
@@ -37,6 +37,9 @@ class WorkEntry(db.Model):
|
||||
time_slots: so.Mapped[list["TimeSlot"]] = so.relationship(
|
||||
back_populates="entry", cascade="all, delete-orphan", order_by="TimeSlot.start_time"
|
||||
)
|
||||
presence_events: so.Mapped[list["WorkplacePresenceEvent"]] = so.relationship(
|
||||
back_populates="entry", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
def total_minutes(self) -> int:
|
||||
"""
|
||||
@@ -86,6 +89,50 @@ class TimeSlot(db.Model):
|
||||
end_time: so.Mapped[time] = so.mapped_column(sa.Time, nullable=False)
|
||||
|
||||
entry: so.Mapped["WorkEntry"] = so.relationship(back_populates="time_slots")
|
||||
presence_events: so.Mapped[list["WorkplacePresenceEvent"]] = so.relationship(
|
||||
back_populates="time_slot", passive_deletes=True
|
||||
)
|
||||
|
||||
|
||||
class WorkplacePresenceEvent(db.Model):
|
||||
"""Événement de présence reçu de Home Assistant.
|
||||
|
||||
``received_at`` est un instant normalisé en UTC, stocké naïf selon la convention
|
||||
actuelle de l'application. À l'inverse, ``occurred_at`` est l'heure murale naïve
|
||||
dans ``Europe/Paris`` et ``local_date`` est le jour local dérivé de cette heure.
|
||||
Cette distinction est volontaire : elle sera utilisée par le service métier futur
|
||||
pour rattacher les arrivées et départs aux journées, notamment autour de minuit.
|
||||
"""
|
||||
|
||||
__tablename__ = "workplace_presence_events"
|
||||
__table_args__ = (
|
||||
sa.CheckConstraint("event_type IN ('arrival', 'departure')", name="ck_presence_event_type"),
|
||||
sa.Index("ix_presence_events_local_date", "local_date"),
|
||||
sa.Index("ix_presence_events_entry_id", "entry_id"),
|
||||
)
|
||||
|
||||
id: so.Mapped[int] = so.mapped_column(primary_key=True)
|
||||
idempotency_key: so.Mapped[str] = so.mapped_column(sa.String(255), unique=True, nullable=False)
|
||||
event_type: so.Mapped[str] = so.mapped_column(sa.String(9), nullable=False)
|
||||
received_at: so.Mapped[datetime] = so.mapped_column(sa.DateTime, nullable=False)
|
||||
# Heure locale Europe/Paris, sans fuseau : ne pas la traiter comme un instant UTC.
|
||||
occurred_at: so.Mapped[datetime] = so.mapped_column(sa.DateTime, nullable=False)
|
||||
local_date: so.Mapped[date] = so.mapped_column(sa.Date, nullable=False)
|
||||
entry_id: so.Mapped[int] = so.mapped_column(
|
||||
sa.ForeignKey("work_entries.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
time_slot_id: so.Mapped[int | None] = so.mapped_column(
|
||||
sa.ForeignKey("time_slots.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
created_at: so.Mapped[datetime] = so.mapped_column(
|
||||
sa.DateTime, default=lambda: datetime.now(UTC), nullable=False
|
||||
)
|
||||
processed_at: so.Mapped[datetime | None] = so.mapped_column(sa.DateTime, nullable=True)
|
||||
|
||||
entry: so.Mapped["WorkEntry"] = so.relationship(back_populates="presence_events")
|
||||
time_slot: so.Mapped["TimeSlot | None"] = so.relationship(
|
||||
back_populates="presence_events", passive_deletes=True
|
||||
)
|
||||
|
||||
|
||||
class LeaveBalance(db.Model):
|
||||
|
||||
Reference in New Issue
Block a user