funifier-mcp 0.2.26 → 0.2.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/.cursor/rules/funifier.mdc +38 -41
  2. package/.github/copilot-instructions.md +38 -41
  3. package/AGENTS.md +56 -49
  4. package/README.md +40 -22
  5. package/datasource-funifier-docs/.coverage.json +326 -0
  6. package/datasource-funifier-docs/.validation.json +593 -0
  7. package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
  8. package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
  9. package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
  10. package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
  11. package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
  12. package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
  13. package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
  14. package/datasource-funifier-docs/knowledge/index.md +4 -1
  15. package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
  16. package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
  17. package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
  18. package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
  19. package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
  20. package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
  21. package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
  22. package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
  23. package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
  24. package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
  25. package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
  26. package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
  27. package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
  28. package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
  29. package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
  30. package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
  31. package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
  32. package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
  33. package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
  34. package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
  35. package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
  36. package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
  37. package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
  38. package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
  39. package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
  40. package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
  41. package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
  42. package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
  43. package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
  44. package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
  45. package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
  46. package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
  47. package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
  48. package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
  49. package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
  50. package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
  51. package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
  52. package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
  53. package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
  54. package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
  55. package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
  56. package/dist/cli/init.d.ts.map +1 -1
  57. package/dist/cli/init.js +42 -1
  58. package/dist/cli/init.js.map +1 -1
  59. package/dist/cli/init.test.js +74 -3
  60. package/dist/cli/init.test.js.map +1 -1
  61. package/dist/cli/persona.d.ts +3 -0
  62. package/dist/cli/persona.d.ts.map +1 -0
  63. package/dist/cli/persona.js +25 -0
  64. package/dist/cli/persona.js.map +1 -0
  65. package/dist/mcp/bundle.js +119 -93
  66. package/dist/mcp/check-update.d.ts +5 -0
  67. package/dist/mcp/check-update.d.ts.map +1 -1
  68. package/dist/mcp/check-update.js +21 -10
  69. package/dist/mcp/check-update.js.map +1 -1
  70. package/dist/mcp/check-update.test.d.ts +2 -0
  71. package/dist/mcp/check-update.test.d.ts.map +1 -0
  72. package/dist/mcp/check-update.test.js +33 -0
  73. package/dist/mcp/check-update.test.js.map +1 -0
  74. package/dist/mcp/index.js +2 -2
  75. package/dist/mcp/index.js.map +1 -1
  76. package/dist/mcp/prompts/templates.d.ts.map +1 -1
  77. package/dist/mcp/prompts/templates.js +35 -0
  78. package/dist/mcp/prompts/templates.js.map +1 -1
  79. package/dist/mcp/resources/documentation.d.ts +1 -1
  80. package/dist/mcp/resources/documentation.d.ts.map +1 -1
  81. package/dist/mcp/resources/documentation.js +39 -3
  82. package/dist/mcp/resources/documentation.js.map +1 -1
  83. package/dist/mcp/tools/connect.d.ts.map +1 -1
  84. package/dist/mcp/tools/connect.js +18 -8
  85. package/dist/mcp/tools/connect.js.map +1 -1
  86. package/dist/mcp/tools/database.d.ts.map +1 -1
  87. package/dist/mcp/tools/database.js +59 -47
  88. package/dist/mcp/tools/database.js.map +1 -1
  89. package/dist/mcp/tools/database.test.js +2 -2
  90. package/dist/mcp/tools/database.test.js.map +1 -1
  91. package/dist/mcp/tools/delete.d.ts.map +1 -1
  92. package/dist/mcp/tools/delete.js +13 -3
  93. package/dist/mcp/tools/delete.js.map +1 -1
  94. package/dist/mcp/tools/execute.d.ts.map +1 -1
  95. package/dist/mcp/tools/execute.js +20 -9
  96. package/dist/mcp/tools/execute.js.map +1 -1
  97. package/dist/mcp/tools/folder.d.ts.map +1 -1
  98. package/dist/mcp/tools/folder.js +22 -12
  99. package/dist/mcp/tools/folder.js.map +1 -1
  100. package/dist/mcp/tools/get.d.ts.map +1 -1
  101. package/dist/mcp/tools/get.js +16 -6
  102. package/dist/mcp/tools/get.js.map +1 -1
  103. package/dist/mcp/tools/index.d.ts +1 -1
  104. package/dist/mcp/tools/index.d.ts.map +1 -1
  105. package/dist/mcp/tools/index.js +28 -1
  106. package/dist/mcp/tools/index.js.map +1 -1
  107. package/dist/mcp/tools/list.d.ts.map +1 -1
  108. package/dist/mcp/tools/list.js +38 -14
  109. package/dist/mcp/tools/list.js.map +1 -1
  110. package/dist/mcp/tools/logs.d.ts.map +1 -1
  111. package/dist/mcp/tools/logs.js +15 -5
  112. package/dist/mcp/tools/logs.js.map +1 -1
  113. package/dist/mcp/tools/save.d.ts.map +1 -1
  114. package/dist/mcp/tools/save.js +14 -4
  115. package/dist/mcp/tools/save.js.map +1 -1
  116. package/dist/mcp/tools/save.test.js +3 -3
  117. package/dist/mcp/tools/save.test.js.map +1 -1
  118. package/dist/mcp/tools/search-docs.d.ts +3 -0
  119. package/dist/mcp/tools/search-docs.d.ts.map +1 -0
  120. package/dist/mcp/tools/search-docs.js +102 -0
  121. package/dist/mcp/tools/search-docs.js.map +1 -0
  122. package/package.json +6 -2
  123. package/skills/acquire-funifier-knowledge/SKILL.md +155 -0
  124. package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
  125. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
  126. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
  127. package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
  128. package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
  129. package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
  130. package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +86 -0
  131. package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
  132. package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
  133. package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
  134. package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
  135. package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
  136. package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
  137. package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
  138. package/skills/funifier/SKILL.md +88 -0
  139. package/skills/funifier/references/configure-security.md +96 -0
  140. package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
  141. package/skills/funifier/references/create-aggregate.md +144 -0
  142. package/skills/funifier/references/create-challenge.md +116 -0
  143. package/skills/funifier/references/create-competition.md +98 -0
  144. package/skills/funifier/references/create-crossword.md +574 -0
  145. package/skills/funifier/references/create-custom-object.md +91 -0
  146. package/skills/funifier/references/create-custom-page.md +135 -0
  147. package/skills/funifier/references/create-folder.md +104 -0
  148. package/skills/funifier/references/create-lastmile.md +643 -0
  149. package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
  150. package/skills/funifier/references/create-level.md +94 -0
  151. package/skills/funifier/references/create-lottery.md +913 -0
  152. package/skills/funifier/references/create-mystery.md +769 -0
  153. package/skills/funifier/references/create-notification.md +75 -0
  154. package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
  155. package/skills/funifier/references/create-quiz.md +98 -0
  156. package/skills/funifier/references/create-scheduler.md +141 -0
  157. package/skills/funifier/references/create-story.md +636 -0
  158. package/skills/funifier/references/create-swap.md +95 -0
  159. package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
  160. package/skills/funifier/references/create-virtual-good.md +96 -0
  161. package/skills/funifier/references/create-webhook.md +72 -0
  162. package/skills/funifier/references/create-websocket.md +71 -0
  163. package/skills/funifier/references/create-widget.md +76 -0
  164. package/skills/funifier/references/debug.md +87 -0
  165. package/skills/funifier/references/help.md +81 -0
  166. package/skills/funifier/references/implement-frontend.md +106 -0
  167. package/skills/funifier/references/import-csv.md +75 -0
  168. package/skills/funifier/references/manage-player.md +82 -0
  169. package/skills/funifier/references/manage-team.md +76 -0
  170. package/skills/funifier/references/upload-file.md +91 -0
  171. package/skills/funifier-create-aggregate/SKILL.md +0 -127
  172. package/skills/funifier-create-challenge/SKILL.md +0 -88
  173. package/skills/funifier-create-custom-page/SKILL.md +0 -127
  174. package/skills/funifier-create-level/SKILL.md +0 -87
  175. package/skills/funifier-create-quiz/SKILL.md +0 -87
  176. package/skills/funifier-create-scheduler/SKILL.md +0 -127
  177. package/skills/funifier-create-virtual-good/SKILL.md +0 -87
  178. package/skills/funifier-debug/SKILL.md +0 -92
  179. package/skills/funifier-help/SKILL.md +0 -86
  180. package/skills/funifier-implement-frontend/SKILL.md +0 -90
  181. package/skills/funifier-index/SKILL.md +0 -58
