funifier-mcp 0.2.25 → 0.2.27

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 (211) 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 +5 -2
  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 +1011 -77
  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/core/api-client.d.ts +21 -1
  66. package/dist/core/api-client.d.ts.map +1 -1
  67. package/dist/core/api-client.js +154 -1
  68. package/dist/core/api-client.js.map +1 -1
  69. package/dist/core/constants.d.ts +14 -0
  70. package/dist/core/constants.d.ts.map +1 -1
  71. package/dist/core/constants.js +14 -0
  72. package/dist/core/constants.js.map +1 -1
  73. package/dist/core/types/Folder.d.ts +16 -0
  74. package/dist/core/types/Folder.d.ts.map +1 -0
  75. package/dist/core/types/Folder.js +3 -0
  76. package/dist/core/types/Folder.js.map +1 -0
  77. package/dist/core/types/FolderContent.d.ts +10 -0
  78. package/dist/core/types/FolderContent.d.ts.map +1 -0
  79. package/dist/core/types/FolderContent.js +3 -0
  80. package/dist/core/types/FolderContent.js.map +1 -0
  81. package/dist/core/types/FolderContentType.d.ts +10 -0
  82. package/dist/core/types/FolderContentType.d.ts.map +1 -0
  83. package/dist/core/types/FolderContentType.js +3 -0
  84. package/dist/core/types/FolderContentType.js.map +1 -0
  85. package/dist/core/types/FolderLog.d.ts +11 -0
  86. package/dist/core/types/FolderLog.d.ts.map +1 -0
  87. package/dist/core/types/FolderLog.js +3 -0
  88. package/dist/core/types/FolderLog.js.map +1 -0
  89. package/dist/core/types/index.d.ts +4 -0
  90. package/dist/core/types/index.d.ts.map +1 -1
  91. package/dist/core/types/index.js +4 -0
  92. package/dist/core/types/index.js.map +1 -1
  93. package/dist/mcp/bundle.js +121 -87
  94. package/dist/mcp/check-update.d.ts +2 -0
  95. package/dist/mcp/check-update.d.ts.map +1 -0
  96. package/dist/mcp/check-update.js +44 -0
  97. package/dist/mcp/check-update.js.map +1 -0
  98. package/dist/mcp/index.js +5 -2
  99. package/dist/mcp/index.js.map +1 -1
  100. package/dist/mcp/resources/documentation.d.ts +1 -1
  101. package/dist/mcp/resources/documentation.d.ts.map +1 -1
  102. package/dist/mcp/resources/documentation.js +39 -3
  103. package/dist/mcp/resources/documentation.js.map +1 -1
  104. package/dist/mcp/tools/_char-guard.js +1 -1
  105. package/dist/mcp/tools/_char-guard.js.map +1 -1
  106. package/dist/mcp/tools/_fetch-current.d.ts +1 -1
  107. package/dist/mcp/tools/_fetch-current.d.ts.map +1 -1
  108. package/dist/mcp/tools/_fetch-current.js +12 -0
  109. package/dist/mcp/tools/_fetch-current.js.map +1 -1
  110. package/dist/mcp/tools/connect.d.ts.map +1 -1
  111. package/dist/mcp/tools/connect.js +18 -8
  112. package/dist/mcp/tools/connect.js.map +1 -1
  113. package/dist/mcp/tools/database.d.ts.map +1 -1
  114. package/dist/mcp/tools/database.js +59 -47
  115. package/dist/mcp/tools/database.js.map +1 -1
  116. package/dist/mcp/tools/database.test.js +2 -2
  117. package/dist/mcp/tools/database.test.js.map +1 -1
  118. package/dist/mcp/tools/delete.d.ts.map +1 -1
  119. package/dist/mcp/tools/delete.js +33 -3
  120. package/dist/mcp/tools/delete.js.map +1 -1
  121. package/dist/mcp/tools/execute.d.ts.map +1 -1
  122. package/dist/mcp/tools/execute.js +20 -9
  123. package/dist/mcp/tools/execute.js.map +1 -1
  124. package/dist/mcp/tools/folder.d.ts +4 -0
  125. package/dist/mcp/tools/folder.d.ts.map +1 -0
  126. package/dist/mcp/tools/folder.js +68 -0
  127. package/dist/mcp/tools/folder.js.map +1 -0
  128. package/dist/mcp/tools/get.d.ts.map +1 -1
  129. package/dist/mcp/tools/get.js +16 -6
  130. package/dist/mcp/tools/get.js.map +1 -1
  131. package/dist/mcp/tools/index.d.ts +1 -1
  132. package/dist/mcp/tools/index.d.ts.map +1 -1
  133. package/dist/mcp/tools/index.js +5 -1
  134. package/dist/mcp/tools/index.js.map +1 -1
  135. package/dist/mcp/tools/list.d.ts.map +1 -1
  136. package/dist/mcp/tools/list.js +38 -14
  137. package/dist/mcp/tools/list.js.map +1 -1
  138. package/dist/mcp/tools/logs.d.ts.map +1 -1
  139. package/dist/mcp/tools/logs.js +15 -5
  140. package/dist/mcp/tools/logs.js.map +1 -1
  141. package/dist/mcp/tools/save.d.ts.map +1 -1
  142. package/dist/mcp/tools/save.js +26 -4
  143. package/dist/mcp/tools/save.js.map +1 -1
  144. package/dist/mcp/tools/save.test.js +192 -1
  145. package/dist/mcp/tools/save.test.js.map +1 -1
  146. package/dist/mcp/tools/search-docs.d.ts +3 -0
  147. package/dist/mcp/tools/search-docs.d.ts.map +1 -0
  148. package/dist/mcp/tools/search-docs.js +102 -0
  149. package/dist/mcp/tools/search-docs.js.map +1 -0
  150. package/package.json +6 -2
  151. package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
  152. package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
  153. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
  154. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
  155. package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
  156. package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
  157. package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
  158. package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
  159. package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
  160. package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
  161. package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
  162. package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
  163. package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
  164. package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
  165. package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
  166. package/skills/funifier/SKILL.md +88 -0
  167. package/skills/funifier/references/configure-security.md +96 -0
  168. package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
  169. package/skills/funifier/references/create-aggregate.md +144 -0
  170. package/skills/funifier/references/create-challenge.md +116 -0
  171. package/skills/funifier/references/create-competition.md +98 -0
  172. package/skills/funifier/references/create-crossword.md +574 -0
  173. package/skills/funifier/references/create-custom-object.md +91 -0
  174. package/skills/funifier/references/create-custom-page.md +135 -0
  175. package/skills/funifier/references/create-folder.md +104 -0
  176. package/skills/funifier/references/create-lastmile.md +643 -0
  177. package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
  178. package/skills/funifier/references/create-level.md +94 -0
  179. package/skills/funifier/references/create-lottery.md +913 -0
  180. package/skills/funifier/references/create-mystery.md +769 -0
  181. package/skills/funifier/references/create-notification.md +75 -0
  182. package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
  183. package/skills/funifier/references/create-quiz.md +98 -0
  184. package/skills/funifier/references/create-scheduler.md +141 -0
  185. package/skills/funifier/references/create-story.md +636 -0
  186. package/skills/funifier/references/create-swap.md +95 -0
  187. package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
  188. package/skills/funifier/references/create-virtual-good.md +96 -0
  189. package/skills/funifier/references/create-webhook.md +72 -0
  190. package/skills/funifier/references/create-websocket.md +71 -0
  191. package/skills/funifier/references/create-widget.md +76 -0
  192. package/skills/funifier/references/debug.md +87 -0
  193. package/skills/funifier/references/help.md +81 -0
  194. package/skills/funifier/references/implement-frontend.md +106 -0
  195. package/skills/funifier/references/import-csv.md +75 -0
  196. package/skills/funifier/references/manage-player.md +82 -0
  197. package/skills/funifier/references/manage-team.md +76 -0
  198. package/skills/funifier/references/upload-file.md +91 -0
  199. package/datasource-funifier-docs/.search-index.json +0 -17318
  200. package/datasource-funifier-docs/.skills-map.json +0 -73
  201. package/skills/funifier-create-aggregate/SKILL.md +0 -127
  202. package/skills/funifier-create-challenge/SKILL.md +0 -88
  203. package/skills/funifier-create-custom-page/SKILL.md +0 -127
  204. package/skills/funifier-create-level/SKILL.md +0 -87
  205. package/skills/funifier-create-quiz/SKILL.md +0 -87
  206. package/skills/funifier-create-scheduler/SKILL.md +0 -127
  207. package/skills/funifier-create-virtual-good/SKILL.md +0 -87
  208. package/skills/funifier-debug/SKILL.md +0 -92
  209. package/skills/funifier-help/SKILL.md +0 -86
  210. package/skills/funifier-implement-frontend/SKILL.md +0 -90
  211. package/skills/funifier-index/SKILL.md +0 -58
