Skip to content

feat(quiz-service): pipeline complet de gamification (RabbitMQ) - #27

Merged
ASSONDJI merged 4 commits into
mainfrom
feat/lessons-content
Jul 12, 2026
Merged

feat(quiz-service): pipeline complet de gamification (RabbitMQ)#27
ASSONDJI merged 4 commits into
mainfrom
feat/lessons-content

Conversation

@ASSONDJI

@ASSONDJI ASSONDJI commented Jul 12, 2026

Copy link
Copy Markdown
Owner

feat(quiz-service): pipeline complet de gamification (RabbitMQ)

  • X-User-Id (injecte par api-gateway) desormais persiste sur chaque
    AnswerAttempt -- la table ne trackait auparavant aucun utilisateur
  • Les tentatives sont enfin reellement persistees (submitAnswer ne faisait
    que lire, jamais ecrire, malgre la table dediee)
  • Normalisation Unicode NFC avant comparaison des reponses -- corrige un
    bug reel ou des mots stockes en NFD (diacritiques combinants, frequent
    sur les tons Yemba) etaient injustement marques faux
  • LessonCompletionService : detecte la fin d'une lecon (toute question a
    au moins une tentative correcte -- les erreurs doivent etre corrigees
    avant completion, modele Duolingo) et publie lesson.completed +
    quiz.completed vers RabbitMQ (exchange afrilingua.events), consommes par
    le pipeline de gamification deja existant cote recommendation-service
  • is_perfect calcule honnetement (aucune tentative incorrecte enregistree)
    plutot que fige a true -- integrite du badge Perfectionniste
  • Table lesson_completions (contrainte unique user_id+lesson_id) : garde
    d'idempotence, empeche de republier/re-crediter l'XP a chaque reponse
    correcte soumise apres que la lecon soit deja terminee
  • Teste de bout en bout : login reel -> reponses avec erreur volontaire
    puis correction -> XP/niveau/lessons_completed/has_perfect_quiz corrects
    en base cote recommendation-service

Summary by CodeRabbit

  • New Features

    • Sessions now persist securely and restore automatically when the app starts.
    • Protected services require valid authentication, improving access security.
    • Quiz answers are recorded with improved accent and case handling.
    • Lesson completion is recognized after all questions are answered correctly.
    • Questions can be linked to vocabulary for improved learning progress tracking.
    • Added tools to generate lessons and multiple-choice quizzes from vocabulary.
  • Bug Fixes

    • Improved authentication handling for outgoing mobile requests.

ASSONDJI added 4 commits July 10, 2026 23:12
- generate_lessons.py : decoupe le vocabulaire deja importe (content-service)
  en lecons de 8-10 mots, regroupees par niveau de difficulte deja calcule
  (BEGINNER/INTERMEDIATE/ADVANCED, classification C4.5 automatique cote
  recommendation-service). Idempotent par langue.
- generate_quizzes.py : genere une question QCM par mot de chaque lecon
  ('Comment dit-on X en {langue} ?'), distracteurs tires parmi les autres
  mots de la meme lecon. Idempotent par lecon.
- Corrige l'absence totale de contenu pedagogique reel : Yemba (39 lecons/
  344 mots), Duala (21/189), Bassa (81/727), avec 1257 questions generees
  au total
- Question.wordId (nullable) : relie chaque question au mot content-service
  qu'elle teste -- necessaire pour que recommendation-service puisse suivre
  les mots uniques appris (gamification)
- Migration V2 : ajoute la colonne + index
- generate_quizzes.py : transmet desormais wordId a la creation
- Les 1257 questions existantes ont ete
… session mobile

