diff --git a/src/quiz/services/quiz_service.py b/src/quiz/services/quiz_service.py index f722dd7..62320e8 100644 --- a/src/quiz/services/quiz_service.py +++ b/src/quiz/services/quiz_service.py @@ -353,6 +353,21 @@ def process_on_demand_quiz(self, chat_id: str, lang: str, topic: str, difficulty ) if poll_result: + poll_id = str(poll_result.get("poll", {}).get("id", "")) + message_id = poll_result.get("message_id", 0) + self._repo.save_on_demand_quiz_record( + chat_id=chat_id, + question=question["question"], + options=question["options"], + correct_option_id=question["correct_option_index"], + explanation=question.get("explanation"), + category=topic, + lang=lang, + poll_id=poll_id, + message_id=message_id, + difficulty=difficulty, + points=question["points"], + ) logger.info("On-demand quiz sent", extra={"chat_id": chat_id, "topic": topic}) return {"status": "ok", "sent": 1, "total": 1, **self._rpd_payload()} diff --git a/src/quiz/services/repository.py b/src/quiz/services/repository.py index 7d90d02..6f33352 100644 --- a/src/quiz/services/repository.py +++ b/src/quiz/services/repository.py @@ -252,13 +252,81 @@ def save_quiz_record( """Write a daily quiz record for a chat.""" now = datetime.now(_ALMATY_TZ) today = now.strftime("%Y-%m-%d") + self._save_quiz_record( + chat_id=chat_id, + sort_key=f"DATE#{today}", + question=question, + options=options, + correct_option_id=correct_option_id, + explanation=explanation, + category=category, + lang=lang, + poll_id=poll_id, + message_id=message_id, + difficulty=difficulty, + points=points, + sent_at=now, + log_context={"date": today}, + ) + + def save_on_demand_quiz_record( + self, + chat_id: str, + question: str, + options: list[str], + correct_option_id: int, + explanation: str | None, + category: str, + lang: str, + poll_id: str, + message_id: int, + difficulty: str = "easy", + points: int = 1, + ) -> None: + """Write an on-demand quiz record keyed by poll id for answer lookup.""" + now = datetime.now(_ALMATY_TZ) + self._save_quiz_record( + chat_id=chat_id, + sort_key=f"ON_DEMAND#{poll_id}", + question=question, + options=options, + correct_option_id=correct_option_id, + explanation=explanation, + category=category, + lang=lang, + poll_id=poll_id, + message_id=message_id, + difficulty=difficulty, + points=points, + sent_at=now, + log_context={"poll_id": str(poll_id)}, + ) + + def _save_quiz_record( + self, + chat_id: str, + sort_key: str, + question: str, + options: list[str], + correct_option_id: int, + explanation: str | None, + category: str, + lang: str, + poll_id: str, + message_id: int, + difficulty: str, + points: int, + sent_at: datetime, + log_context: dict[str, Any], + ) -> None: + """Persist quiz metadata used by poll_answer scoring.""" ttl = int(time.time()) + (_TTL_DAYS * 86400) try: self._table.put_item( Item={ "PK": f"QUIZ#{chat_id}", - "SK": f"DATE#{today}", + "SK": sort_key, "question": question, "options": options, "correct_option_id": correct_option_id, @@ -269,11 +337,11 @@ def save_quiz_record( "message_id": message_id, "difficulty": difficulty, "points": points, - "sent_at": now.isoformat(), + "sent_at": sent_at.isoformat(), "ttl": ttl, } ) - logger.info("Quiz record saved", extra={"chat_id": chat_id, "date": today}) + logger.info("Quiz record saved", extra={"chat_id": chat_id, **log_context}) except Exception as e: - logger.error("Failed to save quiz record", extra={"chat_id": chat_id, "error": str(e)}) + logger.error("Failed to save quiz record", extra={"chat_id": chat_id, **log_context, "error": str(e)}) raise diff --git a/tests/test_quiz_service.py b/tests/test_quiz_service.py new file mode 100644 index 0000000..8547f2f --- /dev/null +++ b/tests/test_quiz_service.py @@ -0,0 +1,63 @@ +"""Tests for quiz service orchestration.""" + +import os +import sys +from unittest.mock import MagicMock + +os.environ.setdefault("QUIZ_LLM_RPD", "1000") + +_quiz_dir = os.path.join(os.path.dirname(__file__), "..", "src", "quiz") +_saved_modules: dict[str, object] = {} + +try: + for mod_name in list(sys.modules): + if mod_name in ("core", "services") or mod_name.startswith(("core.", "services.")): + _saved_modules[mod_name] = sys.modules.pop(mod_name) + + sys.path.insert(0, _quiz_dir) + from services.quiz_service import QuizService # noqa: E402 +finally: + if _quiz_dir in sys.path: + sys.path.remove(_quiz_dir) + for mod_name in list(sys.modules): + if mod_name in ("core", "services") or mod_name.startswith(("core.", "services.")): + sys.modules.pop(mod_name, None) + sys.modules.update(_saved_modules) + + +def test_process_on_demand_quiz_persists_poll_for_answer_lookup() -> None: + service = QuizService.__new__(QuizService) + service._generator = MagicMock() + service._sender = MagicMock() + service._repo = MagicMock() + + service._generator.generate_question.return_value = { + "question": "What is S3?", + "options": ["Storage", "Compute", "Network", "Database"], + "correct_option_index": 0, + "explanation": "S3 stores objects.", + "difficulty": "easy", + "points": 1, + } + service._generator.get_rpd_status.return_value = (99, 100) + service._sender.send_quiz_poll.return_value = { + "message_id": 123, + "poll": {"id": "poll-123"}, + } + + result = service.process_on_demand_quiz("chat-1", "en", "cloud", "easy") + + assert result["status"] == "ok" + service._repo.save_on_demand_quiz_record.assert_called_once_with( + chat_id="chat-1", + question="What is S3?", + options=["Storage", "Compute", "Network", "Database"], + correct_option_id=0, + explanation="S3 stores objects.", + category="cloud", + lang="en", + poll_id="poll-123", + message_id=123, + difficulty="easy", + points=1, + )