@@ -1,136 +1,656 @@
1
- # Question (Pergunta)
1
+ # `question`
2
2
 
3
3
  **Acesso Studio:** `/studio/question`
4
4
  **API Endpoint:** `/v3/question`
5
+ **Coleção MongoDB:** `question` (definições) e `question_log` (respostas dos jogadores)
5
6
 
6
- ## O que é
7
+ > Documentação de engenharia reversa baseada **exclusivamente** no código-fonte do projeto `funifier-service` (commit `830037e`). Fonte primária:
8
+ > `QuestionRest.java`, `QuestionManager.java`, `Question.java`, `QuestionLog.java`, `Choice.java`, `Feedback.java`, `InteractionModel.java` (+ `MatchingModel`, `MatchingItem`, `MatchingPair`, `MissingWordsModel`, `Blank`, `PoolItem`, `DragDropTextModel`, `Target`) e `presentation/PresentationSettings.java`.
7
9
 
8
- Configuração de perguntas e respostas para os jogadores. Permite criar perguntas em diferentes formatos (múltipla escolha, verdadeiro/falso, com mídia), definir respostas corretas ou colher opiniões, e aplicar pesos diferentes para as perguntas. A apresentação pode ser no formato tradicional ou em mini games como caça-palavras e cruzadinhas.
10
+ ---
9
11
 
10
- ## Quando usar
12
+ ## 1. Visão Geral
11
13
 
12
- - Para criar quizzes e avaliações
13
- - Para pesquisas de opinião
14
- - Para treinamentos e capacitação
15
- - Para mini games educativos
14
+ O módulo `question` implementa **perguntas avaliáveis e coletas de resposta** dentro da gamificação Funifier. Ele tem dois documentos distintos:
16
15
 
17
- ## Checklist de Configuração no Studio
16
+ - **`question`** a *definição* da pergunta (enunciado, opções, gabarito, tipo, peso, feedbacks, limites).
17
+ - **`question_log`** — o *registro de cada resposta* dada por um jogador, já com a nota calculada (`grade`) e o percentual de acerto (`percent`).
18
18
 
19
- - [ ] Definir tipo da pergunta (MULTIPLE_CHOICE, TRUE_FALSE, etc.)
20
- - [ ] Definir título e texto da pergunta
21
- - [ ] Criar opções de resposta com grades (pontuação)
22
- - [ ] Definir se permite uma ou múltiplas respostas (select)
23
- - [ ] Configurar embaralhamento (shuffle)
24
- - [ ] Definir peso/grade da pergunta
19
+ Papel arquitetural:
25
20
 
26
- ## API Endpoints
21
+ - É um módulo de **avaliação síncrona**: ao registrar um `question_log`, o `QuestionManager` correge a resposta no mesmo request (`insertLog`) e devolve `grade`, `percent`, `feedbacks` e eventuais `errors` na própria resposta HTTP.
22
+ - Funciona **standalone** ou como **peça interna de um `quiz`**. Quando o log carrega `quiz_log`, o módulo recalcula o progresso do quiz (`QuizManager.updateLog`).
23
+ - É uma **porta de entrada para o motor de gamificação**: se a pergunta tiver o campo `action`, responder gera um `ActionLog` que é processado por `ActionManager.trackWithRestrictions`, podendo creditar pontos, completar desafios e disparar achievements.
24
+ - Persiste tudo diretamente via `Jongo` (MongoDB). **Não há `Repository`/`Dao` dedicado** — a manipulação é feita dentro do próprio `QuestionManager`.
27
25
 
28
- ### Listar Perguntas
29
- **Método:** GET
30
- **Endpoint:** `/v3/question`
26
+ Problemas que resolve:
31
27
 