- JwtAuthenticationFilter (api-gateway) : seul point de validation JWT du
  systeme. Laisse passer /api/auth/** sans verification, exige un Bearer
  token valide partout ailleurs, injecte X-User-Id (extrait du token) avant
  de router vers les services internes -- qui n'ont donc jamais besoin de
  decoder un JWT eux-memes
- Reutilise la meme cle de signature HMAC que auth-service (jjwt)
- mobile : AuthInterceptor attache automatiquement le token stocke a chaque
  requete Dio sortante
- mobile : AuthController.restoreSession() reconstruit la session depuis
  flutter_secure_storage au demarrage -- corrige la perte de session a
  chaque rechargement de page (F5, navigation directe vers une URL,
  hot restart), la session n'etait auparavant gardee qu'en memoire
- Necessaire avant de brancher quiz-service sur user_id fiable pour la
  gamification (aucune propagation d'identite inter-services n'existait
  avant ce commit)
- X-User-Id (injecte par api-gateway) desormais persiste sur chaque
  AnswerAttempt -- la table ne trackait auparavant aucun utilisateur
- Les tentatives sont enfin reellement persistees (submitAnswer ne faisait
  que lire, jamais ecrire, malgre la table dediee)
- Normalisation Unicode NFC avant comparaison des reponses -- corrige un
  bug reel ou des mots stockes en NFD (diacritiques combinants, frequent
  sur les tons Yemba) etaient injustement marques faux
- LessonCompletionService : detecte la fin d'une lecon (toute question a
  au moins une tentative correcte -- les erreurs doivent etre corrigees
  avant completion, modele Duolingo) et publie lesson.completed +
  quiz.completed vers RabbitMQ (exchange afrilingua.events), consommes par
  le pipeline de gamification deja existant cote recommendation-service
- is_perfect calcule honnetement (aucune tentative incorrecte enregistree)
  plutot que fige a true -- integrite du badge Perfectionniste
- Table lesson_completions (contrainte unique user_id+lesson_id) : garde
  d'idempotence, empeche de republier/re-crediter l'XP a chaque reponse
  correcte soumise apres que la lecon soit deja terminee
- Teste de bout en bout : login reel -> reponses avec erreur volontaire
  puis correction -> XP/niveau/lessons_completed/has_perfect_quiz corrects
  en base cote recommendation-service
@ASSONDJI ASSONDJI self-assigned this Jul 12, 2026
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds mobile session restoration and token injection, gateway JWT authentication, lesson and quiz generation scripts, and quiz-service answer-attempt persistence with lesson-completion events.

Changes

Mobile authentication

Layer / File(s) Summary
Session persistence and startup restoration
mobile/lib/features/auth/presentation/auth_providers.dart, mobile/lib/main.dart
Session fields are persisted and restored before the app builds, while routing uses the restored provider container.
Secure-storage API client wiring
mobile/lib/core/network/api_client.dart, mobile/lib/core/network/auth_interceptor.dart, mobile/lib/features/auth/presentation/auth_providers.dart
ApiClient receives secure storage and injects a stored access token into outgoing requests.

Gateway JWT authentication

Layer / File(s) Summary
JWT configuration and library wiring
services/api-gateway/pom.xml, services/api-gateway/src/main/java/..., services/api-gateway/src/main/resources/application.yml
JWT dependencies and configuration-property binding are added for the configured signing key.
Protected-request filtering
services/api-gateway/src/main/java/cm/afrilingua/gateway/security/JwtAuthenticationFilter.java
Protected requests require a valid bearer token; the filter forwards the JWT subject as X-User-Id and returns JSON 401 responses for invalid requests.

Lesson and quiz generation tooling

Layer / File(s) Summary
Vocabulary-to-lesson generation
services/lesson-service/scripts/generate_lessons.py
Vocabulary is grouped by difficulty and converted into ordered lessons with attached words.
Lesson-to-quiz generation
services/quiz-service/scripts/generate_quizzes.py
Lessons with enough words receive multiple-choice questions with sampled distractors and optional regeneration.

Quiz completion tracking

Layer / File(s) Summary
RabbitMQ and event configuration
services/quiz-service/pom.xml, services/quiz-service/src/main/java/cm/afrilingua/quiz/config/*, services/quiz-service/src/main/resources/application.yml
AMQP support, event properties, exchange configuration, JSON conversion, and broker settings are added.
Question and completion persistence contracts
services/quiz-service/src/main/java/cm/afrilingua/quiz/entity/*, services/quiz-service/src/main/java/cm/afrilingua/quiz/repository/*, services/quiz-service/src/main/resources/db/migration/*, services/quiz-service/src/main/resources/openapi/quiz-service.yaml
Question word IDs, answer attempts, lesson completions, repositories, migrations, and OpenAPI fields are introduced.
Answer recording and completion evaluation
services/quiz-service/src/main/java/cm/afrilingua/quiz/service/QuestionService.java, services/quiz-service/src/main/java/cm/afrilingua/quiz/service/LessonCompletionService.java
Answers are normalized and recorded, and successful lesson completion is evaluated using user attempts.
Completion event publishing
services/quiz-service/src/main/java/cm/afrilingua/quiz/messaging/LessonCompletionEventPublisher.java
Quiz and lesson completion messages are published through RabbitMQ.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MobileApp
  participant ApiGateway
  participant QuizService
  participant RabbitMQ
  MobileApp->>ApiGateway: request with Authorization bearer token
  ApiGateway->>QuizService: request with trusted X-User-Id
  QuizService->>QuizService: record answer and evaluate completion
  QuizService->>RabbitMQ: publish quiz.completed and lesson.completed
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main quiz-service gamification pipeline and RabbitMQ aspect of the changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/lessons-content

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ASSONDJI
ASSONDJI merged commit c6e4329 into main Jul 12, 2026
5 of 7 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (8)
services/lesson-service/scripts/generate_lessons.py (1)

24-27: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use requests.Session() for connection reuse across the script.

The script makes hundreds of HTTP calls (one create_lesson + N attach_word per lesson). Each requests.get/post creates a new TCP connection. A shared requests.Session() reuses connections via keep-alive, significantly reducing latency for batch generation scripts.

♻️ Proposed refactor
 import requests
 
 CONTENT_BASE_URL = "http://localhost:8080/api/content"
 LESSON_BASE_URL = "http://localhost:8080/api"
 
+session = requests.Session()
+
 DIFFICULTY_ORDER = ["BEGINNER", "INTERMEDIATE", "ADVANCED"]

Then replace all requests.get(...) / requests.post(...) calls with session.get(...) / session.post(...).

Also applies to: 42-71

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/lesson-service/scripts/generate_lessons.py` around lines 24 - 27,
Introduce one shared requests.Session() for the lesson-generation script and
reuse it across all HTTP operations. Update the request calls in the lesson
creation and word-attachment flows, including the code around create_lesson and
attach_word, to use the shared session’s get/post methods instead of
module-level requests calls; preserve existing URLs, payloads, and response
handling.
services/quiz-service/scripts/generate_quizzes.py (1)

35-56: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use requests.Session() and consider batching word fetches to reduce HTTP overhead.

fetch_word is called once per word in the list comprehension at line 85, producing ~1,267 individual requests for the full dataset. Each also opens a new TCP connection. A requests.Session() amortizes connection overhead; if the content-service supports a batch/bulk endpoint, fetching all words for a lesson in one call would eliminate the N+1 entirely.

♻️ Proposed Session refactor
 import requests
 
 CONTENT_BASE_URL = "http://localhost:8080/api/content"
 LESSON_BASE_URL = "http://localhost:8080/api"
 QUIZ_BASE_URL = "http://localhost:8080/api"
 
+session = requests.Session()
+
 MIN_WORDS_FOR_QUESTION = 4

Then replace all requests.get/post calls with session.get/post.

Also applies to: 85-85

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/quiz-service/scripts/generate_quizzes.py` around lines 35 - 56,
Refactor the quiz-generation request flow to create and reuse a
requests.Session, replacing the direct requests.get calls in fetch_languages,
fetch_lessons, fetch_word, and fetch_existing_question_count with session
requests and passing the session through their callers. If the content service
exposes a bulk word endpoint, update the word-fetching flow around fetch_word to
retrieve lesson words in one request and eliminate the per-word N+1 calls.
mobile/lib/features/auth/presentation/auth_providers.dart (2)

36-42: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Sequential un-batched writes risk partial-session state.

_persistSession performs 5 independent await writes. If interrupted (app killed, storage exception mid-write) between writes, storage ends up with some but not all keys set. Since restoreSession (lines 55-63) requires all five fields to be non-null to reconstruct a session, a partial write silently forces the user back to the login screen even though a valid access/refresh token pair was persisted.

Consider persisting the session as a single JSON-encoded value under one key (single atomic write) instead of 5 separate keys.

♻️ Suggested consolidation
-  Future<void> _persistSession(AuthResponse result) async {
-    await _storage.write(key: 'access_token', value: result.accessToken);
-    await _storage.write(key: 'refresh_token', value: result.refreshToken);
-    await _storage.write(key: 'account_id', value: result.id);
-    await _storage.write(key: 'email', value: result.email);
-    await _storage.write(key: 'role', value: result.role);
-  }
+  Future<void> _persistSession(AuthResponse result) async {
+    await _storage.write(key: 'session', value: jsonEncode({
+      'access_token': result.accessToken,
+      'refresh_token': result.refreshToken,
+      'account_id': result.id,
+      'email': result.email,
+      'role': result.role,
+    }));
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile/lib/features/auth/presentation/auth_providers.dart` around lines 36 -
42, Update _persistSession and the corresponding restoreSession logic to store
the complete session as one JSON-encoded value under a single storage key, using
one atomic write. Encode all five AuthResponse fields together, then decode that
object during restoration and preserve the existing session reconstruction
behavior.

79-85: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused sessionRestorationProvider. main.dart already calls authControllerProvider.notifier.restoreSession() before runApp, so this provider is dead code unless you plan to watch it somewhere else.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile/lib/features/auth/presentation/auth_providers.dart` around lines 79 -
85, Remove the unused sessionRestorationProvider declaration and its
accompanying documentation comment; keep the existing authControllerProvider and
restoreSession implementation unchanged because main.dart already invokes
restoration before runApp.
services/quiz-service/src/main/java/cm/afrilingua/quiz/config/EventProperties.java (1)

5-6: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a default value for exchange.

If application.events.exchange is not set in application.yml, exchange will be null, causing RabbitTemplate.convertAndSend(null, ...) to use the default AMQP exchange — which silently ignores routing keys and breaks event delivery to the recommendation service. Spring Boot 3.x supports @DefaultValue on record components.

♻️ Suggested refactor
 import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.bind.DefaultValue;

 `@ConfigurationProperties`(prefix = "application.events")
-public record EventProperties(String exchange) {}
+public record EventProperties(`@DefaultValue`("afrilingua.events") String exchange) {}

Please verify that application.events.exchange is set in application.yml.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@services/quiz-service/src/main/java/cm/afrilingua/quiz/config/EventProperties.java`
around lines 5 - 6, Ensure application.events.exchange is explicitly configured
in application.yml and add a suitable `@DefaultValue` to the exchange component of
EventProperties so it cannot be null when configuration is missing. Use the same
exchange name in both places and preserve the existing configuration-properties
binding.
services/quiz-service/src/main/resources/application.yml (1)

21-25: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Externalize RabbitMQ credentials for non-dev environments.

The RabbitMQ username and password are hardcoded, matching the datasource credentials. While acceptable for local development, ensure production deployments override these via environment variables (e.g., ${RABBITMQ_USERNAME:afrilingua}, ${RABBITMQ_PASSWORD:afrilingua_dev_password}) or a secrets manager.

🔒 Suggested externalization
   rabbitmq:
-    host: localhost
-    port: 5672
-    username: afrilingua
-    password: afrilingua_dev_password
+    host: ${RABBITMQ_HOST:localhost}
+    port: ${RABBITMQ_PORT:5672}
+    username: ${RABBITMQ_USERNAME:afrilingua}
+    password: ${RABBITMQ_PASSWORD:afrilingua_dev_password}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/quiz-service/src/main/resources/application.yml` around lines 21 -
25, Update the RabbitMQ credentials in the rabbitmq configuration to resolve
username and password from environment variables with the current development
credentials as defaults, allowing production deployments to override them
without changing the application configuration.
services/quiz-service/src/main/resources/db/migration/V2__add_word_id_to_questions.sql (1)

1-3: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider concurrent index creation for production deployments.

Squawk flagged that CREATE INDEX blocks writes. While this is negligible for a new/empty table in early development, consider using CREATE INDEX CONCURRENTLY for production-scale tables. Note that CONCURRENTLY cannot run inside a transaction, so it would require a separate Flyway migration with executeInTransaction: false.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@services/quiz-service/src/main/resources/db/migration/V2__add_word_id_to_questions.sql`
around lines 1 - 3, Update the questions index migration to create
idx_questions_word concurrently for production-scale tables, and configure the
Flyway migration with executeInTransaction: false because concurrent index
creation cannot run in a transaction. Keep the word_id column addition unchanged
and ensure the index creation is placed in a migration that runs outside a
transaction.

Source: Linters/SAST tools

services/quiz-service/src/main/resources/openapi/quiz-service.yaml (1)

178-197: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider adding wordId to QuestionWithAnswer for response consistency.

The QuestionWithAnswer schema (returned to the creator on question creation) omits the new wordId field, while both CreateQuestionRequest and the learner-facing Question schema include it. Adding it would let creators verify the stored wordId without a separate fetch.

♻️ Suggested addition
     QuestionWithAnswer:
       type: object
       description: Full view, returned only to the caller who just created the question
       properties:
         id:
           type: string
           format: uuid
         lessonId:
           type: string
           format: uuid
+        wordId:
+          type: string
+          format: uuid
+          nullable: true
         type:
           type: string
           enum: [MULTIPLE_CHOICE, MATCHING, FILL_IN_THE_BLANK]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/quiz-service/src/main/resources/openapi/quiz-service.yaml` around
lines 178 - 197, Add the wordId property to the QuestionWithAnswer schema,
matching the existing definition and UUID format used by CreateQuestionRequest
and Question, so question-creation responses expose the stored word association
consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mobile/lib/core/network/auth_interceptor.dart`:
- Around line 13-18: Update onRequest to catch failures from _storage.read,
allowing the request to continue without an Authorization header when token
retrieval fails. Ensure handler.next(options) is always called after the read
attempt, while preserving the existing header behavior for successfully
retrieved non-null tokens.

In `@mobile/lib/features/auth/presentation/auth_providers.dart`:
- Around line 44-63: Update Auth provider’s restoreSession() to catch
secure-storage read failures, including PlatformException, and reset or retain
the logged-out state instead of allowing the exception to propagate to startup.
Keep successful token reconstruction via AuthResponse unchanged, and use the
existing logging mechanism if available to record the failure.

In `@services/api-gateway/pom.xml`:
- Line 23: Update the jjwt.version property from 0.12.6 to 0.12.7 in the Maven
configuration, ensuring jjwt-api, jjwt-impl, and jjwt-jackson continue resolving
the same aligned version through this property.

In
`@services/api-gateway/src/main/java/cm/afrilingua/gateway/security/JwtAuthenticationFilter.java`:
- Around line 61-82: Validate the subject returned by claims.getSubject() in
JwtAuthenticationFilter before mutating the request: reject null or blank values
through the existing unauthorized flow instead of setting X-User-Id. Preserve
the current header replacement and valid-token behavior for nonblank subjects.

In `@services/api-gateway/src/main/resources/application.yml`:
- Around line 73-77: Remove the hardcoded fallback from the JWT secret-key
configuration and require JWT_SECRET_KEY to be explicitly provided, failing
application startup when it is absent. Apply the same required-secret
configuration to auth-service’s equivalent JwtProperties setting, preserving the
exact shared secret contract between both services.

In `@services/quiz-service/scripts/generate_quizzes.py`:
- Line 120: In the caller assigning the result of generate_for_lesson, rename
the ambiguous variable l to a descriptive name such as lesson data while
preserving the existing q value and downstream behavior.

In
`@services/quiz-service/src/main/java/cm/afrilingua/quiz/service/LessonCompletionService.java`:
- Around line 39-88: Update LessonCompletionService.checkAndPublishIfComplete to
catch DataIntegrityViolationException specifically around
lessonCompletionRepository.save and return when a concurrent completion already
won, preserving the answer transaction. Move both completion event publications
behind an AFTER_COMMIT transactional event listener: publish an internal
lesson-completed event after the save, and handle it in a listener that invokes
the existing eventPublisher methods only after the surrounding transaction
commits.

In
`@services/quiz-service/src/main/java/cm/afrilingua/quiz/service/QuestionService.java`:
- Around line 64-90: The submitAnswer transaction currently couples saving
AnswerAttempt with lesson completion processing, so completion failures can roll
back the recorded answer. Update submitAnswer and the completion flow so the
answer attempt is committed independently, then invoke checkAndPublishIfComplete
in a separate transaction or failure-isolated path while preserving the existing
correct-answer response behavior.
- Around line 92-99: Update the private normalize method in QuestionService to
handle a null value before calling trim or Normalizer.normalize, returning the
expected null-safe result so missing submitted answers do not cause an
exception. Preserve the existing trim and NFC normalization behavior for
non-null values.

---

Nitpick comments:
In `@mobile/lib/features/auth/presentation/auth_providers.dart`:
- Around line 36-42: Update _persistSession and the corresponding restoreSession
logic to store the complete session as one JSON-encoded value under a single
storage key, using one atomic write. Encode all five AuthResponse fields
together, then decode that object during restoration and preserve the existing
session reconstruction behavior.
- Around line 79-85: Remove the unused sessionRestorationProvider declaration
and its accompanying documentation comment; keep the existing
authControllerProvider and restoreSession implementation unchanged because
main.dart already invokes restoration before runApp.

In `@services/lesson-service/scripts/generate_lessons.py`:
- Around line 24-27: Introduce one shared requests.Session() for the
lesson-generation script and reuse it across all HTTP operations. Update the
request calls in the lesson creation and word-attachment flows, including the
code around create_lesson and attach_word, to use the shared session’s get/post
methods instead of module-level requests calls; preserve existing URLs,
payloads, and response handling.

In `@services/quiz-service/scripts/generate_quizzes.py`:
- Around line 35-56: Refactor the quiz-generation request flow to create and
reuse a requests.Session, replacing the direct requests.get calls in
fetch_languages, fetch_lessons, fetch_word, and fetch_existing_question_count
with session requests and passing the session through their callers. If the
content service exposes a bulk word endpoint, update the word-fetching flow
around fetch_word to retrieve lesson words in one request and eliminate the
per-word N+1 calls.

In
`@services/quiz-service/src/main/java/cm/afrilingua/quiz/config/EventProperties.java`:
- Around line 5-6: Ensure application.events.exchange is explicitly configured
in application.yml and add a suitable `@DefaultValue` to the exchange component of
EventProperties so it cannot be null when configuration is missing. Use the same
exchange name in both places and preserve the existing configuration-properties
binding.

In `@services/quiz-service/src/main/resources/application.yml`:
- Around line 21-25: Update the RabbitMQ credentials in the rabbitmq
configuration to resolve username and password from environment variables with
the current development credentials as defaults, allowing production deployments
to override them without changing the application configuration.

In
`@services/quiz-service/src/main/resources/db/migration/V2__add_word_id_to_questions.sql`:
- Around line 1-3: Update the questions index migration to create
idx_questions_word concurrently for production-scale tables, and configure the
Flyway migration with executeInTransaction: false because concurrent index
creation cannot run in a transaction. Keep the word_id column addition unchanged
and ensure the index creation is placed in a migration that runs outside a
transaction.

In `@services/quiz-service/src/main/resources/openapi/quiz-service.yaml`:
- Around line 178-197: Add the wordId property to the QuestionWithAnswer schema,
matching the existing definition and UUID format used by CreateQuestionRequest
and Question, so question-creation responses expose the stored word association
consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2c3c81a0-9e5b-4212-95a6-d1ba6ee5d86e

📥 Commits

Reviewing files that changed from the base of the PR and between d2628f1 and 021c7a5.

📒 Files selected for processing (28)
  • mobile/lib/core/network/api_client.dart
  • mobile/lib/core/network/auth_interceptor.dart
  • mobile/lib/features/auth/presentation/auth_providers.dart
  • mobile/lib/main.dart
  • services/api-gateway/pom.xml
  • services/api-gateway/src/main/java/cm/afrilingua/gateway/ApiGatewayApplication.java
  • services/api-gateway/src/main/java/cm/afrilingua/gateway/config/JwtProperties.java
  • services/api-gateway/src/main/java/cm/afrilingua/gateway/security/JwtAuthenticationFilter.java
  • services/api-gateway/src/main/resources/application.yml
  • services/lesson-service/scripts/generate_lessons.py
  • services/quiz-service/pom.xml
  • services/quiz-service/scripts/generate_quizzes.py
  • services/quiz-service/src/main/java/cm/afrilingua/quiz/QuizServiceApplication.java
  • services/quiz-service/src/main/java/cm/afrilingua/quiz/config/EventProperties.java
  • services/quiz-service/src/main/java/cm/afrilingua/quiz/config/RabbitMQConfig.java
  • services/quiz-service/src/main/java/cm/afrilingua/quiz/entity/AnswerAttempt.java
  • services/quiz-service/src/main/java/cm/afrilingua/quiz/entity/LessonCompletion.java
  • services/quiz-service/src/main/java/cm/afrilingua/quiz/entity/Question.java
  • services/quiz-service/src/main/java/cm/afrilingua/quiz/messaging/LessonCompletionEventPublisher.java
  • services/quiz-service/src/main/java/cm/afrilingua/quiz/repository/AnswerAttemptRepository.java
  • services/quiz-service/src/main/java/cm/afrilingua/quiz/repository/LessonCompletionRepository.java
  • services/quiz-service/src/main/java/cm/afrilingua/quiz/service/LessonCompletionService.java
  • services/quiz-service/src/main/java/cm/afrilingua/quiz/service/QuestionService.java
  • services/quiz-service/src/main/resources/application.yml
  • services/quiz-service/src/main/resources/db/migration/V2__add_word_id_to_questions.sql
  • services/quiz-service/src/main/resources/db/migration/V3__add_user_id_to_answer_attempts.sql
  • services/quiz-service/src/main/resources/db/migration/V4__create_lesson_completions_table.sql
  • services/quiz-service/src/main/resources/openapi/quiz-service.yaml

Comment on lines +13 to +18
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
final token = await _storage.read(key: 'access_token');
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unhandled storage read failure can hang requests indefinitely.

If _storage.read throws (platform exception, corrupted storage, permissions change), handler.next(options) is never called and the request stalls until the Dio timeout fires. Wrap the read in a try-catch so the request proceeds without a token — the gateway will return 401, which is the correct degraded behavior.

🛡️ Proposed fix: add error handling around storage read
   `@override`
   void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
-    final token = await _storage.read(key: 'access_token');
-    if (token != null) {
-      options.headers['Authorization'] = 'Bearer $token';
+    try {
+      final token = await _storage.read(key: 'access_token');
+      if (token != null) {
+        options.headers['Authorization'] = 'Bearer $token';
+      }
+    } catch (_) {
+      // If storage is unavailable, proceed without token; gateway will reject with 401.
     }
     handler.next(options);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
final token = await _storage.read(key: 'access_token');
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
try {
final token = await _storage.read(key: 'access_token');
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
} catch (_) {
// If storage is unavailable, proceed without token; gateway will reject with 401.
}
handler.next(options);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile/lib/core/network/auth_interceptor.dart` around lines 13 - 18, Update
onRequest to catch failures from _storage.read, allowing the request to continue
without an Authorization header when token retrieval fails. Ensure
handler.next(options) is always called after the read attempt, while preserving
the existing header behavior for successfully retrieved non-null tokens.

Comment on lines +44 to +63
/// Reconstructs the session from secure storage on app startup (page
/// reload, direct URL navigation, hot restart). Without this, the
/// in-memory-only state means every reload silently logs the user out
/// even though valid tokens are still on disk.
Future<void> restoreSession() async {
final accessToken = await _storage.read(key: 'access_token');
final refreshToken = await _storage.read(key: 'refresh_token');
final accountId = await _storage.read(key: 'account_id');
final email = await _storage.read(key: 'email');
final role = await _storage.read(key: 'role');

if (accessToken != null && refreshToken != null && accountId != null && email != null && role != null) {
state = AuthResponse(
id: accountId,
accessToken: accessToken,
refreshToken: refreshToken,
email: email,
role: role,
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== files ==\n'
git ls-files 'mobile/lib/features/auth/presentation/auth_providers.dart' 'mobile/lib/main.dart'

printf '\n== outline auth_providers ==\n'
ast-grep outline mobile/lib/features/auth/presentation/auth_providers.dart --view expanded || true

printf '\n== outline main ==\n'
ast-grep outline mobile/lib/main.dart --view expanded || true

printf '\n== relevant lines auth_providers ==\n'
sed -n '1,220p' mobile/lib/features/auth/presentation/auth_providers.dart | cat -n

printf '\n== relevant lines main ==\n'
sed -n '1,160p' mobile/lib/main.dart | cat -n

Repository: ASSONDJI/afrilingua

Length of output: 5898


Guard restoreSession() against secure-storage failures
mobile/lib/main.dart awaits restoreSession() before runApp(), so any PlatformException from _storage.read(...) will abort startup and leave the app blank. Catch the reads here and fall back to a logged-out state instead of crashing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile/lib/features/auth/presentation/auth_providers.dart` around lines 44 -
63, Update Auth provider’s restoreSession() to catch secure-storage read
failures, including PlatformException, and reset or retain the logged-out state
instead of allowing the exception to propagate to startup. Keep successful token
reconstruction via AuthResponse unchanged, and use the existing logging
mechanism if available to record the failure.

<properties>
<java.version>21</java.version>
<spring-cloud.version>2023.0.3</spring-cloud.version>
<jjwt.version>0.12.6</jjwt.version>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check Maven Central for the latest JJWT 0.12.x release
curl -s "https://repo1.maven.org/maven2/io/jsonwebtoken/jjwt-api/maven-metadata.xml" | grep -oP '<version>0\.12\.\d+</version>' | sort -V | tail -5

# Check GitHub advisories for JJWT
gh api graphql -f query='
{
  securityVulnerabilities(first: 5, ecosystem: MAVEN, package: "io.jsonwebtoken:jjwt-api") {
    nodes {
      advisory { summary severity }
      vulnerableVersionRange
      firstPatchedVersion { identifier }
    }
  }
}'

Repository: ASSONDJI/afrilingua

Length of output: 336


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for artifact in jjwt-api jjwt-impl jjwt-jackson; do
  echo "== $artifact =="
  curl -fsS "https://repo1.maven.org/maven2/io/jsonwebtoken/${artifact}/maven-metadata.xml" \
    | grep -oP '<version>0\.12\.\d+</version>' \
    | sort -V \
    | tail -5
done

Repository: ASSONDJI/afrilingua

Length of output: 597


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,120p' services/api-gateway/pom.xml | cat -n

Repository: ASSONDJI/afrilingua

Length of output: 3634


Bump JJWT to 0.12.70.12.6 is behind the current 0.12.x patch; update the version property and keep jjwt-api, jjwt-impl, and jjwt-jackson aligned.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/api-gateway/pom.xml` at line 23, Update the jjwt.version property
from 0.12.6 to 0.12.7 in the Maven configuration, ensuring jjwt-api, jjwt-impl,
and jjwt-jackson continue resolving the same aligned version through this
property.

Comment on lines +61 to +82
try {
Claims claims = Jwts.parser()
.verifyWith(signingKey)
.build()
.parseSignedClaims(token)
.getPayload();

String userId = claims.getSubject();

ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
.headers(headers -> {
headers.remove("Authorization");
headers.remove("X-User-Id"); // never trust a client-supplied value
headers.set("X-User-Id", userId);
})
.build();

return chain.filter(exchange.mutate().request(mutatedRequest).build());
} catch (JwtException | IllegalArgumentException e) {
return unauthorized(exchange, "Invalid or expired token");
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Unvalidated null subject propagates into X-User-Id.

claims.getSubject() (line 68) isn't checked for null/blank before being set as X-User-Id (line 74). If a token is ever parsed without a sub claim (malformed/legacy token, misconfigured issuer), downstream services would receive either a missing header or a header literally set to "null", and QuestionService.extractUserId() (which does UUID.fromString(userId)) would throw on a non-UUID value rather than the request being rejected here at the gateway boundary.

🛡️ Suggested defensive check
             String userId = claims.getSubject();
+            if (userId == null || userId.isBlank()) {
+                return unauthorized(exchange, "Invalid or expired token");
+            }

JWT parsing itself (Jwts.parser().verifyWith(signingKey).build().parseSignedClaims(token)) correctly follows the jjwt 0.12.x API.

Note: the static analysis hint about a "randomly-generated IV" on line 91 is a false positive — that line only builds a JSON error string, no cryptographic/CBC operation is involved.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
Claims claims = Jwts.parser()
.verifyWith(signingKey)
.build()
.parseSignedClaims(token)
.getPayload();
String userId = claims.getSubject();
ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
.headers(headers -> {
headers.remove("Authorization");
headers.remove("X-User-Id"); // never trust a client-supplied value
headers.set("X-User-Id", userId);
})
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
} catch (JwtException | IllegalArgumentException e) {
return unauthorized(exchange, "Invalid or expired token");
}
}
try {
Claims claims = Jwts.parser()
.verifyWith(signingKey)
.build()
.parseSignedClaims(token)
.getPayload();
String userId = claims.getSubject();
if (userId == null || userId.isBlank()) {
return unauthorized(exchange, "Invalid or expired token");
}
ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
.headers(headers -> {
headers.remove("Authorization");
headers.remove("X-User-Id"); // never trust a client-supplied value
headers.set("X-User-Id", userId);
})
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
} catch (JwtException | IllegalArgumentException e) {
return unauthorized(exchange, "Invalid or expired token");
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@services/api-gateway/src/main/java/cm/afrilingua/gateway/security/JwtAuthenticationFilter.java`
around lines 61 - 82, Validate the subject returned by claims.getSubject() in
JwtAuthenticationFilter before mutating the request: reject null or blank values
through the existing unauthorized flow instead of setting X-User-Id. Preserve
the current header replacement and valid-token behavior for nonblank subjects.

Comment on lines +73 to +77
include: health,gateway
application:
security:
jwt:
secret-key: ${JWT_SECRET_KEY:404E635266556A586E3272357538782F413F4428472B4B6250645367566B59}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Hardcoded JWT secret default checked into source control.

${JWT_SECRET_KEY:404E635266556A586E3272357538782F413F4428472B4B6250645367566B59} bakes a fixed fallback secret directly into the repo. Anyone with source access can read this value and forge valid JWTs for any user if JWT_SECRET_KEY isn't set in an environment (misconfigured deploy, forgotten env var, local/staging environment mistakenly treated as trusted). Given JwtAuthenticationFilter is documented as "the single point of authentication for the whole platform," a leaked/guessable static secret defeats the entire auth model.

Fail fast instead of silently falling back to a checked-in secret:

🔒 Suggested fix
 application:
   security:
     jwt:
-      secret-key: ${JWT_SECRET_KEY:404E635266556A586E3272357538782F413F4428472B4B6250645367566B59}
+      secret-key: ${JWT_SECRET_KEY}

Note: this must be coordinated with auth-service's equivalent property, which per the JwtProperties comment must share the exact same secret.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
include: health,gateway
application:
security:
jwt:
secret-key: ${JWT_SECRET_KEY:404E635266556A586E3272357538782F413F4428472B4B6250645367566B59}
include: health,gateway
application:
security:
jwt:
secret-key: ${JWT_SECRET_KEY}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/api-gateway/src/main/resources/application.yml` around lines 73 -
77, Remove the hardcoded fallback from the JWT secret-key configuration and
require JWT_SECRET_KEY to be explicitly provided, failing application startup
when it is absent. Apply the same required-secret configuration to
auth-service’s equivalent JwtProperties setting, preserving the exact shared
secret contract between both services.

lessons = fetch_lessons(language["id"])
total_questions, total_lessons = 0, 0
for lesson in lessons:
q, l = generate_for_lesson(language["name"], lesson, args.force)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Rename ambiguous variable l to satisfy Ruff E741.

Ruff flags l as an ambiguous variable name (E741). Use a descriptive name instead.

💚 Proposed fix
         for lesson in lessons:
-            q, l = generate_for_lesson(language["name"], lesson, args.force)
+            q, processed = generate_for_lesson(language["name"], lesson, args.force)
             total_questions += q
-            total_lessons += l
+            total_lessons += processed
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
q, l = generate_for_lesson(language["name"], lesson, args.force)
for lesson in lessons:
q, processed = generate_for_lesson(language["name"], lesson, args.force)
total_questions += q
total_lessons += processed
🧰 Tools
🪛 Ruff (0.15.20)

[error] 120-120: Ambiguous variable name: l

(E741)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/quiz-service/scripts/generate_quizzes.py` at line 120, In the caller
assigning the result of generate_for_lesson, rename the ambiguous variable l to
a descriptive name such as lesson data while preserving the existing q value and
downstream behavior.

Source: Linters/SAST tools

Comment on lines +39 to +88
public void checkAndPublishIfComplete(UUID userId, UUID lessonId) {
if (userId == null) {
return; // no trusted identity (e.g. request bypassed the gateway) -- skip silently
}

// Idempotency guard: once this (user, lesson) pair is recorded, never
// re-publish -- without this, every correct answer submitted AFTER
// the lesson was already completed (e.g. re-answering a question
// that was already correct) would re-fire both events and
// re-award XP/badges.
if (lessonCompletionRepository.existsByUserIdAndLessonId(userId, lessonId)) {
return;
}

List<Question> lessonQuestions = questionRepository.findByLessonId(lessonId);
if (lessonQuestions.isEmpty()) {
return;
}

List<UUID> questionIds = lessonQuestions.stream().map(Question::getId).toList();

Set<UUID> correctlyAnsweredQuestionIds = answerAttemptRepository
.findByUserIdAndQuestionIdInAndIsCorrectTrue(userId, questionIds)
.stream()
.map(cm.afrilingua.quiz.entity.AnswerAttempt::getQuestionId)
.collect(Collectors.toSet());

boolean allAnswered = questionIds.stream().allMatch(correctlyAnsweredQuestionIds::contains);
if (!allAnswered) {
return;
}

List<UUID> correctWordIds = lessonQuestions.stream()
.map(Question::getWordId)
.filter(java.util.Objects::nonNull)
.distinct()
.toList();

// Accurate perfection check: true only if NO wrong attempt exists for
// any question in this lesson, not just "eventually got it right".
boolean isPerfect = !answerAttemptRepository
.existsByUserIdAndQuestionIdInAndIsCorrectFalse(userId, questionIds);

lessonCompletionRepository.save(
LessonCompletion.builder().userId(userId).lessonId(lessonId).build()
);

eventPublisher.publishLessonCompleted(userId);
eventPublisher.publishQuizCompleted(userId, correctWordIds, isPerfect);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

Events are published before the transaction commits — rollback causes duplicate XP/badges and silent answer loss.

checkAndPublishIfComplete is called within QuestionService.submitAnswer's @Transactional boundary. The eventPublisher.publish* calls (lines 86–87) send messages to RabbitMQ before the transaction commits. Two failure modes result:

  1. Duplicate events on rollback: If the transaction fails to commit after events are published (e.g., unique constraint violation from a concurrent request, connection drop), the events are already in RabbitMQ. The LessonCompletion record is not persisted, so the next correct answer passes the idempotency check (line 49) and re-publishes events — awarding duplicate XP/badges.

  2. Answer attempt loss on concurrent completion: Two concurrent requests for the last questions of a lesson can both pass the existsByUserIdAndLessonId check (lines 49–51). One succeeds; the other fails with a unique constraint violation on lessonCompletionRepository.save() (line 82). This exception rolls back the entire submitAnswer transaction, including the AnswerAttempt save in QuestionService — the user's correct answer is silently lost.

Recommended fix: Decouple event publishing from the answer-submission transaction using @TransactionalEventListener(phase = AFTER_COMMIT) so events fire only after a successful commit. Additionally, catch DataIntegrityViolationException on the save call to handle the TOCTOU race gracefully — the completion already exists from the winning request, so simply return.

🔧 Proposed approach using TransactionalEventListener
 // In LessonCompletionService: instead of calling eventPublisher directly,
 // publish a Spring application event within the transaction.

+import org.springframework.context.ApplicationEventPublisher;
 // ...
+private final ApplicationEventPublisher applicationEventPublisher;

 // Replace lines 86-87:
-eventPublisher.publishLessonCompleted(userId);
-eventPublisher.publishQuizCompleted(userId, correctWordIds, isPerfect);
+applicationEventPublisher.publishEvent(
+    new LessonCompletedEvent(userId, correctWordIds, isPerfect));

 // Catch constraint violations from concurrent requests:
 try {
     lessonCompletionRepository.save(
         LessonCompletion.builder().userId(userId).lessonId(lessonId).build()
     );
+} catch (org.springframework.dao.DataIntegrityViolationException e) {
+    return; // completion already recorded by a concurrent request
 }

 // New event class + listener in LessonCompletionEventPublisher:
+public record LessonCompletedEvent(UUID userId, List<UUID> correctWordIds, boolean isPerfect) {}

+@org.springframework.transaction.event.TransactionalEventListener(phase = org.springframework.transaction.event.TransactionPhase.AFTER_COMMIT)
+public void onLessonCompleted(LessonCompletedEvent event) {
+    publishLessonCompleted(event.userId());
+    publishQuizCompleted(event.userId(), event.correctWordIds(), event.isPerfect());
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@services/quiz-service/src/main/java/cm/afrilingua/quiz/service/LessonCompletionService.java`
around lines 39 - 88, Update LessonCompletionService.checkAndPublishIfComplete
to catch DataIntegrityViolationException specifically around
lessonCompletionRepository.save and return when a concurrent completion already
won, preserving the answer transaction. Move both completion event publications
behind an AFTER_COMMIT transactional event listener: publish an internal
lesson-completed event after the save, and handle it in a listener that invokes
the existing eventPublisher methods only after the surrounding transaction
commits.

Comment on lines +64 to 90
@Transactional
public AnswerResult submitAnswer(UUID questionId, SubmitAnswerRequest request) {
cm.afrilingua.quiz.entity.Question question = questionRepository.findById(questionId)
.orElseThrow(() -> new QuestionNotFoundException(questionId));

boolean isCorrect = question.getCorrectAnswer()
.trim().equalsIgnoreCase(request.getSubmittedAnswer().trim());
boolean isCorrect = normalize(question.getCorrectAnswer())
.equalsIgnoreCase(normalize(request.getSubmittedAnswer()));

UUID userId = extractUserId();

AnswerAttempt attempt = AnswerAttempt.builder()
.questionId(question.getId())
.userId(userId)
.submittedAnswer(request.getSubmittedAnswer())
.isCorrect(isCorrect)
.build();
answerAttemptRepository.save(attempt);

if (isCorrect) {
lessonCompletionService.checkAndPublishIfComplete(userId, question.getLessonId());
}

return new AnswerResult()
.questionId(question.getId())
.correct(isCorrect)
.correctAnswer(question.getCorrectAnswer());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Answer attempt and completion check share a single transaction — failures in completion roll back the attempt.

submitAnswer is @Transactional and calls checkAndPublishIfComplete within the same transaction. If the completion check throws (e.g., unique constraint violation from a concurrent request, or RabbitMQ connectivity failure), the AnswerAttempt save at line 80 is also rolled back — the user's correct answer is silently lost. See the detailed analysis in the LessonCompletionService review comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@services/quiz-service/src/main/java/cm/afrilingua/quiz/service/QuestionService.java`
around lines 64 - 90, The submitAnswer transaction currently couples saving
AnswerAttempt with lesson completion processing, so completion failures can roll
back the recorded answer. Update submitAnswer and the completion flow so the
answer attempt is committed independently, then invoke checkAndPublishIfComplete
in a separate transaction or failure-isolated path while preserving the existing
correct-answer response behavior.

Comment on lines +92 to +99
/** Normalizes to NFC before comparison: the words dataset (Yemba tones in
* particular) can be stored with combining diacritics (NFD) while user
* input -- or even the reference data itself -- may use precomposed
* characters (NFC). Without this, visually identical strings compare
* as different, silently marking correct answers as wrong. */
private String normalize(String value) {
return Normalizer.normalize(value.trim(), Normalizer.Form.NFC);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the request model and controller references
git ls-files | rg 'services/quiz-service/src/main/java/.*QuestionService\.java|SubmitAnswerRequest|Quiz.*Controller|Answer.*Controller|openapi|swagger|api'

# Search for submittedAnswer annotations and validation usage
rg -n --hidden -S 'submittedAnswer|`@NotNull`|`@Valid`|`@Validated`|`@RequestBody`' services/quiz-service/src/main/java services/quiz-service/src/main/resources

# Show the relevant request class if present
fd -a 'SubmitAnswerRequest.*' services/quiz-service/src/main/java services/quiz-service/src/main/resources

Repository: ASSONDJI/afrilingua

Length of output: 2015


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the quiz-service source tree
ast-grep outline services/quiz-service/src/main/java/cm/afrilingua/quiz/service/QuestionService.java --view expanded || true
ast-grep outline services/quiz-service/src/main/resources/openapi/quiz-service.yaml --view expanded || true

# Inspect the OpenAPI contract around submit answer
sed -n '120,170p' services/quiz-service/src/main/resources/openapi/quiz-service.yaml

# Find the request model and controller handling submit-answer
rg -n --hidden -S 'class SubmitAnswerRequest|submittedAnswer|`@NotNull`|`@Valid`|`@Validated`|`@RequestBody`|submitAnswer' services/quiz-service/src/main/java services/quiz-service/src/main/resources/openapi/quiz-service.yaml

Repository: ASSONDJI/afrilingua

Length of output: 3217


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the controller and generated/request model definitions
sed -n '1,120p' services/quiz-service/src/main/java/cm/afrilingua/quiz/controller/QuestionController.java

# Find the request class if it exists in source
fd -a 'SubmitAnswerRequest.java' services/quiz-service/src/main/java services/quiz-service/target services/quiz-service/src/generated || true

# Search for bean-validation annotations around the request model and controller package
rg -n --hidden -S '`@Valid`|`@Validated`|`@NotNull`|`@RequestBody`|class SubmitAnswerRequest|interface SubmitAnswerRequest' services/quiz-service/src/main/java services/quiz-service/src/generated

Repository: ASSONDJI/afrilingua

Length of output: 1887


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the API interface that QuestionController implements
sed -n '1,240p' services/quiz-service/src/main/java/cm/afrilingua/quiz/api/QuizzesApi.java

# Look for global validation/configuration in quiz-service
rg -n --hidden -S 'MethodValidationPostProcessor|`@Validated`|`@ControllerAdvice`|`@ExceptionHandler`|ConstraintViolation|Validating|BeanValidation|Validation' services/quiz-service/src/main/java services/quiz-service/src/main/resources

# Find the request DTO definition
rg -n --hidden -S 'class SubmitAnswerRequest|record SubmitAnswerRequest|interface SubmitAnswerRequest' services/quiz-service/src/main/java

Repository: ASSONDJI/afrilingua

Length of output: 275


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the generated API and DTO sources
git ls-files | rg 'services/quiz-service/.*/QuizzesApi\.java|services/quiz-service/.*/SubmitAnswerRequest\.java|services/quiz-service/.*/CreateQuestionRequest\.java|services/quiz-service/.*/AnswerResult\.java'

# Inspect the quiz-service module build/codegen setup for validation generation
sed -n '1,260p' services/quiz-service/pom.xml

# If found, inspect the request DTO source
for f in $(git ls-files | rg 'services/quiz-service/.*/SubmitAnswerRequest\.java$'); do
  echo "### $f"
  sed -n '1,220p' "$f"
done

Repository: ASSONDJI/afrilingua

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect quiz-service build/codegen configuration for validation generation
sed -n '1,260p' services/quiz-service/pom.xml

# Search for OpenAPI generator settings in the service and parent build files
rg -n --hidden -S 'openapi|swagger|generator|useBeanValidation|useJakartaEe|useSpringBoot3|validation|NotNull|NotBlank' \
  services/quiz-service/pom.xml pom.xml .github .mvn services/quiz-service/src/main/resources

Repository: ASSONDJI/afrilingua

Length of output: 7473


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find any generated Java sources for quiz-service outside git-tracked files
fd -a 'SubmitAnswerRequest.java' .
fd -a 'QuizzesApi.java' .
fd -a 'QuestionController.java' services/quiz-service/src/main/java

# Inspect any matching request model if present
for f in $(fd -a 'SubmitAnswerRequest.java' .); do
  echo "### $f"
  sed -n '1,220p' "$f"
done

Repository: ASSONDJI/afrilingua

Length of output: 266


🌐 Web query:

openapi-generator spring generator useBeanValidation default interfaceOnly @Valid required field generated model

💡 Result:

To generate models with Bean Validation (such as @NotNull for required fields) and apply @Valid annotations when using the OpenAPI Generator Spring generator, you should configure the useBeanValidation option to true [1][2][3]. Key Configuration Details: 1. Enable Bean Validation: Set the useBeanValidation configuration option to true in your generator configuration (e.g., in your pom.xml or build.gradle) [2][3][4]. This ensures that the generator includes Jakarta/Javax validation annotations (like @NotNull, @Size, etc.) on model fields based on your OpenAPI specification [3]. 2. Handling interfaceOnly: The useBeanValidation option works in conjunction with interfaceOnly. Recent updates (as of early 2026) have improved support for this, particularly within the spring-http-interface library, allowing the generator to respect your Bean Validation settings rather than forcing them to false [2][3]. 3. Applying @Valid: When useBeanValidation is enabled, the generator typically applies @Valid annotations to request bodies and controller method parameters to trigger the validation process when the request is processed by the Spring framework [2][3]. If you are using the spring-http-interface library, ensure you are on a recent version of the generator (post-April/May 2026), as previous versions incorrectly forced these validation settings to false [2][3][4]. Enabling useBeanValidation will now correctly generate the required validation annotations and include necessary dependencies (e.g., spring-boot-starter-validation) [2][3]. Example Configuration (Maven): spring true true By setting useBeanValidation to true, the generator will inspect your OpenAPI schema for required fields and automatically apply the appropriate @NotNull annotations to the corresponding fields in the generated Java models [3].

Citations:


🌐 Web query:

Spring MVC interface method parameter annotations @Valid on generated OpenAPI interfaceOnly controller validation

💡 Result:

When using the OpenAPI Generator for Spring with the interfaceOnly option, support for @Valid and validation annotations depends on specific configuration flags in your plugin settings. To enable validation in generated interfaces, you must set useBeanValidation to true in your configuration options [1][2]. This ensures the generator includes necessary Jakarta Validation annotations (e.g., @NotNull, @Size) on model fields and adds @Valid to method parameters in the generated interface [3][4]. Key configuration considerations: 1. Enable Bean Validation: Set true in your configOptions [1][2]. 2. Use Jakarta EE: Because modern Spring Boot (3.x+) relies on Jakarta Validation, ensure you set true to avoid namespace conflicts (using javax vs. jakarta packages) [5]. 3. Class-Level @Validated: By default, the generator may place @Validated at the class level [6][2]. For Spring Framework 6.x and later, this is often discouraged. You can use the option true to disable this class-level annotation if you prefer to rely on Spring's built-in validation mechanism [7][2]. 4. Dependency Management: Ensure your project includes the spring-boot-starter-validation dependency [3][4]. Note that even if useBeanValidation is set to false, some generated models may still contain validation imports, which can cause compilation errors if the validation dependency is missing [8]. If validation is not working as expected, verify that you are not overriding these behaviors with conflicting model templates or additional annotations (like Lombok, which can sometimes interfere with generation) [5].

Citations:


Guard normalize() against null. submittedAnswer is only marked required in the OpenAPI schema; this path still has no explicit @Valid/@NotNull enforcement, so a missing value can reach value.trim() and turn into a 500.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@services/quiz-service/src/main/java/cm/afrilingua/quiz/service/QuestionService.java`
around lines 92 - 99, Update the private normalize method in QuestionService to
handle a null value before calling trim or Normalizer.normalize, returning the
expected null-safe result so missing submitted answers do not cause an
exception. Preserve the existing trim and NFC normalization behavior for
non-null values.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant