AttemptRepository:
- Start: проверка max_attempts (учитывает уже использованные с этого
user_id или public_token_id), вставка in_progress'а;
- Get/ListByUser/ListByTest: чтение с per-attempt scope;
- SubmitAndGrade: транзакционно сохраняет ответы в attempt_answers
(JSONB payload + correct + score), считает итог:
single — 1 правильный → points за вопрос, иначе 0;
multi — set ответов == set is_correct=TRUE → points, иначе 0
(частичные баллы не делаем в MVP);
text — correct=NULL и score=NULL, ждут ручной оценки HR'ом.
max_score = SUM(points) по всем вопросам (не только отвеченным).
passed = NULL если у теста нет passing_score; иначе процент vs порог.
status: graded если все автогрейд'ятся; submitted если есть text.
AttemptHandler:
- POST /tests/{id}/attempts — Start (X-User-Id из portal-gateway).
Не-владелец стартует только если is_published=true.
- GET /attempts/{id} — Get с проверкой «я респондент / я владелец теста».
- POST /attempts/{id}/submit — Submit (только свою попытку).
- GET /attempts — ListMine.
- GET /tests/{id}/attempts — ListByTest (только для владельца).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>