@@ -1,163 +1,608 @@
1
- # Quiz (Quiz)
1
+ # `quiz`
2
2
 
3
3
  **Acesso Studio:** `/studio/quiz`
4
4
  **API Endpoint:** `/v3/quiz`
5
+ **Endpoint Legado:** não existe (apenas v3)
6
+ **Coleção MongoDB:** `quiz` (configuração) + `quiz_log` (tentativas dos jogadores)
5
7
 
6
- ## O que é
8
+ > Documentação de engenharia reversa baseada no código real de `funifier-service`
9
+ > (`com.funifier.engine.quiz.*`, `com.funifier.rest.v3.rest.QuizRest`). Onde o comportamento
10
+ > do runtime diverge do schema/documentação anterior, o fato está registrado explicitamente.
7
11
 
8
- Agrupamento de perguntas em formato de prova ou simulado. Permite criar provas compostas por perguntas, definir nota total, controlar tentativas e embaralhar questões para cada jogador. Os quizzes podem ser apresentados como mini games.
12
+ ---
9
13
 
10
- ## Quando usar
14
+ ## 1. Visão Geral
11
15
 
12
- - Para criar provas e avaliações
13
- - Para simulados com nota
14
- - Para treinamentos com pontuação
15
- - Para concursos de conhecimento
16
+ O módulo `quiz` agrupa um conjunto de perguntas (`question`) em uma "prova" e gerencia o ciclo de uma **tentativa** de um jogador: iniciar → responder → finalizar → calcular nota → registrar uma `action_log`.
16
17
 
17
- ## Dependências
18
+ Papel arquitetural:
18
19
 
19
- - **Question**: perguntas devem ser criadas para compor o quiz
20
+ - A coleção `quiz` guarda **apenas a configuração** da prova (título, nota máxima, feedbacks por faixa de nota, limite de tentativas, ação a disparar). **Ela não contém as perguntas.** As perguntas vivem na coleção `question` e apontam para o quiz pelo campo `Question.quiz` (relação inversa — vide §3.1).
21
+ - A coleção `quiz_log` é o **log de tentativas**: cada documento representa uma tentativa de um jogador em um quiz, com data de início (`started`), data de fim (`finished`) e os totais consolidados (`percent`, `grade`, `questionsGrade`, `questions`, `answers`).
22
+ - A **avaliação por questão** não acontece no módulo `quiz` — acontece no módulo `question` (`QuestionManager.answer`), que calcula `QuestionLog.percent ∈ [0,1]` e `QuestionLog.grade`. O módulo `quiz` apenas **agrega** esses valores ao finalizar a tentativa.
20
23
 
21
- ## Checklist de Configuração no Studio
24
+ Problemas que resolve:
22
25
 
23
- - [ ] Definir título e descrição do quiz
24
- - [ ] Definir nota total (grade)
25
- - [ ] Criar perguntas associadas ao quiz
26
- - [ ] Configurar embaralhamento (shuffle)
27
- - [ ] Definir se mostra nota antes de finalizar (showGradeBeforeFinish)
28
- - [ ] Configurar numeração das questões (questionNumbering)
26
+ - Provas, simulados, mini-testes e quizzes de conhecimento com nota.
27
+ - Controle de quantas vezes um quiz pode ser respondido (`Limit`, por jogador ou por gamificação, com janela de tempo).
28
+ - Disparo de uma `action_log` ao finalizar (integração com o motor de gamificação — pontos, desafios, etc. via `quiz.action`).
29
+ - Feedback textual por faixa de nota (`feedbacks` com `grade boundary`).
29
30
 
30
- ## API Endpoints
31
+ Relação com outros módulos:
31
32
 
32
- ### Listar Quizzes
33
- **Método:** GET
34
- **Endpoint:** `/v3/database/quiz`
33
+ - **`question` / `question_log`** — fonte real das notas. Ao responder uma questão vinculada a um `quiz_log`, o `QuestionManager` chama de volta `QuizManager.updateLog(quiz_log)` (vide §2.4).
34
+ - **`action` / `achievement`** — `finishLog` monta uma `ActionLog` (`actionId = quiz.action`) e a registra via `ActionManager.trackWithRestrictions`, alimentando o motor de gamificação.
35
+ - **`trigger`** — eventos `before_create`/`after_create`/`before_update`/`after_update` são disparados sobre as coleções `quiz` e `quiz_log` (vide §6).
36
+ - **`player`** — usado para resolver `player` ("me" → token) e para parametrizar fórmulas de `Limit` e mensagens de feedback (Mustache).
37
+ - **AI (`/v3/ai/build/quiz`)** — gera *texto* de configuração de quiz via OpenAI; **não persiste nada** (vide §7).
35
38
 
36
- ### Criar Quiz
37
- **Método:** POST
38
- **Endpoint:** `/v3/quiz`
39
+ ---
40
+
41
+ ## 2. Arquitetura e Fluxos
42
+
43
+ ### 2.1 Classes envolvidas
44
+
45
+ | Classe | Arquivo | Papel |
46
+ |---|---|---|
47
+ | `com.funifier.engine.quiz.Quiz` | `engine/quiz/Quiz.java` | Entidade de configuração (documento raiz da coleção `quiz`) |
48
+ | `com.funifier.engine.quiz.QuizLog` | `engine/quiz/QuizLog.java` | Entidade da tentativa (documento raiz da coleção `quiz_log`) |
49
+ | `com.funifier.engine.quiz.Feedback` | `engine/quiz/Feedback.java` | Feedback por faixa de nota (`grade` + `message`) |
50
+ | `com.funifier.engine.quiz.QuizManager` | `engine/quiz/QuizManager.java` | Manager: CRUD, start/finish, cálculo de nota, agregação |
51
+ | `com.funifier.rest.v3.rest.QuizRest` | `rest/v3/rest/QuizRest.java` | Controller REST v3 (`/v3/quiz`) |
52
+ | `com.funifier.engine.quiz.settings.QuizSettings` | `engine/quiz/settings/QuizSettings.java` | Settings de UI (feedback/stuck) — **persistido mas não aplicado** (§7) |
53
+ | `com.funifier.engine.quiz.settings.FeedbackSettings` | idem | Subobjeto de UI — não aplicado pelo servidor |
54
+ | `com.funifier.engine.quiz.settings.StuckSettings` | idem | Subobjeto de UI — não aplicado pelo servidor |
55
+ | `com.funifier.engine.quiz.settings.FeedbackDefaults` | idem | Helpers de default — **sem chamadores** (código morto, §7) |
56
+ | `com.funifier.engine.action.Limit` | `engine/action/Limit.java` | Limite de tentativas (reaproveitado de `action`) |
57
+
58
+ Não há `Repository`/`Dao` dedicado — toda manipulação MongoDB é feita via `Jongo` dentro de `QuizManager`. Não há scheduler/job que processe o módulo `quiz`.
59
+
60
+ ### 2.2 Pipeline principal (sequência de uma tentativa)
61
+
62
+ A ordem real de chamadas, do início ao fim de uma tentativa:
39
63
 