32
- ### Criar Pergunta
33
- **Método:** POST
34
- **Endpoint:** `/v3/question`
28
+ - Quizzes, avaliações, provas, pesquisas de opinião e mini-games (matching, completar lacunas, arrastar-e-soltar).
29
+ - Correção automática multi-formato (8 tipos de pergunta) com pesos configuráveis.
30
+ - Coleta auditável de respostas por jogador, com limites de tentativas e integração com triggers/actions.
31
+
32
+ Relação com outros módulos (confirmada no código):
33
+
34
+ | Módulo | Direção | Como |
35
+ |---|---|---|
36
+ | `quiz` | `question` → `quiz` | `insertLog` chama `QuizManager.updateLog(quiz_log)` quando o log pertence a um quiz |
37
+ | `action` / `achievement` | `question` → `action` | `insertLog` cria um `ActionLog` e chama `ActionManager.trackWithRestrictions` quando `question.action` está setado |
38
+ | `trigger` | bidirecional | eventos `before_create`/`after_create`/`before_update`/`after_update` na coleção `question`; `before_create`/`after_create` na coleção `question_log` |
39
+ | `player` | leitura | `PlayerManager.findById` em `verifyLimit` e na montagem do contexto de feedback |
40
+ | `technique` (GameTechnique) | escrita pontual | `GameTechniqueManager.autoConfigureMissingTechniqueFields` atribui o código `GT05` a perguntas sem `techniques` |
41
+
42
+ > **Fora de escopo deste módulo** (não servidos por `/v3/question`): a coleção `partner_question` (mesma classe `Question`, mas outra coleção) e o pacote `com.funifier.engine.system.question` (perguntas de suporte do sistema: `accountId`, `topic`, `subject`, `description` — domínio totalmente distinto). Ver §7.
43
+
44
+ ---
45
+
46
+ ## 2. Arquitetura e Fluxos
47
+
48
+ ### 2.1 Classes envolvidas
49
+
50
+ | Classe | Papel |
51
+ |---|---|
52
+ | `com.funifier.rest.v3.rest.QuestionRest` | Controller REST v3 (`/v3/question`) |
53
+ | `com.funifier.engine.question.QuestionManager` | Manager: CRUD, validação, correção por tipo, limites, integração quiz/action |
54
+ | `com.funifier.engine.question.Question` | Documento raiz da coleção `question` |
55
+ | `com.funifier.engine.question.QuestionLog` | Documento da coleção `question_log` (resposta do jogador) |
56
+ | `Choice` | Opção de resposta (formato legado) |
57
+ | `Feedback` | Mensagem de retorno por evento (`correct`/`partial_correct`/`wrong`) |
58
+ | `InteractionModel` + `MatchingModel`/`MissingWordsModel`/`DragDropTextModel` | Modelo "novo" para tipos especiais (substitui a avaliação legada por `choices`) |
59
+ | `MatchingItem`, `MatchingPair`, `Blank`, `PoolItem`, `Target` | Sub-elementos dos modelos novos |
60
+ | `presentation.PresentationSettings` | Configuração de UI (`FeedbackSettings`, `StuckSettings`) — apenas armazenada |
61
+
62
+ Não há Scheduler/Job/Runner que processe `question`/`question_log` (confirmado por busca no projeto).
63
+
64
+ ### 2.2 Pipeline de criação/atualização — `insert(Question)`
65
+
66
+ `QuestionManager.insert` (linhas 69-104) é um **upsert idempotente por `_id`**:
67
+
68
+ ```
69
+ [1] validate(question) → coleta erros por tipo
70
+ se erros.size() > 0 → NÃO salva; retorna a lista de erros (NÃO lança exceção)
71
+ [2] gera _id (Guid.newShortGuid) se vazio
72
+ [3] busca documento atual por _id
73
+ se não existe → created = now; create = true
74
+ se existe → mantém created original; create = false
75
+ [4] updated = now (sempre)
76
+ [5] trigger before_create OU before_update (coleção "question")
77
+ [6] normalizeQuestionForPersistence(question) → normaliza aliases do dragDropText
78
+ [7] c.save(question) → upsert no Mongo
79
+ [8] trigger after_create OU after_update (coleção "question")
80
+ ```
81
+
82
+ ```mermaid
83
+ flowchart TD
84
+ A["POST /v3/question"] --> B["QuestionManager.insert"]
85
+ B --> C{"validate() vazio?"}
86
+ C -->|"não"| Z["retorna exceptions<br/>NÃO salva — HTTP 201"]
87
+ C -->|"sim"| D["gera _id se vazio"]
88
+ D --> E{"_id já existe?"}
89
+ E -->|"não"| F["created=now<br/>trigger before_create"]
90
+ E -->|"sim"| G["mantém created<br/>trigger before_update"]
91
+ F --> H["updated=now<br/>normalizeQuestionForPersistence"]
92
+ G --> H
93
+ H --> I["c.save(question)"]
94
+ I --> J["trigger after_create / after_update"]
95
+ ```
96
+
97
+ > **Atenção (síncrono, sem transação):** não há transação MongoDB. Os triggers `before_*` rodam **antes** do `save`; se um trigger lançar, o comportamento depende do `TriggerManager` (fora do escopo deste módulo).
98
+
99
+ ### 2.3 Pipeline de resposta — `insertLog(QuestionLog, async)`
100
+
101
+ `QuestionManager.insertLog` (linhas 425-583) corrige e persiste a resposta **de forma síncrona**:
102
+
103
+ ```
104
+ [1] valida obrigatórios: log != null, log.question, log.player
105
+ [2] carrega Question por _id (erro "question dont exist" se não achar)
106
+ [3] verifyLimit(question, log, errors) → checa limite de tentativas
107
+ se há erros → encerra sem salvar
108
+ [4] dispatch por question.type → answerXxx(...) → calcula log.percent e log.grade
109
+ [5] monta log._id:
110
+ se log.quiz_log != null → id = quiz_log + "_" + question (impede 2ª resposta no quiz)
111
+ senão → id = Guid.newShortGuid()
112
+ [6] log.time = now (se ausente)
113
+ [7] feedbacks = question.getFeedback(log.grade) ← chave é grade, NÃO percent (ver §5)
114
+ [8] monta ActionLog (al) com id=log.id, actionId=question.action, userId=log.player
115
+ [9] trigger before_create (coleção "question_log", com TriggerContext)
116
+ [10] save(question_log)
117
+ [11] trigger after_create
118
+ [12] meta.attemptsCount = count(question_log {player, question})
119
+ [13] se quiz_log != null → QuizManager.updateLog(quiz_log)
120
+ [14] resolve mensagens de feedback via Mustache (player, log, feedbacks, errors, question)
121
+ [15] se question.action != null:
122
+ valida action existe (erro se não) → ActionManager.trackWithRestrictions(al, async)
123
+ adiciona result["action"]
124
+ [16] result = { log, feedbacks?, errors?, meta? , action? }
125
+ ```
126
+
127
+ ```mermaid
128
+ sequenceDiagram
129
+ participant C as Client
130
+ participant QR as QuestionRest
131
+ participant QM as QuestionManager
132
+ participant TM as TriggerManager
133
+ participant QZ as QuizManager
134
+ participant AM as ActionManager
135
+ C->>QR: POST /v3/question/log
136
+ QR->>QM: insertLog(log, async)
137
+ QM->>QM: valida + carrega Question
138
+ QM->>QM: verifyLimit
139
+ QM->>QM: answerXxx → percent, grade
140
+ QM->>TM: execute(before_create, question_log)
141
+ QM->>QM: save(question_log)
142
+ QM->>TM: execute(after_create, question_log)
143
+ alt log.quiz_log != null
144
+ QM->>QZ: updateLog(quiz_log)
145
+ end
146
+ alt question.action != null
147
+ QM->>AM: trackWithRestrictions(actionLog, async)
148
+ AM-->>QM: action (points/achievements)
149
+ end
150
+ QM-->>QR: {log, grade, percent, feedbacks, action, errors}
151
+ ```
152
+
153
+ ### 2.4 Avaliação por tipo (métodos `answerXxx`)
154
+
155
+ | Tipo | Método | Formato de `answer` | Cálculo de `percent` |
156
+ |---|---|---|---|
157
+ | `MULTIPLE_CHOICE` | `answerMultipleChoice` | `List<String>` (valores `choice.answer`) | **soma** de `choice.grade` das opções marcadas. **Sem limite superior** (pode passar de 1.0) |
158
+ | `TRUE_FALSE` | `answerTrueFalse` | `Boolean` | `1` se `answer == question.correctAnswer`, senão `0` |
159
+ | `MATCHING` | `answerMatching` | `Map<String,String>` (`leftId`→`rightId`) | modelo: `correct/totalPairs` (`per_pair`) ou tudo-ou-nada (`all_or_nothing`); legado: `1/nº choices` por par correto (`answer`+`match`) |
160
+ | `SHORT_ANSWER` | `answerShortAnswer` | `String` | **maior** `choice.grade` entre as opções que casam com o texto (case-sensitive opcional, default `false`) |
161
+ | `SELECT_MISSING_WORDS` | `answerSelectMissingWords` | `Map<String,String>` (`blankId`→`optionId`) | modelo: `correct/nº blanks` (`per_blank`) ou `all_or_nothing`; legado: `1/nº lacunas [[n]]` por acerto |
162
+ | `DRAG_AND_DROP_INTO_TEXT` | `answerDragAndDropIntoText` | `Map<String,String>` (`targetId`→`optionId`) | modelo: `correct/nº targets` (`per_target`) ou `all_or_nothing`; legado: `1/nº lacunas [[n]]` por acerto |
163
+ | `ESSAY` | `answerEssay` | `String` | **sempre `0`** (não há correção automática) |
164
+ | `CUSTOM` | `answerCustom` | qualquer | **sempre `0`** (sem avaliação) |
165
+
166
+ **Em todos os tipos**: `log.grade = question.grade * percent` e `log.percent = percent`. Ou seja, `grade` é o `percent` multiplicado pelo peso da pergunta.
167
+
168
+ Pseudocódigo do dispatcher (`insertLog`, linhas 450-473):
169
+
170
+ ```
171
+ se type == MULTIPLE_CHOICE → answerMultipleChoice
172
+ senão se type == TRUE_FALSE → answerTrueFalse
173
+ senão se type == MATCHING → answerMatching
174
+ senão se type == SHORT_ANSWER → answerShortAnswer
175
+ senão se type == SELECT_MISSING_WORDS → answerSelectMissingWords
176
+ senão se type == DRAG_AND_DROP_INTO_TEXT → answerDragAndDropIntoText
177
+ senão se type == ESSAY → answerEssay
178
+ senão se type == CUSTOM → answerCustom
179
+ (nenhum else: tipo desconhecido NÃO calcula grade/percent → ficam null)
180
+ ```
181
+
182
+ > **Modelo novo vs legado:** para `MATCHING`, `SELECT_MISSING_WORDS` e `DRAG_AND_DROP_INTO_TEXT`, a correção usa `question.model` **se** ele estiver presente e `model.interactionType` for `null` ou igual ao `type`. Caso contrário cai no caminho **legado** baseado em `choices` e marcadores `[[n]]` no campo `question`.
183
+
184
+ ### 2.5 Cálculo de "já respondida" — campo `answered` (runtime, não persistido)
185
+
186
+ `findAllPaginated` (linhas 121-186) injeta o booleano `answered` **apenas quando `player` é informado**, via pipeline `$facet`:
187
+
188
+ ```
189
+ { $match: { _id: { $exists: true } , <id?> , <q?> } }
190
+ { $facet: {
191
+ p1: [ { $match: { _id: { $in: <ids_respondidos> } } }, { $addFields: { answered: true } } ],
192
+ p2: [ { $match: { _id: { $nin: <ids_respondidos> } } }, { $addFields: { answered: false } } ]
193
+ } }
194
+ { $project: { all: { $concatArrays: [ "$p1", "$p2" ] } } }
195
+ { $unwind: "$all" }
196
+ { $replaceRoot: { newRoot: "$all" } }
197
+ ```
198
+
199
+ `<ids_respondidos>` vem de `getAnsweredQuestionIds`: `db.question_log.distinct("question", { player: #, time: {$gte/$lte} })`. Os filtros `answered_min`/`answered_max` aceitam timestamp RFC 3339 **ou** keywords relativas (`-1d`, `-30m`, etc.) via `DateUtil.fromKeyword`.
200
+
201
+ ---
202
+
203
+ ## 3. Estrutura dos Objetos
204
+
205
+ > Todas as classes do módulo usam `@JsonIgnoreProperties(ignoreUnknown=true)` → **qualquer campo desconhecido enviado é silenciosamente descartado** (sem erro, sem log).
206
+
207
+ ### 3.1 `Question` — documento da coleção `question`
208
+
209
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
210
+ |---|---|---|---|---|
211
+ | `_id` | String | GUID curto se vazio | — | Identificador. Definido pelo cliente ou gerado em `insert` |
212
+ | `quiz` | String | `null` | não | ID do quiz pai. **Apenas referência informativa** — o vínculo de resposta é feito via `quiz_log` no log |
213
+ | `type` | String | `null` | **sim** | Tipo da pergunta (enum, §3.5) |
214
+ | `title` | String | `null` | **sim** (exceto `CUSTOM`) | Título usado no admin. Exigido por `validateBasic` |
215
+ | `question` | String | `null` | depende | Enunciado. Obrigatório (com `[[1]]`) nos caminhos **legados** de `DRAG_AND_DROP_INTO_TEXT` e `SELECT_MISSING_WORDS` |
216
+ | `grade` | double | `1` | não | **Peso** da pergunta. `log.grade = grade * percent` |
217
+ | `choices` | `List<Choice>` | `[]` | depende | Opções (obrigatório em `MULTIPLE_CHOICE`; usado nos caminhos legados) |
218
+ | `i18n` | `Map<String,Map<String,String>>` | `{}` | não | Traduções. **Apenas armazenado** — nenhuma lógica de servidor o lê |
219
+ | `techniques` | `List<String>` | `null` | não | Códigos de técnica de jogo (GT). Default `GT05` via job de técnicas |
220
+ | `select` | String | `null` | **sim** p/ `MULTIPLE_CHOICE` | `one_answer` / `multiple_answers`. **Dica de UI** — a correção NÃO força escolha única |
221
+ | `answerNumbering` | String | `null` | não | `arabic_numerals` / `roman_numerals` / `uppercase_letters` (UI) |
222
+ | `correctAnswer` | Boolean | `null` | **sim** p/ `TRUE_FALSE` | Resposta correta do verdadeiro/falso |
223
+ | `caseSensitive` | Boolean | `null` | **sim** p/ `SHORT_ANSWER` | Na avaliação assume `false` se `null` |
224
+ | `totalLines` | Integer | `null` | não | Linhas sugeridas para `ESSAY` (UI) |
225
+ | `shuffle` | boolean | `false` | não | Embaralhar opções (UI) |
226
+ | `feedbacks` | `List<Feedback>` | `[]` | não | Mensagens por evento (§3.4) |
227
+ | `presentation` | `PresentationSettings` | `null` | não | Settings de UI (feedback/stuck) — apenas armazenado |
228
+ | `model` | `InteractionModel` | `null` | não | Modelo novo para `MATCHING`/`SELECT_MISSING_WORDS`/`DRAG_AND_DROP_INTO_TEXT` |
229
+ | `extra` | `Map<String,Object>` | `{}` | não | Campos livres (imagens, vídeo, metadados) |
230
+ | `created` | Date | auto | — | Definido no primeiro `insert`; preservado em updates |
231
+ | `updated` | Date | auto | — | Atualizado a cada `insert` |
232
+ | `limit` | `Limit` | `null` | não | Limite de respostas (§3.6) |
233
+ | `requires` | `List<Requirement>` | `[]` | não | **Aceito e persistido, mas NUNCA lido** pelo `QuestionManager` (§7) |
234
+ | `action` | String | `null` | não | ID/nome da action disparada ao responder |
235
+
236
+ **Campos computados (não persistem na coleção `question`):**
237
+ - `answered` (boolean) — injetado em runtime no `GET /v3/question` quando `player` é informado (§2.5).
238
+
239
+ **Campos aceitos e silenciosamente ignorados:**
240
+ - Qualquer atributo não declarado nas classes (`@JsonIgnoreProperties(ignoreUnknown=true)`). Exemplos comuns que **não existem** no modelo de `Choice` e seriam descartados: `gradePercent`, `gradeCheck`.
241
+
242
+ ### 3.2 `QuestionLog` — documento da coleção `question_log`
243
+
244
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
245
+ |---|---|---|---|---|
246
+ | `_id` | String | GUID, ou `quiz_log + "_" + question` | — | Em quiz, o id composto impede responder a mesma pergunta 2x |
247
+ | `player` | String | — | **sim** | Jogador que respondeu. `"me"` resolve para o player do token |
248
+ | `question` | String | — | **sim** | ID da pergunta respondida |
249
+ | `answer` | Object | — | **sim** | Resposta crua; formato varia por tipo (§2.4) |
250
+ | `percent` | Double | calculado | — | Percentual de acerto (saída da correção) |
251
+ | `grade` | Double | calculado | — | `question.grade * percent` (saída da correção) |
252
+ | `time` | Date | now | — | Momento da resposta |
253
+ | `mentor` | String | `null` | não | Instrutor avaliador. **Aceito/persistido, mas sem workflow** que o utilize (§7) |
254
+ | `extra` | `Map<String,Object>` | `null` | não | Campos adicionais |
255
+ | `quiz` | String | `null` | não | Referência ao quiz |
256
+ | `quiz_log` | String | `null` | não | Vincula a resposta ao log do quiz (dispara `QuizManager.updateLog`) |
257
+
258
+ ### 3.3 `Choice` — opção de resposta (formato legado)
259
+
260
+ | Campo | Tipo | Descrição |
261
+ |---|---|---|
262
+ | `answer` | String | Identificador/valor da opção (referenciado pela resposta do jogador) |
263
+ | `label` | String | Texto amigável exibido |
264
+ | `grade` | Double | Percentual desta opção (0..1). Em `MULTIPLE_CHOICE` soma; em `SHORT_ANSWER` usa-se o máximo |
265
+ | `match` | String | Sub-pergunta do `MATCHING` legado |
266
+ | `extra` | `Map<String,Object>` | Metadados |
267
+
268
+ ### 3.4 `Feedback`
269
+
270
+ | Campo | Tipo | Descrição |
271
+ |---|---|---|
272
+ | `event` | String | `correct` / `partial_correct` / `wrong` |
273
+ | `message` | String | Texto (processado por Mustache com `{player, log, feedbacks, errors, question}`) |
274
+
275
+ ### 3.5 Enum de tipos (`Question.TYPE_*`)
276
+
277
+ | Valor | Significado operacional |
278
+ |---|---|
279
+ | `MULTIPLE_CHOICE` | Múltipla escolha; nota = soma dos `grade` das opções marcadas |
280
+ | `TRUE_FALSE` | Verdadeiro/Falso; nota cheia ou zero |
281
+ | `MATCHING` | Relacionar colunas (left↔right); pontuação por par ou tudo-ou-nada |
282
+ | `SHORT_ANSWER` | Resposta curta em texto; nota = maior `grade` entre as opções que casam |
283
+ | `SELECT_MISSING_WORDS` | Selecionar palavras em lacunas (dropdown) |
284
+ | `DRAG_AND_DROP_INTO_TEXT` | Arrastar opções para lacunas do texto |
285
+ | `ESSAY` | Dissertativa; **nota sempre 0** (correção manual não implementada) |
286
+ | `CUSTOM` | Customizada; **sem validação e sem avaliação** (nota 0) |
287
+
288
+ Outros enums:
289
+ - `select`: `one_answer`, `multiple_answers`.
290
+ - `answerNumbering`: `arabic_numerals`, `roman_numerals`, `uppercase_letters`.
291
+ - `scoring` (modelos novos): `per_pair`/`per_blank`/`per_target` (default) ou `all_or_nothing`.
292
+
293
+ ### 3.6 `Limit` — limite de respostas
294
+
295
+ | Campo | Tipo | Descrição |
296
+ |---|---|---|
297
+ | `total` | Object | Número **ou** expressão Mustache+exp4j (`{{player.extra.max}}`) avaliada em runtime |
298
+ | `per` | String | `player` ou `gamification`. **`team` é definido mas NÃO tratado** em `verifyLimit` (§5/§7) |
299
+ | `every` | String | Janela (ex.: `1d`, `5h`). Sem `every` → janela de `-10y` |
300
+ | `query` | String | Filtro Mongo adicional (Mustache, ex.: `attributes.city:'{{player.extra.city}}'`) |
301
+
302
+ ### 3.7 `InteractionModel` e submodelos (modelo novo)
303
+
304
+ `InteractionModel`: `interactionType` (`MATCHING`/`SELECT_MISSING_WORDS`/`DRAG_AND_DROP_INTO_TEXT`), `questionHtml`, `shufflePool`, mais um dos modelos:
305
+
306
+ - **`MatchingModel`**: `left: List<MatchingItem{id,text}>`, `right: List<MatchingItem>`, `solution: List<MatchingPair{leftId,rightId}>`, `solutions: Map<String,String>` (forma alternativa), `scoring`.
307
+ - **`MissingWordsModel`**: `html` (com `[[1]]`...), `blanks: List<Blank>`, `scoring`. `Blank`: `id`, `options: List<PoolItem{id,text}>`, `correctId` (JSON: `correctOptionId`, alias `correctId`).
308
+ - **`DragDropTextModel`**: `html` (JSON `text`, alias `html`), `pool` (JSON `optionsPool`, alias `pool`), `targets: List<Target>`, `scoring`. `Target`: `id`, `accepts` (JSON `acceptedOptionIds`, alias `accepts`), `correctId` (JSON `correctOptionId`, alias `correctId`).
309
+
310
+ > **Normalização assimétrica (`normalizeQuestionForPersistence`, linhas 42-65):** apenas o `dragDropText` é "endurecido" — o manager copia manualmente `text→html`, `optionsPool→pool`, `target.acceptedOptionIds→accepts`, `target.correctOptionId→correctId` antes de salvar. `MatchingModel` e `MissingWordsModel` dependem **somente** dos `@JsonAlias` do Jackson. Ver §7.
311
+
312
+ ### 3.8 Técnicas de jogo (`techniques`)
313
+
314
+ | Código | Origem no código | Significado |
315
+ |---|---|---|
316
+ | `GT05` | `GameTechniqueManager` (`BLANKFILLS_CODE`) | Atribuído automaticamente a perguntas **sem** `techniques` durante `autoConfigureMissingTechniqueFields` |
317
+
318
+ Outros códigos GT podem ser adicionados manualmente ao array `techniques`, mas o único default automático para `question` é `GT05`.
319
+
320
+ ---
321
+
322
+ ## 4. Endpoints
323
+
324
+ Base: `/v3/question`. Autenticação: **Bearer token** (`AuthBean`) em todos. Multi-tenant via `apiKey` (`FrontController.getInstance(apiKey)`).
325
+
326
+ ### `POST /v3/question`
327
+
328
+ | Aspecto | Detalhe |
329
+ |---|---|
330
+ | Finalidade | Criar **ou** atualizar (upsert por `_id`) uma pergunta |
331
+ | Full replace ou patch | **Full replace** — o `save` grava o documento inteiro recebido |
332
+ | Status | Sempre `201 CREATED`, **mesmo com erros de validação** |
333
+
334
+ **Comportamento real:** a validação **não lança** — erros voltam no array `exceptions` do corpo e o documento **não é salvo**. O cliente **deve** inspecionar `exceptions`.
35
335
 
36
- **Exemplo de Body:**
37
336
  ```json
38
- {
39
- "_id": "64a5b2c2d8dcca49bcf7eb6e",
40
- "type": "MULTIPLE_CHOICE",
41
- "title": "Visual Components",
42
- "question": "In Funifier, what is the small visual components that can be added to a web page to display feedbacks?",
43
- "grade": 1,
44
- "choices": [
45
- { "answer": "1", "label": "Points", "grade": 0, "extra": {} },
46
- { "answer": "2", "label": "Badges", "grade": 0, "extra": {} },
47
- { "answer": "3", "label": "Challenges", "grade": 0, "extra": {} },
48
- { "answer": "4", "label": "Leaderboards", "grade": 0, "extra": {} },
49
- { "answer": "5", "label": "Widgets", "grade": 1, "extra": {} }
50
- ],
51
- "techniques": ["GT05"],
52
- "select": "one_answer",
53
- "answerNumbering": "uppercase_letters",
54
- "shuffle": false,
55
- "feedbacks": [],
56
- "extra": {}
57
- }
337
+ // resposta
338
+ { "question": { "_id": "...", "type": "MULTIPLE_CHOICE", ... }, "exceptions": [] }
58
339
  ```
59
340
 
60
- ### Deletar Pergunta
61
- **Método:** DELETE
62
- **Endpoint:** `/v3/question/:id`
341
+ ### `POST /v3/question/bulk`
63
342
 
