"""Transactional learning-event ledger helpers. Domain rows such as QuizAttempt, LessonProgress and RevisionItem remain the source of truth. This ledger only records the meaningful action and the domain record that caused it, in the same database transaction. """ from __future__ import annotations from typing import Any from sqlalchemy import select from sqlalchemy.orm import Session from app.models.learning_state import LearningEvent def add_learning_event( db: Session, *, user_id: str, event_type: str, entity_type: str, entity_id: str, idempotency_key: str, subject_id: str | None = None, chapter_id: str | None = None, topic_key: str | None = None, event_data: dict[str, Any] | None = None, ) -> LearningEvent: """Stage one idempotent event in the caller's current transaction.""" existing = db.scalar( select(LearningEvent).where( LearningEvent.user_id == user_id, LearningEvent.idempotency_key == idempotency_key, ) ) if existing is not None: return existing event = LearningEvent( user_id=user_id, event_type=event_type, entity_type=entity_type, entity_id=entity_id, idempotency_key=idempotency_key, subject_id=subject_id, chapter_id=chapter_id, topic_key=topic_key, event_data=event_data or {}, ) db.add(event) return event