40
- **Exemplo de Body:**
41
- ```json
42
- {
43
- "_id": "650c82fe832",
44
- "title": "Funifier Exam",
45
- "description": "Test your knowledge about the best gamification platform in the world.",
46
- "grade": 10,
47
- "i18n": {},
48
- "extra": {},
49
- "feedbacks": [],
50
- "questionNumbering": "uppercase_letters",
51
- "showGradeBeforeFinish": false,
52
- "shuffle": false
53
- }
54
64
  ```
65
+ 1. [Admin] POST /v3/quiz -> QuizManager.insert(quiz) (cria/atualiza config)
66
+ 2. [Admin] POST /v3/question (xN) -> QuestionManager.insert (cada questão com quiz=<quizId>)
67
+ 3. [Player] POST /v3/quiz/start -> QuizManager.startLog(log) (cria quiz_log, verifica Limit)
68
+ 4. [Player] POST /v3/question/log (xN) -> QuestionManager.answer (avalia questão, salva question_log)
69
+ ou /v3/question/log/bulk -> se log.quiz_log != null -> QuizManager.updateLog(quiz_log)
70
+ 5. [Player] POST /v3/quiz/finish -> QuizManager.finishLog(id,async) (consolida nota, dispara action_log)
71
+ ```
72
+
73
+ Características:
74
+
75
+ - **Síncrono** em todas as etapas. A única assíncronia opcional é o registro da `action_log` em `finishLog`, controlado pelo query param `?async=` repassado a `ActionManager.trackWithRestrictions(al, async)`.
76
+ - **Sem transação MongoDB.** Cada `save`/`remove` é independente; uma falha no meio do pipeline deixa estado parcial.
77
+ - A nota só é **definitiva** após o `finish`. Durante a tentativa, `updateLog` só grava nota parcial se `quiz.showGradeBeforeFinish == true` (§2.3).
78
+
79
+ #### Diagrama — sequência de uma tentativa (`firequiz`)
80
+
81
+ ```mermaid
82
+ sequenceDiagram
83
+ participant C as Cliente (Player)
84
+ participant QR as QuizRest
85
+ participant QM as QuizManager
86
+ participant QnM as QuestionManager
87
+ participant DB as MongoDB
88
+
89
+ C->>QR: POST /v3/quiz/start {quiz, player}
90
+ QR->>QM: startLog(log)
91
+ QM->>QM: verifyLimit(quiz, log)
92
+ QM->>DB: save quiz_log (started=now)
93
+ QM-->>C: { log: quiz_log }
94
+
95
+ loop para cada questão
96
+ C->>QR: POST /v3/question/log {question, answer, quiz_log}
97
+ QR->>QnM: answer(log)
98
+ QnM->>QnM: avalia tipo -> percent, grade
99
+ QnM->>DB: save question_log (id = quiz_log + "_" + question)
100
+ QnM->>QM: updateLog(quiz_log)
101
+ QM->>DB: atualiza questions/answers (e nota se showGradeBeforeFinish)
102
+ end
103
+
104
+ C->>QR: POST /v3/quiz/finish {quiz_log}
105
+ QR->>QM: finishLog(id, async)
106
+ QM->>QM: percent = soma(answer.percent)/questions, grade = quiz.grade x percent
107
+ QM->>DB: save quiz_log (finished=now)
108
+ QM->>QnM: (action) ActionManager.trackWithRestrictions(action_log)
109
+ QM-->>C: { log, feedbacks, action }
110
+ ```
111
+
112
+ ### 2.3 Cálculo de nota — `finishLog` e `updateLog`
113
+
114
+ Pseudocódigo real de `finishLog` (`QuizManager.java:251`):
115
+
116
+ ```
117
+ questions = SELECT * FROM question WHERE quiz = quiz_log.quiz // todas as questões do quiz
118
+ answers = SELECT * FROM question_log WHERE quiz_log = quiz_log.id // respostas desta tentativa
119
+
120
+ percent = 0
121
+ questionsGrade = 0
122
+ para cada answer em answers:
123
+ se answer.grade != null: questionsGrade += answer.grade
124
+ se answer.percent != null: percent += answer.percent
125
+
126
+ log.percent = percent / questions.size() // média simples sobre TODAS as questões
127
+ log.grade = quiz.grade * log.percent // escala 0..quiz.grade (default 0..10)
128
+ log.questionsGrade = questionsGrade // soma ponderada (peso por questão)
129
+ log.questions = questions.size()
130
+ log.answers = answers.size()
131
+ se log.finished == null: log.finished = now()
132
+ feedback = quiz.getFeedback(log.grade) // maior boundary <= grade
133
+ ```
134
+
135
+ Pontos não óbvios:
136
+
137
+ - `log.percent` é **média sobre o total de questões do quiz**, não sobre as respondidas. Questões não respondidas entram no denominador e **penalizam** a nota.
138
+ - `questionsGrade` é a **soma ponderada** (`Σ question.grade × percent_da_questão`), enquanto `grade` trata todas as questões com peso igual. Os dois números podem divergir.
139
+ - `quiz.getFeedback(grade)` (`Quiz.java:98`) retorna o `Feedback` cujo `grade boundary` é o **maior valor ≤ grade obtida**. Sem boundary aplicável → `null`.
140
+ - `updateLog` (`QuizManager.java:201`) roda a cada resposta, mas: **(a)** só atualiza se `log.finished == null`; **(b)** só grava `percent`/`grade`/`questionsGrade` se `quiz.showGradeBeforeFinish == true`; **(c)** sempre atualiza `questions` e `answers`.
141
+
142
+ > ⚠️ **Divisão por zero:** se o quiz não tem questões, `questions.size() == 0` e `percent/0` produz `NaN` (Java, double). `log.grade` vira `NaN`, `getFeedback(NaN)` retorna `null` (toda comparação com NaN é falsa) e a `action_log` recebe `grade = NaN`. Não há erro nem log.
143
+
144
+ ### 2.4 Integração com `question` (callback de atualização)
145
+
146
+ Quando uma resposta é registrada (`QuestionManager.answer`, `QuestionManager.java:533`):
147
+
148
+ ```
149
+ se log.quiz_log != null e não vazio:
150
+ manager.getQuizManager().updateLog(log.quiz_log)
151
+ ```
152
+
153
+ É **a única forma** de manter `quiz_log.questions`/`answers` em dia durante a tentativa. Se o cliente responder questões sem informar `quiz_log`, o `quiz_log` não é atualizado e o `finish` ainda assim recalcula tudo (busca `question_log` por `quiz_log`). Ou seja: o vínculo crítico é `QuestionLog.quiz_log` apontar para `QuizLog._id`.
154
+
155
+ ### 2.5 Cascata de exclusão
156
+
157
+ #### Diagrama — `QuizManager.delete(id)`
158
+
159
+ ```mermaid
160
+ flowchart LR
161
+ A["delete(quizId)"] --> B["remove quiz where _id = quizId"]
162
+ A --> C["remove quiz_log where quiz = quizId"]
163
+ A --> D["remove question where quiz = quizId"]
164
+ A --> E["remove question_log where quiz = quizId"]
165
+ ```
166
+
167
+ - `delete(id)` (`QuizManager.java:75`) apaga o quiz **e** todas as suas tentativas, questões e respostas. Operação destrutiva e **sem trigger** (§6/§7).
168
+ - `deleteLog(id)` (`QuizManager.java:83`) apaga uma tentativa e os `question_log` ligados a ela (`quiz_log = id`).
169
+ - `PlayerDaoMongo` (`engine/player/PlayerDaoMongo.java:108`) remove `quiz_log` de um jogador quando o player é excluído (`remove {player:#}`).
170
+
171
+ ---
172
+
173
+ ## 3. Estrutura dos Objetos
174
+
175
+ ### 3.1 `Quiz` — documento raiz (coleção `quiz`)
176
+
177
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
178
+ |---|---|---|---|---|
179
+ | `_id` | String | auto (`Guid.newShortGuid()`) | — | ID; gerado se ausente/vazio no insert |
180
+ | `title` | String | `null` | não | Título exibido no admin |
181
+ | `description` | String | `null` | não | Descrição |
182
+ | `grade` | double | `10` | não | Nota máxima da prova (escala da nota final) |
183
+ | `i18n` | Map<String,Map<String,String>> | `{}` | não | Internacionalização |
184
+ | `extra` | Map<String,Object> | `{}` | não | Campos livres |
185
+ | `created` | Date | auto | — | Definido no 1º insert; preservado em updates |
186
+ | `updated` | Date | auto | — | Atualizado a cada insert |
187
+ | `limit` | `Limit` | `null` | não | Limite de tentativas (§3.5) |
188
+ | `action` | String | `null` | não | ID/nome da ação registrada ao finalizar |
189
+ | `techniques` | List<String> | `null` | não | Códigos de técnica de jogo (GT) — apenas armazenados |
190
+ | `feedbacks` | List<`Feedback`> | `[]` | não | Mensagens por faixa de nota (§3.3) |
191
+ | `settings` | `QuizSettings` | `null` | não | Settings de UI — **persistido mas não aplicado** (§3.4/§7) |
192
+ | `questionNumbering` | String | `null` | não | Numeração das respostas (enum em §3.1) |
193
+ | `showGradeBeforeFinish` | boolean | `false` | não | Se `true`, expõe nota parcial em `updateLog` |
194
+ | `shuffle` | boolean | `false` | não | Embaralhar questões — **flag apenas armazenada** (servidor não embaralha; §7) |
195
+
196
+ **Enum `questionNumbering`** (constantes estáticas em `Quiz.java`, aplicação no frontend):
197
+
198
+ | Valor | Significado |
199
+ |---|---|
200
+ | `arabic_numerals` | 1, 2, 3 |
201
+ | `roman_numerals` | I, II, III |
202
+ | `uppercase_letters` | A, B, C |
203
+
204
+ **Campos computados / não presentes no schema do `Quiz`:**
205
+
206
+ - **`questions` NÃO é campo do `Quiz`.** O comentário de cabeçalho de `Quiz.java` cita "questions", e a versão legada de `findAllQuestionsByQuiz` (comentada, `QuizManager.java:384`) lia `q.questions`. O modelo atual **não tem esse campo** — as questões são buscadas por `question.quiz = quizId`. A relação é inversa.
207
+
208
+ **Campos aceitos e silenciosamente ignorados:**
209
+
210
+ - `@JsonIgnoreProperties(ignoreUnknown=true)` em `Quiz` → qualquer campo extra no JSON é descartado sem erro (ex.: `gradePercent`, `gradeCheck` citados em docs antigas **não existem** no modelo).
211
+
212
+ #### Relação entre coleções
213
+
214
+ ```mermaid
215
+ erDiagram
216
+ QUIZ ||--o{ QUESTION : "question.quiz = quiz._id"
217
+ QUIZ ||--o{ QUIZ_LOG : "quiz_log.quiz = quiz._id"
218
+ QUIZ_LOG ||--o{ QUESTION_LOG : "question_log.quiz_log = quiz_log._id"
219
+ QUESTION ||--o{ QUESTION_LOG : "question_log.question = question._id"
220
+ ```
221
+
222
+ ### 3.2 `QuizLog` — documento da tentativa (coleção `quiz_log`)
223
+
224
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
225
+ |---|---|---|---|---|
226
+ | `_id` | String | auto (`Guid.newShortGuid()`) | — | ID da tentativa; gerado em `startLog` se ausente |
227
+ | `player` | String | — | **sim** | Jogador da tentativa |
228
+ | `quiz` | String | — | **sim** | ID do quiz |
229
+ | `questions` | int | `0` | auto | Total de questões do quiz (preenchido em update/finish) |
230
+ | `answers` | int | `0` | auto | Total de respostas registradas (preenchido em update/finish) |
231
+ | `percent` | Double | `null` | auto | Média de acerto ∈ [0,1] (parcial só se `showGradeBeforeFinish`) |
232
+ | `grade` | Double | `null` | auto | `quiz.grade × percent` |
233
+ | `questionsGrade` | Double | `null` | auto | Soma ponderada dos `grade` das respostas |
234
+ | `started` | Date | auto (`startLog`) | auto | Início; gerado se ausente |
235
+ | `finished` | Date | auto (`finishLog`) | auto | Fim; define a tentativa como encerrada |
236
+
237
+ **Estados de uma tentativa:**
238
+
239
+ ```mermaid
240
+ stateDiagram-v2
241
+ [*] --> Iniciada: startLog (started=now)
242
+ Iniciada --> Iniciada: answer chama updateLog (questions/answers, nota se showGradeBeforeFinish)
243
+ Iniciada --> Finalizada: finishLog (finished=now, nota consolidada, action_log)
244
+ Finalizada --> [*]
245
+ note right of Finalizada
246
+ updateLog não altera mais nada
247
+ (guarda: log.finished == null)
248
+ end note
249
+ ```
250
+
251
+ ### 3.3 `Feedback` (quiz-level) — `engine/quiz/Feedback.java`
252
+
253
+ | Campo | Tipo | Descrição |
254
+ |---|---|---|
255
+ | `grade` | double | *Grade boundary*: nota mínima para este feedback aparecer |
256
+ | `message` | String | Texto exibido (suporta Mustache; parâmetros: `player`, `log`, `feedbacks`, `errors`, `quiz`) |
257
+
258
+ > ⚠️ Existe **outra** classe `Feedback` em `engine/question/Feedback.java`, totalmente diferente: usa o campo `event` (`correct`/`partial_correct`/`wrong`) em vez de `grade`. A do quiz é por **faixa de nota**; a da question é por **evento**. Não confundir.
259
+
260
+ ### 3.4 `QuizSettings` / `FeedbackSettings` / `StuckSettings` (UI — não aplicado pelo servidor)
261
+
262
+ `Quiz.settings` é persistido como veio no JSON, mas **nenhum caminho de runtime lê esses campos** (vide §7). Documentado aqui por completude do schema:
263
+
264
+ `QuizSettings`: `feedback: FeedbackSettings`, `stuck: StuckSettings`.
265
+
266
+ `FeedbackSettings`:
267
+
268
+ | Campo | Tipo | Valores / default sugerido |
269
+ |---|---|---|
270
+ | `mode` | String | `immediate` \| `deferred` \| `none` (default sugerido `deferred`) |
271
+ | `position` | String | `inline` \| `toast` \| `modal` |
272
+ | `autoHideMs` | Integer | `0`/`null` = não auto-esconde |
273
+ | `allowRetry` | Boolean | default sugerido `false` |
274
+ | `lockOnCorrect` | Boolean | default sugerido `true` |
275
+ | `showPercent` | Boolean | default sugerido `false` |
276
+ | `showAfterSubmitReview` | Boolean | relevante para `deferred` |
277
+ | `blockNavigationUntilFeedbackHidden` | Boolean | — |
278
+
279
+ `StuckSettings`: `enabled` (Boolean), `attemptsThreshold` (Integer), `timeThresholdSeconds` (Integer), `message` (String), `offerHint` (Boolean).
280
+
281
+ > Os defaults acima existem apenas em `FeedbackDefaults.defaultQuizFeedback()` / `defaultQuizStuck()`, métodos **sem chamadores** no código.
282
+
283
+ ### 3.5 `Limit` — controle de tentativas (`engine/action/Limit.java`)
284
+
285
+ | Campo | Tipo | Descrição |
286
+ |---|---|---|
287
+ | `total` | Object | Número (ex.: `3`) **ou** fórmula Mustache+exp4j (ex.: `{{player.extra.max}}`) |
288
+ | `per` | String | `player` (PER_PLAYER), `team`, `gamification` (PER_GAME) |
289
+ | `every` | String | Janela de tempo (ex.: `1d`, `5h`); ausência ⇒ janela de `10y` |
290
+ | `query` | String | Filtro extra concatenado à query de contagem (Mustache; mínimo 3 chars) |
291
+
292
+ Avaliado em `verifyLimit` (`QuizManager.java:91`) durante `startLog`. **Atenção:** apenas `per = player` e `per = gamification` são tratados na contagem; **`team` não tem ramo** e não limita nada.
293
+
294
+ ---
295
+
296
+ ## 4. Endpoints
297
+
298
+ Todos exigem `Authorization: Bearer <token>` (`@BeanParam AuthBean`). O isolamento por organização é feito por `FrontController.getInstance(authBean.getApiKey())` (cada API key → conexão/banco próprios).
299
+
300
+ ### `POST /v3/quiz` — criar/atualizar quiz
301
+
302
+ | Aspecto | Detalhe |
303
+ |---|---|
304
+ | Finalidade | Cria ou atualiza a configuração do quiz (upsert por `_id`) |
305
+ | Autenticação | Bearer token |
306
+ | Full replace ou patch | **Full replace** — `c.save(quiz)` substitui o documento inteiro |
307
+ | Método de update | Não há PUT; reenviar POST com o mesmo `_id` sobrescreve |
308
+
309
+ **Comportamento real:** se `_id` ausente/vazio → gera ID. `created` é preservado de um documento existente; `updated` sempre renovado. Triggers `before_create`/`after_create` **só disparam quando é criação** (vide §6). Resposta: `JsonUtil.toJsonRemoveNullFields(quiz)` com status `201 CREATED` (inclusive em updates).
310
+
311
+ > ⚠️ **Bug de doc na fonte:** o `@apiExample` de `QuizRest.insert` mostra um payload de **Question** (`type`, `choices`, `select`...), não de Quiz. Ignore-o. Exemplo real de Quiz em §10.
55
312
 
56
- ### Deletar Quiz
57
- **Método:** DELETE
58
- **Endpoint:** `/v3/quiz/:id`
313
+ ### `GET /v3/quiz/{id}` — buscar quiz
59
314
 
60
- ### Listar Perguntas de um Quiz
61
- **Método:** GET
62
- **Endpoint:** `/v3/quiz/:id/question`
315
+ Retorna a configuração do quiz (`200 OK`). Retorna corpo `null` se não existir (não 404).
63
316
 
64
- ### Criar Pergunta para um Quiz
65
- **Método:** POST
66
- **Endpoint:** `/v3/question`
317
+ ### `POST /v3/quiz/aggregate` listar/agregar quizzes
318
+
319
+ | Param (query) | Tipo | Descrição |
320
+ |---|---|---|
321
+ | `player` | String | Se informado, **injeta** campos `started`/`finished` por quiz |
322
+ | `started_min` / `started_max` | String | Janela para marcar `started` (keyword de data ou ISO) |
323
+ | `finished_min` / `finished_max` | String | Janela para marcar `finished` |
324
+ | Header `Range` | String | Paginação (ex.: `items=0-100`) |
325
+ | Body | List<Object> | **Pipeline de aggregation do MongoDB** enviado pelo cliente |
326
+
327
+ **Comportamento real (enriquecimento server-side, `findAllAggregate`, `QuizManager.java:478`):** quando `player` é informado, antes do pipeline do cliente são injetados estágios `$facet`/`$project`/`$unwind`/`$replaceRoot` que adicionam, em cada quiz, os booleanos `started` e `finished` (se aquele player iniciou/finalizou aquele quiz na janela). Os IDs vêm de `getStartedQuizIds`/`getFinishedQuizIds` (distinct em `quiz_log`). Esses campos **não existem** no documento `quiz` — são computados na consulta.
328
+
329
+ ### `GET /v3/quiz/{id}/question` — listar questões do quiz
330
+
331
+ Retorna as questões onde `question.quiz = id` (`findAllQuestionsByQuiz` → `find({quiz:#})`). Não pagina (limite implícito do cursor / header `Range` global).
332
+
333
+ ### `DELETE /v3/quiz/{id}` — excluir quiz (cascata)
334
+
335
+ Remove `quiz`, `quiz_log`, `question` e `question_log` do quiz (§2.5). `204 NO_CONTENT`. **Sem trigger.**
336
+
337
+ ### `POST /v3/quiz/start` — iniciar tentativa
338
+
339
+ | Aspecto | Detalhe |
340
+ |---|---|
341
+ | Finalidade | Cria um `quiz_log` (tentativa) |
342
+ | Body | `{ "quiz": "<id>", "player": "<player|me>" }` |
343
+ | Resolução de player | `player == null` ou `"me"` → resolvido do token (`getPlayerFromTokenIfExist`) |
344
+
345
+ **Comportamento real:** valida `quiz`/`player` obrigatórios e existência do quiz; aplica `verifyLimit`. Se passar, gera `_id`/`started` e dispara `before_create`/`after_create` em `quiz_log`. Retorna `{ log, errors? }` com `201`. Se o limite estourar, retorna `errors` e **não** cria o log.
346
+
347
+ ### `POST /v3/quiz/finish` — finalizar tentativa
348
+
349
+ | Aspecto | Detalhe |
350
+ |---|---|
351
+ | Finalidade | Consolida nota, dispara feedback e `action_log` |
352
+ | Body | `{ "quiz_log": "<id>" }` (campo lido é `quiz_log`) |
353
+ | Query | `?async=` repassado ao `ActionManager.trackWithRestrictions` |
354
+
355
+ **Comportamento real:** recalcula tudo (§2.3) **independente** de `showGradeBeforeFinish`; grava `finished`; dispara `before_update`/`after_update` em `quiz_log`; aplica Mustache nas mensagens de feedback; se `quiz.action` existir e a ação for válida, registra `action_log` com atributos `quiz`, `percent`, `answers`, `questions`, `grade`, `questionsGrade`. Se `quiz.action` apontar para ação inexistente → adiciona erro `quiz.action ... does not exist` (mas o log já foi salvo). Body ausente de `quiz_log` → erro `field id is required`. Status `201`.
356
+
357
+ ### `DELETE /v3/quiz/log/{id}` — excluir tentativa
358
+
359
+ Remove o `quiz_log` e seus `question_log` (`quiz_log = id`). **Sem trigger.**
360
+
361
+ > ⚠️ **NPE:** `deleteLog` faz `findOne(...)` e na linha seguinte usa `log.id`. Se o `id` não existir, `log` é `null` → `NullPointerException` (`QuizManager.java:85`).
362
+
363
+ ### Endpoints relacionados (módulo `question`, usados no fluxo)
364
+
365
+ - `POST /v3/question/log` — registra **uma** resposta (`QuestionManager.answer`). Inclua `quiz_log` para vincular à tentativa.
366
+ - `POST /v3/question/log/bulk` — registra **várias** respostas de uma vez.
367
+
368
+ ---
369
+
370
+ ## 5. Regras de Negócio
371
+
372
+ Regras presentes no código e ausentes do schema:
373
+
374
+ - **ID composto de resposta:** em `QuestionManager.answer`, se `log.id == null` **e** `log.quiz_log` informado → `log.id = quiz_log + "_" + question` (`QuestionManager.java:478`). Isso garante **uma resposta por questão por tentativa** — re-responder **sobrescreve**. Se o cliente enviar um `id` explícito, essa proteção é **burlada** e podem surgir respostas duplicadas (contadas em `answers`).
375
+ - **Nota penaliza não respondidas:** `percent = Σpercent / total_de_questões` (não / respondidas). Pular questões reduz a nota.
376
+ - **Nota parcial é opt-in:** `updateLog` só grava `percent`/`grade` se `quiz.showGradeBeforeFinish == true`; caso contrário, durante a tentativa só evoluem `questions`/`answers`.
377
+ - **Limite por janela:** `verifyLimit` conta tentativas em `quiz_log` no período `every` (default `10y`). `per=player` filtra por player+quiz; `per=gamification` filtra só por quiz; **`per=team` não é tratado** (não limita).
378
+ - **Limite com fórmula:** `total` pode ser expressão Mustache avaliada por exp4j (ex.: `{{player.extra.max_attempts}}`). Falha na avaliação → erro e a tentativa **não** é registrada.
379
+ - **Feedback por faixa:** `quiz.getFeedback(grade)` escolhe o maior `boundary ≤ grade`. As faixas são comparadas na escala de `quiz.grade` (default 0..10).
380
+ - **`finish` é idempotente em parte:** se `finished` já existe, não é sobrescrito; mas a nota **é recalculada** a cada chamada de `finish` (não há guarda de "já finalizado").
381
+ - **Multi-tenant:** todo acesso é resolvido por API key → banco isolado. Não há campo de organização no documento.
382
+ - **Consistência eventual:** sem transações; `updateLog`/`finishLog` recalculam a partir das coleções, então valores convergem mesmo que um passo intermediário falhe — desde que `question_log.quiz_log` esteja correto.
383
+
384
+ ---
385
+
386
+ ## 6. Comportamentos Automáticos
387
+
388
+ | Comportamento | Trigger (gatilho) | Impacto | Persistência |
389
+ |---|---|---|---|
390
+ | Geração de `_id` (quiz) | `insert` sem `_id` | `Guid.newShortGuid()` | Sim (`quiz`) |
391
+ | `created`/`updated` | `insert` | `created` no 1º insert; `updated` sempre | Sim (`quiz`) |
392
+ | Trigger `before_create`/`after_create` (quiz) | `insert` **apenas em criação** | Executa triggers da coleção `quiz` | Conforme trigger |
393
+ | Geração de `_id`/`started` (quiz_log) | `startLog` | IDs e início | Sim (`quiz_log`) |
394
+ | Trigger `before_create`/`after_create` (quiz_log) | `startLog` | Triggers da coleção `quiz_log` | Conforme trigger |
395
+ | `updateLog` (counters) | cada `question.answer` com `quiz_log` | Atualiza `questions`/`answers` (e nota se `showGradeBeforeFinish`) | Sim (`quiz_log`) |
396
+ | Consolidação de nota | `finishLog` | `percent`/`grade`/`questionsGrade`/`finished` | Sim (`quiz_log`) |
397
+ | Trigger `before_update`/`after_update` (quiz_log) | `finishLog` | Triggers da coleção `quiz_log` | Conforme trigger |
398
+ | Registro de `action_log` | `finishLog` com `quiz.action` | Alimenta gamificação (pontos/desafios) | Sim (`action_log`) |
399
+ | Mustache em feedback | `finishLog` | Interpola mensagens com `player`/`log`/`quiz` | Não (só na resposta) |
400
+ | Cascata de exclusão | `delete`/`deleteLog`/exclusão de player | Remove logs/questões relacionados | Sim (remoções) |
401
+
402
+ #### Diagrama — gatilhos do `finishLog`
403
+
404
+ ```mermaid
405
+ flowchart LR
406
+ F["finishLog(id)"] --> R["recalcula percent/grade/questionsGrade"]
407
+ R --> FB["getFeedback(grade)"]
408
+ R --> T1["trigger before_update (quiz_log)"]
409
+ T1 --> S["save quiz_log (finished)"]
410
+ S --> T2["trigger after_update (quiz_log)"]
411
+ R --> AC{"quiz.action definido?"}
412
+ AC -- sim --> AL["ActionManager.trackWithRestrictions(action_log)"]
413
+ AC -- não --> X["sem action_log"]
414
+ ```
415
+
416
+ > ⚠️ **Assimetria importante:** `insert` dispara trigger **apenas quando cria** o quiz. Atualizar um quiz existente **não** dispara `before_update`/`after_update`. `delete`/`deleteLog` **não** disparam nenhum trigger.
417
+
418
+ ---
419
+
420
+ ## 7. Suportado vs NÃO Suportado
421
+
422
+ ### ✅ Suportado
423
+
424
+ - CRUD de configuração de quiz (`POST`/`GET`/`DELETE /v3/quiz`).
425
+ - Ciclo de tentativa: `start` → respostas (via módulo `question`) → `finish`.
426
+ - Cálculo de nota agregada (`percent`, `grade`, `questionsGrade`) ponderada por questão.
427
+ - Feedback textual por faixa de nota, com Mustache.
428
+ - Limite de tentativas por `player` ou `gamification`, com janela temporal e fórmula.
429
+ - Disparo de `action_log` ao finalizar (integração com gamificação).
430
+ - Triggers `before_create`/`after_create` (quiz e quiz_log) e `before_update`/`after_update` (quiz_log no finish).
431
+ - Exclusão em cascata; listagem/agregação com enriquecimento `started`/`finished` por player.
432
+ - Nota parcial durante a tentativa via `showGradeBeforeFinish`.
433
+
434
+ ### ❌ NÃO Suportado
435
+
436
+ - **`Quiz.settings` (QuizSettings/FeedbackSettings/StuckSettings) não tem efeito no servidor.** É persistido, mas `resolvePresentation`/`ensureQuizDefaults`/`defaultQuiz*` (em `FeedbackDefaults`) **não têm chamadores** — código morto. `mode`, `position`, `lockOnCorrect`, `stuck.*` etc. são apenas dicas de UI.
437
+ - **`shuffle` (quiz e question) não embaralha no servidor.** É apenas uma flag armazenada; eventual embaralhamento é responsabilidade do frontend.
438
+ - **`questionNumbering`/`answerNumbering`** — apenas armazenados; aplicação é do frontend.
439
+ - **`techniques`** — apenas armazenado; nenhuma lógica server-side no módulo quiz.
440
+ - **Sem campo `questions` no Quiz** — relação é inversa (`question.quiz`); não há endpoint para "adicionar questão ao quiz" (cria-se a questão com `quiz=<id>`).
441
+ - **Sem trigger em update de quiz** e **sem trigger em delete/deleteLog**.
442
+ - **`Limit.per = team` não limita** (sem ramo no `verifyLimit`).
443
+ - **Sem PUT/PATCH** — atualização só por full replace via POST.
444
+ - **Sem endpoint para ler `quiz_log` diretamente** (só `DELETE /v3/quiz/log/{id}`); leitura de resultados via endpoints genéricos de banco/aggregate sobre `quiz_log`.
445
+ - **`/v3/ai/build/quiz` não persiste** — apenas chama OpenAI e retorna o JSON gerado (com questões aninhadas no formato do prompt) para o cliente persistir depois.
446
+ - **Campos desconhecidos descartados** — `@JsonIgnoreProperties(ignoreUnknown=true)`; ex.: `gradePercent`/`gradeCheck` (citados em docs antigas) não existem em `Choice` e somem.
447
+ - **Sem scheduler/job** próprio do módulo.
448
+
449
+ ---
450
+
451
+ ## 8. Segurança e Permissões
452
+
453
+ - **Autenticação:** Bearer token em todos os endpoints (`AuthBean`). Não há checagem de papel/role explícita na camada `QuizRest` — qualquer token válido da organização opera o módulo.
454
+ - **Isolamento multi-tenant:** `FrontController.getInstance(apiKey)` resolve uma conexão Mongo por organização; não há campo de tenant no documento — o isolamento é a nível de conexão/banco.
455
+ - **Resolução de identidade:** `start` aceita `player: "me"` e resolve do token; se `player` for fornecido explicitamente, **não há validação** de que o token corresponde a esse player (um token pode iniciar tentativa em nome de outro player, conforme permissões da API key).
456
+ - **Superfícies de injeção:**
457
+ - `POST /v3/quiz/aggregate` recebe um **pipeline de aggregation arbitrário** (`List<Object>`) do cliente e o executa direto no Mongo. É o comportamento padrão dos endpoints `aggregate` da Funifier, mas é uma superfície poderosa — proteja por permissão de API key.
458
+ - `verifyLimit` concatena `Limit.query` (após Mustache) **diretamente** na string de query do Mongo (`QuizManager.java:120/127`). Como `Limit` é configurado por admin, o risco é de config maliciosa, não de input de player.
459
+ - `findAllQuestionLogsByLog` monta `"quiz_log:\"" + log + "\""` por concatenação de string (`QuizManager.java:393`). O `log` é o `quiz_log.id` (controlado pelo servidor no fluxo normal), mas a montagem por concatenação é frágil.
460
+ - **Comportamento inseguro documentado:** `deleteLog` lança NPE para id inexistente (não trata `null`) — não vaza dado, mas gera erro 500.
461
+
462
+ ---
463
+
464
+ ## 9. Observabilidade e Troubleshooting
465
+
466
+ ### Diagnóstico rápido
467
+
468
+ ```
469
+ # Configuração do quiz
470
+ GET /v3/quiz/{quizId}
471
+
472
+ # Questões vinculadas
473
+ GET /v3/quiz/{quizId}/question
474
+
475
+ # Tentativas de um quiz (via aggregate genérico sobre quiz_log)
476
+ POST /v3/database/quiz_log/aggregate
477
+ [ { "$match": { "quiz": "{quizId}" } }, { "$sort": { "started": -1 } } ]
478
+
479
+ # Respostas de uma tentativa
480
+ POST /v3/database/question_log/aggregate
481
+ [ { "$match": { "quiz_log": "{quizLogId}" } } ]
482
+
483
+ # Status started/finished por player (enriquecimento embutido)
484
+ POST /v3/quiz/aggregate?player={player}
485
+ [ { "$match": {} } ]
486
+ ```
487
+
488
+ ### Erros comuns e causas
489
+
490
+ | Sintoma | Causa provável |
491
+ |---|---|
492
+ | `errors: ["quiz dont exist"]` no start | `quiz` inexistente / id errado |
493
+ | `errors: [... exced the limit ...]` | `Limit` atingido na janela `every` |
494
+ | `errors: ["quiz.action ... does not exist"]` no finish | `quiz.action` aponta para ação inválida (log já salvo) |
495
+ | Nota menor que o esperado | Questões não respondidas entram no denominador (§2.3) |
496
+ | `grade`/`percent` ausentes durante a tentativa | `showGradeBeforeFinish=false` (esperado; só aparecem no finish) |
497
+ | `grade = NaN` | Quiz sem questões (divisão por zero, §2.3) |
498
+ | Respostas duplicadas em `answers` | Cliente enviou `question_log` com `id` explícito, burlando o id composto |
499
+ | HTTP 500 ao deletar log | `quiz_log` inexistente → NPE em `deleteLog` |
500
+ | `quiz_log.questions/answers` desatualizados | Respostas enviadas sem `quiz_log` (não chamam `updateLog`) |
501
+
502
+ ### O que verificar quando "não funciona"
503
+
504
+ 1. `question.quiz` aponta para o `quiz._id` correto? (sem isso, o quiz não tem questões).
505
+ 2. `question_log.quiz_log` aponta para o `quiz_log._id`? (sem isso, nota não consolida).
506
+ 3. `quiz.action` existe? (necessário para gerar pontos/achievement).
507
+ 4. O `Limit` não está bloqueando silenciosamente novas tentativas?
508
+
509
+ ---
510
+
511
+ ## 10. Exemplos Práticos
512
+
513
+ ### 10.1 Exemplo mínimo funcional
514
+
515
+ ```json
516
+ // POST /v3/quiz
517
+ {
518
+ "_id": "prova_historia",
519
+ "title": "Prova de História",
520
+ "grade": 10
521
+ }
522
+ ```
67
523
 
68
- **Exemplo de Body:**
69
524
  ```json
525
+ // POST /v3/question (repetir para cada questão, sempre com "quiz")
70
526
  {
71
- "quiz": "650c82fe832",
527
+ "quiz": "prova_historia",
72
528
  "type": "MULTIPLE_CHOICE",
73
- "title": "Best Platform",
74
- "question": "What is the best gamification platform?",
529
+ "question": "Quem descobriu o Brasil?",
75
530
  "grade": 1,
76
- "choices": [
77
- { "answer": "1", "label": "Funifier", "grade": 1, "extra": {}, "gradePercent": 100, "gradeCheck": true },
78
- { "answer": "2", "label": "Points", "grade": 0, "extra": {}, "gradePercent": 0 },
79
- { "answer": "3", "label": "Badges", "grade": 0, "extra": {}, "gradePercent": 0 },
80
- { "answer": "4", "label": "Leaderboards", "grade": 0, "extra": {}, "gradePercent": 0 }
81
- ],
82
- "i18n": {},
83
531
  "select": "one_answer",
84
- "shuffle": false
532
+ "choices": [
533
+ { "answer": "1", "label": "Pedro Álvares Cabral", "grade": 1.0 },
534
+ { "answer": "2", "label": "Dom Pedro I", "grade": 0.0 }
535
+ ]
85
536
  }
86
537
  ```
87
538
 
88
- ### Iniciar Quiz
89
- **Método:** POST
90
- **Endpoint:** `/v3/quiz/start`
539
+ ```json
540
+ // POST /v3/quiz/start -> retorna { "log": { "_id": "<quizLogId>", "started": ... } }
541
+ { "quiz": "prova_historia", "player": "me" }
542
+ ```
543
+
544
+ ```json
545
+ // POST /v3/question/log (vincular à tentativa via quiz_log)
546
+ { "quiz_log": "<quizLogId>", "quiz": "prova_historia",
547
+ "question": "<questionId>", "answer": ["1"], "player": "tom" }
548
+ ```
549
+
550
+ ```json
551
+ // POST /v3/quiz/finish
552
+ { "quiz_log": "<quizLogId>" }
553
+ ```
554
+
555
+ ### 10.2 Exemplo avançado (feedback por faixa, limite e ação)
91
556
 
92
- **Exemplo de Body:**
93
557
  ```json
558
+ // POST /v3/quiz
94
559
  {
95
- "quiz": "650c82fe832",
96
- "player": "tom"
560
+ "_id": "simulado_enem",
561
+ "title": "Simulado ENEM",
562
+ "description": "Simulado com nota e limite de tentativas",
563
+ "grade": 10,
564
+ "showGradeBeforeFinish": false,
565
+ "action": "finalizou_simulado",
566
+ "limit": { "total": 3, "per": "player", "every": "1d" },
567
+ "feedbacks": [
568
+ { "grade": 0, "message": "Que pena {{player.name}}, continue estudando." },
569
+ { "grade": 6, "message": "Bom trabalho! Você tirou {{log.grade}}." },
570
+ { "grade": 9, "message": "Excelente, {{player.name}}!" }
571
+ ],
572
+ "questionNumbering": "uppercase_letters"
97
573
  }
98
574
  ```
99
575
 
100
- ### Registrar Respostas em Lote
101
- **Método:** POST
102
- **Endpoint:** `/v3/question/log/bulk`
576
+ Notas: `feedbacks` é avaliado por **maior boundary ≤ nota**; `limit` permite 3 tentativas por player a cada 24h; `action` registra `action_log` com `grade`/`percent`/`questions`/`answers` ao finalizar.
577
+
578
+ ### 10.3 Anti-pattern (o que NÃO fazer)
103
579
 
104
- **Exemplo de Body:**
105
- ```json
106
- [
107
- {
108
- "quiz": "650c82fe832",
109
- "quiz_log": "650dc6168325771ffaa94098",
110
- "question": "650dc4d98325771ffaa93e5e",
111
- "answer": ["1"],
112
- "player": "tom"
113
- }
114
- ]
115
- ```
116
-
117
- ### Finalizar Quiz
118
- **Método:** POST
119
- **Endpoint:** `/v3/quiz/finish`
120
-
121
- **Exemplo de Body:**
122
580
  ```json
581
+ // ❌ Achar que "settings"/"shuffle" mudam o comportamento do servidor
123
582
  {
124
- "quiz_log": "650dc6168325771ffaa94098"
583
+ "_id": "quiz_errado",
584
+ "title": "Quiz",
585
+ "grade": 10,
586
+ "shuffle": true, // servidor NÃO embaralha
587
+ "settings": { // persistido, porém IGNORADO no runtime
588
+ "feedback": { "mode": "immediate", "lockOnCorrect": true }
589
+ },
590
+ "questions": [ { "type": "TRUE_FALSE", "question": "..." } ] // ❌ "questions" não é campo do Quiz: descartado
125
591
  }
126
592
  ```
127
593
 
128
- ## Fluxo Completo do Quiz
594
+ Por quê: `shuffle`/`settings` são apenas dicas de UI (§7); `questions` não existe no modelo `Quiz` e é silenciosamente descartado — as questões devem ser criadas em `/v3/question` com o campo `quiz`. Além disso, registrar respostas com `id` explícito quebra a regra de "uma resposta por questão por tentativa" (§5).
129
595
 
130
- ```
131
- 1. Admin cria Quiz (POST /v3/quiz)
132
- 2. Admin cria Questions vinculadas ao Quiz (POST /v3/question com "quiz": "quiz_id")
133
- 3. Jogador inicia Quiz (POST /v3/quiz/start → retorna quiz_log_id)
134
- 4. Jogador responde questões (POST /v3/question/log/bulk com quiz_log_id)
135
- 5. Jogador finaliza Quiz (POST /v3/quiz/finish → calcula nota)
136
- ```
596
+ ---
137
597
 
138
- ## Campos Importantes
598
+ ## Checklist de Configuração
139
599
 
140
- | Campo | Tipo | Descrição |
141
- |-------|------|-----------|
142
- | `grade` | Number | Nota total do quiz (ex: 10) |
143
- | `shuffle` | Boolean | Embaralhar ordem das perguntas |
144
- | `showGradeBeforeFinish` | Boolean | Mostrar nota parcial antes de finalizar |
145
- | `questionNumbering` | String | Formato numeração (`uppercase_letters`, `numbers`, etc.) |
146
- | `feedbacks` | Array | Mensagens de feedback por faixa de nota |
147
-
148
- ## Apresentação como Mini Game
149
-
150
- Quizzes podem ser apresentados em formatos de mini games:
151
- - Corrida espacial
152
- - Quiz tradicional com timer
153
- - Formato personalizado via widget/frontend
154
-
155
- ## Validações e Testes
156
-
157
- - [ ] Quiz aparece na lista GET /v3/database/quiz
158
- - [ ] Perguntas estão associadas ao quiz
159
- - [ ] Jogador consegue iniciar quiz (POST /v3/quiz/start)
160
- - [ ] Respostas são registradas corretamente (POST /v3/question/log/bulk)
161
- - [ ] Ao finalizar, nota é calculada (POST /v3/quiz/finish)
162
- - [ ] Embaralhamento funciona (se ativado)
163
- - [ ] Feedbacks aparecem conforme faixa de nota
600
+ - [ ] `quiz.grade` (nota máxima) definido — é a escala de `feedbacks` e da nota final.
601
+ - [ ] Questões criadas em `/v3/question` com `question.quiz = <quizId>` (a relação é inversa; não há lista `questions` no quiz).
602
+ - [ ] `quiz.action` aponta para uma ação **existente** se você quer gerar pontos/achievement no finish.
603
+ - [ ] `feedbacks` com `grade boundaries` cobrindo as faixas desejadas (lembre: maior boundary ≤ nota).
604
+ - [ ] Decidiu sobre `showGradeBeforeFinish` (nota parcial durante a tentativa? padrão `false`).
605
+ - [ ] `Limit` configurado com `per = player` ou `gamification` (⚠️ `team` **não** limita).
606
+ - [ ] Armadilha: `settings`/`shuffle`/`questionNumbering`/`techniques` são **dicas de UI** não esperar comportamento server-side.
607
+ - [ ] Armadilha: ao registrar respostas, **sempre** enviar `quiz_log` e **não** enviar `id` explícito (mantém o id composto e os counters corretos).
608
+ - [ ] Armadilha: quiz **sem questões** gera `grade = NaN` no finish.