64
- ### Listar Respostas de Jogadores
65
- **Método:** GET
66
- **Endpoint:** `/v3/question/log?question=:id`
343
+ Recebe `List<Question>`; chama `insert` para cada. Resposta separa `registered`/`total_registered` de `ignored`/`total_ignored` (itens com `exceptions`). Status `201`.
67
344
 
68
- ### Criar Resposta de Jogador
69
- **Método:** POST
70
- **Endpoint:** `/v3/question/log`
345
+ ### `GET /v3/question/{id}`
346
+
347
+ | Param | Tipo | Descrição |
348
+ |---|---|---|
349
+ | `id` (path) | String | ID da pergunta, ou `FUNIFIER_RANDOM_QUESTION` |
350
+ | `q` | String | Critério Mongo (usado no random) |
351
+ | `player` | String | `"me"` resolve para o player do token |
352
+ | `answered_min` / `answered_max` | String | Janela RFC 3339 ou keyword |
353
+
354
+ **Comportamento real:** se `id == FUNIFIER_RANDOM_QUESTION` **e** `player` informado → `findRandomNotAnsweredForPlayer` (sorteia uma pergunta ainda não respondida pelo player). Senão → `find(id)` simples.
355
+
356
+ ### `GET /v3/question`
357
+
358
+ | Param | Tipo | Descrição |
359
+ |---|---|---|
360
+ | `id` | String | Filtra por id; aceita `FUNIFIER_RANDOM_QUESTION` |
361
+ | `player` | String | Se informado, injeta `answered:true/false` por pergunta (§2.5). `"me"` suportado |
362
+ | `answered_min`/`answered_max` | String | Janela para considerar "respondida" |
363
+ | `q` | String | Critério Mongo cru concatenado no `$match` |
364
+ | `fields` | String | Projeção (`campo1,campo2`) |
365
+ | `orderby` | String | Campo de ordenação |
366
+ | `reverse` | boolean | Ordem descendente |
367
+ | `Range` (header) | String | Paginação `items=0-100` (default/máx 100) |
368
+
369
+ ### `DELETE /v3/question/{id}`
370
+
371
+ | Aspecto | Detalhe |
372
+ |---|---|
373
+ | Finalidade | Remover a pergunta |
374
+ | Status | `204 NO_CONTENT` |
375
+
376
+ **Comportamento real (cascata, não-REST padrão):** além de remover o documento `question`, executa `db.question_log.remove({ question: id })` — **apaga todos os logs de resposta daquela pergunta** (`delete`, linhas 111-115). Operação irreversível.
377
+
378
+ ### `GET /v3/question/log`
379
+
380
+ Lista respostas. Params: `player`, `question`, `q`, `published_min`, `published_max`, `orderby`, `reverse`, `max_results` (default 100). **Não paginado por header** (retorna lista direta). Não existe `GET /v3/question/log/{id}` (busca de log único é só por filtro).
381
+
382
+ ### `POST /v3/question/log`
383
+
384
+ | Aspecto | Detalhe |
385
+ |---|---|
386
+ | Finalidade | Registrar e **corrigir** a resposta de um jogador |
387
+ | Query `async` | Repassado para `ActionManager.trackWithRestrictions` |
388
+ | Status | `201 CREATED` |
389
+
390
+ **Comportamento real:** corrige a resposta no mesmo request; devolve `grade`, `percent`, `feedbacks`, `meta.attemptsCount` e, se houver `action`, o resultado do tracking. Erros de formato/limite voltam em `errors`. `player == "me"` (ou ausente) resolve para o token.
71
391
 
72
- **Exemplo de Body:**
73
392
  ```json
393
+ // request
394
+ { "player": "ricardo@x.com", "question": "q1", "answer": ["Pedro Alvares Cabral"] }
395
+ // response (exemplo)
74
396
  {
75
- "question": "64a5b2c2d8dcca49bcf7eb6e",
76
- "answer": ["5"],
77
- "player": "tom"
397
+ "log": { "_id":"...", "player":"ricardo@x.com", "question":"q1", "answer":["Pedro Alvares Cabral"], "percent":1.0, "grade":1.0, "time":"..." },
398
+ "feedbacks": [ { "event":"correct", "message":"Parabéns você acertou" } ],
399
+ "meta": { "attemptsCount": 1 }
78
400
  }
79
401
  ```
80
402
 
81
- ### Deletar Resposta
82
- **Método:** DELETE
83
- **Endpoint:** `/v3/question/log/:id`
403
+ ### `POST /v3/question/log/bulk`
84
404
 
85
- ## Tipos de Pergunta
405
+ Recebe `List<QuestionLog>`; chama `insertLog` para cada (capturando exceções por item). Resposta agrega `registered`/`total_registered`.
86
406
 
87
- | Tipo | Descrição |
88
- |------|-----------|
89
- | `MULTIPLE_CHOICE` | Múltipla escolha (uma ou várias respostas) |
90
- | `TRUE_FALSE` | Verdadeiro ou Falso |
91
- | `ESSAY` | Dissertativa (resposta livre) |
92
- | `CROSSWORD` | Caça-palavras (mini game) |
93
- | `WORD_SEARCH` | Cruzadinha (mini game) |
407
+ ### `POST /v3/question/log/aggregate`
94
408
 
95
- ## Campos Importantes
409
+ Roda aggregation customizada sobre `question_log`. Body = `List<String>` de estágios Mongo. Filtros `player`/`question`/`q`/`published_min`/`published_max` montam o `$match` inicial; os estágios do body são anexados. Retorna via `Content-Range`.
96
410
 
97
- | Campo | Tipo | Descrição |
98
- |-------|------|-----------|
99
- | `quiz` | String | ID do quiz pai (vincula pergunta ao quiz) |
100
- | `type` | String | Tipo da pergunta (ver tabela acima) |
101
- | `question` | String | Texto da pergunta |
102
- | `grade` | Number | Peso/pontuação da pergunta |
103
- | `choices` | Array | Opções de resposta (ver estrutura abaixo) |
104
- | `select` | String | `one_answer` ou `multiple_answers` |
105
- | `shuffle` | Boolean | Embaralhar opções |
106
- | `techniques` | Array | IDs de técnicas de jogos vinculadas |
107
-
108
- ### Estrutura de Choice (Opção de Resposta)
411
+ ---
412
+
413
+ ## 5. Regras de Negócio
414
+
415
+ Regras presentes no código mas não óbvias no schema:
416
+
417
+ 1. **Validação por tipo (`validate`, linhas 673-709):**
418
+ - Todos (exceto `CUSTOM`): exigem `type` e `title` (`validateBasic`).
419
+ - `MULTIPLE_CHOICE`: exige `choices` e `select`.
420
+ - `TRUE_FALSE`: exige `correctAnswer`.
421
+ - `SHORT_ANSWER`: exige `caseSensitive`.
422
+ - `MATCHING`: modelo `left`, `right` e (`solution` **ou** `solutions`); legado → `choices`.
423
+ - `SELECT_MISSING_WORDS`: modelo → `blanks` com `options`; legado → `choices` + `[[1]]` em `question`.
424
+ - `DRAG_AND_DROP_INTO_TEXT`: modelo → `targets` + `[[1]]` em `html`/`question`; legado → `choices` + `[[1]]` em `question`.
425
+ - `ESSAY`: apenas básico. `CUSTOM`: **nenhuma validação**.
426
+
427
+ 2. **Peso vs feedback — pegadinha do `grade`:** `getFeedback` (linhas 129-145) seleciona feedback pela **nota final** `log.grade` (= `question.grade * percent`), **não** pelo `percent`. Limiares (inclusivos):
428
+ - `grade >= 1` → `correct`
429
+ - `grade <= 0` → `wrong`
430
+ - `0 < grade < 1` → `partial_correct`
431
+
432
+ Consequência: se `question.grade != 1`, os limiares deslocam. Ex.: `grade=2, percent=0.5 → log.grade=1 → "correct"`; `grade=0.5, percent=1.0 → log.grade=0.5 → "partial_correct"` mesmo com 100% de acerto.
433
+
434
+ 3. **`MULTIPLE_CHOICE` é ilimitado e não força `select`:** o `percent` é a **soma** dos `choice.grade` das opções marcadas, sem teto. Marcar várias opções "certas" acumula acima de `1.0`. O campo `select: one_answer` é só dica de UI — o backend não impede múltiplas marcações.
435
+
436
+ 4. **Limite de tentativas (`verifyLimit`, linhas 374-421):** se `limit.total` (número ou expressão) for atingido na janela `limit.every`, o log **não é registrado** e um erro é retornado. Suporta `per = player` e `per = gamification`; **`per = team` não tem branch** (é ignorado silenciosamente).
437
+
438
+ 5. **Unicidade dentro de quiz:** quando `quiz_log` está presente, o id do log vira `quiz_log + "_" + question`, garantindo **uma resposta por pergunta por execução de quiz** (re-POST sobrescreve).
439
+
440
+ 6. **Propagação para gamificação:** com `question.action` setado, responder gera um `ActionLog` (atributos: `question`, `percent`, `answer`, `grade`, `type`) processado por `trackWithRestrictions`. Se a action não existir → erro `question.action ... does not exist`.
441
+
442
+ 7. **Multi-tenant:** todo acesso é isolado pelo `apiKey` do token; não há ACL por pergunta. O campo `requires` (que poderia gatear elegibilidade) **não é avaliado**.
443
+
444
+ 8. **Consistência eventual:** sem transação. O `save` do log, o `updateLog` do quiz e o `trackWithRestrictions` da action são passos sequenciais independentes; falha em um passo posterior não desfaz os anteriores.
445
+
446
+ ---
447
+
448
+ ## 6. Comportamentos Automáticos
449
+
450
+ | Comportamento | Trigger | Impacto | Persistência |
451
+ |---|---|---|---|
452
+ | Geração de `_id` | `insert`/`insertLog` sem id | Cria GUID curto (ou id composto em quiz) | Sim |
453
+ | `created`/`updated` | `insert` | `created` no 1º save, `updated` sempre | Sim |
454
+ | Normalização de aliases | `insert` (só `dragDropText`) | `text→html`, `optionsPool→pool`, `acceptedOptionIds→accepts`, `correctOptionId→correctId` | Sim |
455
+ | Triggers de ciclo de vida | `insert`/`insertLog` | `before_*`/`after_*` em `question` e `question_log` | Depende do trigger |
456
+ | Correção automática | `insertLog` | Calcula `percent` e `grade` | Sim (no log) |
457
+ | Atualização do quiz | `insertLog` com `quiz_log` | `QuizManager.updateLog` recalcula percent/grade/questions/answers | Sim (em `quiz_log`) |
458
+ | Tracking de action | `insertLog` com `question.action` | `ActionLog` → pontos/desafios/achievements | Sim (via engine) |
459
+ | Cascata de exclusão | `delete` | Remove todos os `question_log` da pergunta | Sim |
460
+ | `meta.attemptsCount` | `insertLog` | Conta tentativas player×question | Não (só na resposta) |
461
+ | Default de técnica | `autoConfigureMissingTechniqueFields` | Adiciona `GT05` a perguntas sem `techniques` | Sim |
462
+
463
+ ```mermaid
464
+ flowchart LR
465
+ A["question_log salvo"] --> B{"quiz_log definido?"}
466
+ B -->|"sim"| C["QuizManager.updateLog<br/>recalcula progresso do quiz"]
467
+ A --> D{"question.action definido?"}
468
+ D -->|"sim"| E["cria ActionLog<br/>(question, percent, answer, grade, type)"]
469
+ E --> F["ActionManager.trackWithRestrictions"]
470
+ F --> G["Engine: pontos, desafios, achievements"]
471
+ D -->|"não"| H["fim"]
472
+ ```
473
+
474
+ ---
475
+
476
+ ## 7. Suportado vs NÃO Suportado
477
+
478
+ ### ✅ Suportado
479
+
480
+ - CRUD de perguntas (`POST`, `GET`, `DELETE`) + `bulk`.
481
+ - 8 tipos de pergunta com correção automática (exceto `ESSAY`/`CUSTOM`).
482
+ - Modelo novo (`model`) para `MATCHING`/`SELECT_MISSING_WORDS`/`DRAG_AND_DROP_INTO_TEXT`, com `scoring` `per_*` ou `all_or_nothing`.
483
+ - Registro de respostas (`question_log`) com nota, percentual e feedbacks.
484
+ - Limite de tentativas por `player`/`gamification`, com `total` literal ou via expressão.
485
+ - Pergunta aleatória não respondida (`FUNIFIER_RANDOM_QUESTION`).
486
+ - Campo `answered` por jogador no `GET`.
487
+ - Integração com `quiz` (composição) e `action`/`achievement` (gamificação).
488
+ - Triggers de ciclo de vida em `question` e `question_log`.
489
+ - Aggregation customizada de logs (`/log/aggregate`).
490
+ - i18n armazenado para uso de front-end.
491
+
492
+ ### ❌ NÃO Suportado
493
+
494
+ - **Correção/nota de `ESSAY`**: sempre `0`. Não há endpoint para o `mentor` lançar nota — `answerEssay` sobrescreve `grade=0` a cada save.
495
+ - **Campo `mentor`**: aceito e persistido, mas **nenhum fluxo o utiliza**.
496
+ - **Campo `requires` (`List<Requirement>`)**: presente na entidade, **nunca lido** pelo `QuestionManager` — não gateia elegibilidade.
497
+ - **`limit.per = team`**: definido em `Limit`, mas `verifyLimit` só trata `player` e `gamification`.
498
+ - **Forçar `select: one_answer`**: o backend não impede múltiplas marcações em `MULTIPLE_CHOICE`.
499
+ - **Teto de `percent` em `MULTIPLE_CHOICE`**: não há clamp; pode passar de `1.0`.
500
+ - **Campos `gradePercent`/`gradeCheck` em `Choice`**: **não existem** no modelo; seriam silenciosamente descartados (vale para qualquer campo desconhecido, devido a `@JsonIgnoreProperties(ignoreUnknown=true)`).
501
+ - **Hardening de aliases para `Matching`/`MissingWords`**: só o `dragDropText` é normalizado no manager; os outros dependem do Jackson. Se o binding ignorar os aliases, esses modelos podem perder dados silenciosamente.
502
+ - **`GET /v3/question/log/{id}`**: não existe — log único só por filtro.
503
+ - **Validação de tipo desconhecido na correção**: um `type` fora do enum não cai em nenhum `answerXxx` → `grade`/`percent` ficam `null`.
504
+ - **Scheduler/Job**: nenhum job processa `question`/`question_log`.
505
+ - **Fora do módulo**: coleção `partner_question` (mesma classe, outra coleção, não exposta em `/v3/question`) e `com.funifier.engine.system.question.*` (perguntas de suporte do sistema — `accountId`/`topic`/`subject`/`description`).
506
+
507
+ ---
508
+
509
+ ## 8. Segurança e Permissões
510
+
511
+ - **Autenticação:** Bearer token (`AuthBean`) em todas as rotas; resolução de tenant pelo `apiKey` (`FrontController.getInstance`).
512
+ - **Identidade implícita:** `player == "me"` (ou ausente, no `POST /log`) é substituído pelo player do token (`getPlayerFromTokenIfExist`).
513
+ - **Isolamento multi-tenant:** cada `apiKey` opera sobre sua própria conexão `Jongo`/banco. Não há ACL por pergunta — qualquer chamador autenticado no tenant pode ler/escrever/excluir.
514
+ - **Superfície de injeção (por design):** os parâmetros `q`, `orderby` e `fields` são **concatenados como string crua** nas pipelines de aggregation:
515
+ - `query.append(", " + q)` no `$match` (`findAllPaginated`, `findAll`, `findAllQuestionLogs*`).
516
+ - `"{$sort : {" + orderby + " : #}}"` no `$sort`.
517
+ - `fields.split(",")` montando `$project`.
518
+ - `/log/aggregate` injeta diretamente os estágios enviados no body.
519
+
520
+ Isso expõe a sintaxe de query do Mongo ao cliente (comportamento intencional da plataforma Funifier), mas significa que entrada não confiável nesses campos pode alterar a pipeline. **Não sanitize no front esperando proteção do back** — trate esses campos como superfície de query Mongo.
521
+ - **Exclusão destrutiva:** `DELETE /v3/question/{id}` apaga em cascata todos os `question_log` — sem confirmação e sem soft-delete.
109
522
 
523
+ ---
524
+
525
+ ## 9. Observabilidade e Troubleshooting
526
+
527
+ **Diagnóstico — a pergunta existe e está correta?**
528
+ ```
529
+ GET /v3/question/<id>
530
+ GET /v3/question?q={"type":"MULTIPLE_CHOICE"}
531
+ ```
532
+
533
+ **A resposta foi registrada?**
534
+ ```
535
+ GET /v3/question/log?question=<id>&player=<player>
536
+ ```
537
+
538
+ **Quantas tentativas um jogador fez?** (mesmo cálculo do `meta.attemptsCount`)
539
+ ```
540
+ db.question_log.count({ player: "<player>", question: "<id>" })
541
+ ```
542
+
543
+ **Erros comuns e causas:**
544
+
545
+ | Mensagem / sintoma | Causa |
546
+ |---|---|
547
+ | `question dont exist` | `question` do log aponta para id inexistente |
548
+ | `answer in invalid format: type X requires ...` | Formato de `answer` não bate com o tipo (ex.: `MULTIPLE_CHOICE` exige `List<String>`, `MATCHING` exige `Map`) |
549
+ | `you have exced the limit ...` | `limit.total` atingido na janela `limit.every` |
550
+ | `question.action ... does not exist` | `question.action` referencia action inexistente |
551
+ | Pergunta criada mas "não salvou" | Erros em `exceptions` no `POST` (HTTP ainda é 201) — inspecione o array |
552
+ | `grade`/`percent` ausentes no log | `type` fora do enum, ou erro de validação anterior interrompeu a correção |
553
+ | Feedback "errado" para acerto parcial | Lembre que o feedback usa `log.grade` (peso × percent), não `percent` (§5.2) |
554
+ | `ESSAY` sempre com nota 0 | Comportamento esperado — sem correção automática |
555
+ | Dados do modelo "sumiram" em Matching/MissingWords | Aliases não normalizados no manager (só `dragDropText` é) — envie os nomes canônicos |
556
+
557
+ **O que verificar quando algo não funciona:**
558
+ 1. O `type` está no enum e os campos obrigatórios da §5.1 estão presentes?
559
+ 2. O `answer` está no formato da §2.4 para o tipo?
560
+ 3. `question.grade` está em `1` quando você espera que `percent` e `grade` coincidam?
561
+ 4. Para quiz: `quiz_log` está sendo enviado? (sem ele, não há `updateLog`.)
562
+ 5. Para gamificação: `question.action` existe e está ativa?
563
+
564
+ ---
565
+
566
+ ## 10. Exemplos Práticos
567
+
568
+ ### 10.1 Mínimo funcional — Múltipla escolha
569
+ ```json
570
+ POST /v3/question
571
+ {
572
+ "type": "MULTIPLE_CHOICE",
573
+ "title": "Descobrimento",
574
+ "question": "Quem descobriu o Brasil?",
575
+ "select": "one_answer",
576
+ "grade": 1.0,
577
+ "choices": [
578
+ { "answer": "cabral", "label": "Pedro Alvares Cabral", "grade": 1.0 },
579
+ { "answer": "dpedro", "label": "Dom Pedro I", "grade": 0.0 }
580
+ ]
581
+ }
582
+ ```
583
+ Resposta do jogador:
584
+ ```json
585
+ POST /v3/question/log
586
+ { "player": "tom", "question": "<id>", "answer": ["cabral"] }
587
+ ```
588
+
589
+ ### 10.2 Avançado — Matching (modelo novo) + limite + action + feedbacks
110
590
  ```json
591
+ POST /v3/question
111
592
  {
112
- "answer": "1", // identificador único da opção
113
- "label": "Funifier", // texto exibido
114
- "grade": 1, // pontuação se escolhida (0 = errada)
115
- "gradePercent": 100, // percentual de acerto
116
- "gradeCheck": true, // indica se é resposta correta
117
- "extra": {} // metadados adicionais
593
+ "type": "MATCHING",
594
+ "title": "Números romanos",
595
+ "grade": 1.0,
596
+ "model": {
597
+ "interactionType": "MATCHING",
598
+ "matching": {
599
+ "left": [ {"id":"1","text":"1"}, {"id":"2","text":"2"}, {"id":"3","text":"3"} ],
600
+ "right": [ {"id":"I","text":"I"}, {"id":"II","text":"II"}, {"id":"III","text":"III"} ],
601
+ "solution": [ {"leftId":"1","rightId":"I"}, {"leftId":"2","rightId":"II"}, {"leftId":"3","rightId":"III"} ],
602
+ "scoring": "all_or_nothing"
603
+ }
604
+ },
605
+ "feedbacks": [
606
+ { "event": "correct", "message": "Mandou bem, {{player.name}}!" },
607
+ { "event": "wrong", "message": "Tente de novo." }
608
+ ],
609
+ "limit": { "total": 3, "per": "player", "every": "1d" },
610
+ "action": "answer_quiz"
118
611
  }
119
612
  ```
613
+ Resposta:
614
+ ```json
615
+ POST /v3/question/log
616
+ { "player": "tom", "question": "<id>", "answer": { "1": "I", "2": "II", "3": "III" } }
617
+ ```
120
618
 
121
- ## Formatos de Apresentação
619
+ ### 10.3 Anti-pattern (o que NÃO fazer)
620
+ ```json
621
+ // ❌ Esperar que "select":"one_answer" impeça marcar 2 opções certas.
622
+ // O backend SOMA os grades — percent pode passar de 1.0.
623
+ {
624
+ "type": "MULTIPLE_CHOICE",
625
+ "title": "Errado",
626
+ "select": "one_answer",
627
+ "grade": 2.0, // ❌ peso != 1 desloca os limiares de feedback:
628
+ "choices": [ // percent=0.5 → grade=1.0 → feedback "correct"
629
+ { "answer": "a", "grade": 1.0 },
630
+ { "answer": "b", "grade": 1.0 } // marcar a+b → percent=2.0, grade=4.0
631
+ ]
632
+ }
633
+ ```
634
+ Por quê é ruim: (1) `select` não é enforced; (2) `grade=2` faz a seleção de feedback (`correct`/`partial`/`wrong`) divergir do percentual real de acerto. Mantenha `grade=1` quando quiser que `grade` e `percent` coincidam, e não dependa de `select` para validar escolha única.
635
+
636
+ ```json
637
+ // ❌ Campos inexistentes em Choice são descartados em silêncio.
638
+ { "answer": "a", "label": "A", "gradePercent": 100, "gradeCheck": true }
639
+ // 'gradePercent' e 'gradeCheck' NÃO existem no modelo → ignorados. Use apenas 'grade'.
640
+ ```
122
641
 
123
- Perguntas podem incluir conteúdo rico:
124
- - Texto com HTML
125
- - Imagens (campo `image`)
126
- - Vídeos e áudio (via `extra`)
127
- - Mini games (caça-palavras, cruzadinha)
642
+ ---
128
643
 
129
- ## Validações e Testes
644
+ ## Checklist de Configuração
130
645
 
131
- - [ ] Pergunta aparece na lista GET /v3/question
132
- - [ ] Resposta do jogador é registrada corretamente (POST /v3/question/log)
133
- - [ ] Grade é calculada com base na resposta
134
- - [ ] Opções de resposta estão corretas
135
- - [ ] Vinculação com quiz funciona (campo `quiz`)
136
- - [ ] Registro em lote funciona (POST /v3/question/log/bulk)
646
+ - [ ] `type` definido e dentro do enum (`MULTIPLE_CHOICE`, `TRUE_FALSE`, `MATCHING`, `SHORT_ANSWER`, `SELECT_MISSING_WORDS`, `DRAG_AND_DROP_INTO_TEXT`, `ESSAY`, `CUSTOM`).
647
+ - [ ] `title` preenchido (obrigatório exceto em `CUSTOM`).
648
+ - [ ] Campos obrigatórios do tipo presentes (§5.1): `choices`+`select` (MC), `correctAnswer` (TF), `caseSensitive` (SA), `model.*` ou `choices` (MATCHING/SELECT/DRAG).
649
+ - [ ] `grade = 1` se você quer que `log.grade` reflita o percentual de acerto (armadilha §5.2).
650
+ - [ ] Para `DRAG_AND_DROP`/`SELECT_MISSING_WORDS` legados: `[[1]]`, `[[2]]`... presentes no campo `question`.
651
+ - [ ] Resposta (`answer`) enviada no formato correto do tipo (§2.4) — formato errado gera `answer in invalid format`.
652
+ - [ ] Para vincular a um quiz: enviar `quiz_log` no log (sem ele não há `updateLog`).
653
+ - [ ] Para gamificar: `action` existe e está ativa antes de criar a pergunta.
654
+ - [ ] Ciente da cascata: `DELETE /v3/question/{id}` apaga **todos** os `question_log` da pergunta.
655
+ - [ ] Não confie em `select` para forçar escolha única, nem use campos inexistentes (`gradePercent`, `gradeCheck`).
656
+ - [ ] Inspecionar `exceptions` no `POST /v3/question` (HTTP 201 não significa salvo).