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.
- package/.cursor/rules/funifier.mdc +38 -41
- package/.github/copilot-instructions.md +38 -41
- package/AGENTS.md +56 -49
- package/README.md +40 -22
- package/datasource-funifier-docs/.coverage.json +326 -0
- package/datasource-funifier-docs/.validation.json +593 -0
- package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
- package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
- package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
- package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
- package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
- package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
- package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
- package/datasource-funifier-docs/knowledge/index.md +4 -1
- package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
- package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
- package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
- package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
- package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
- package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
- package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
- package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
- package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
- package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
- package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
- package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
- package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
- package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
- package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
- package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
- package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
- package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
- package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
- package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
- package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
- package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
- package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
- package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
- package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
- package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
- package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
- package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
- package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
- package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
- package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
- package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
- package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
- package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
- package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
- package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
- package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
- package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
- package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
- package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
- package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +42 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/init.test.js +74 -3
- package/dist/cli/init.test.js.map +1 -1
- package/dist/cli/persona.d.ts +3 -0
- package/dist/cli/persona.d.ts.map +1 -0
- package/dist/cli/persona.js +25 -0
- package/dist/cli/persona.js.map +1 -0
- package/dist/mcp/bundle.js +119 -93
- package/dist/mcp/check-update.d.ts +5 -0
- package/dist/mcp/check-update.d.ts.map +1 -1
- package/dist/mcp/check-update.js +21 -10
- package/dist/mcp/check-update.js.map +1 -1
- package/dist/mcp/check-update.test.d.ts +2 -0
- package/dist/mcp/check-update.test.d.ts.map +1 -0
- package/dist/mcp/check-update.test.js +33 -0
- package/dist/mcp/check-update.test.js.map +1 -0
- package/dist/mcp/index.js +2 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/prompts/templates.d.ts.map +1 -1
- package/dist/mcp/prompts/templates.js +35 -0
- package/dist/mcp/prompts/templates.js.map +1 -1
- package/dist/mcp/resources/documentation.d.ts +1 -1
- package/dist/mcp/resources/documentation.d.ts.map +1 -1
- package/dist/mcp/resources/documentation.js +39 -3
- package/dist/mcp/resources/documentation.js.map +1 -1
- package/dist/mcp/tools/connect.d.ts.map +1 -1
- package/dist/mcp/tools/connect.js +18 -8
- package/dist/mcp/tools/connect.js.map +1 -1
- package/dist/mcp/tools/database.d.ts.map +1 -1
- package/dist/mcp/tools/database.js +59 -47
- package/dist/mcp/tools/database.js.map +1 -1
- package/dist/mcp/tools/database.test.js +2 -2
- package/dist/mcp/tools/database.test.js.map +1 -1
- package/dist/mcp/tools/delete.d.ts.map +1 -1
- package/dist/mcp/tools/delete.js +13 -3
- package/dist/mcp/tools/delete.js.map +1 -1
- package/dist/mcp/tools/execute.d.ts.map +1 -1
- package/dist/mcp/tools/execute.js +20 -9
- package/dist/mcp/tools/execute.js.map +1 -1
- package/dist/mcp/tools/folder.d.ts.map +1 -1
- package/dist/mcp/tools/folder.js +22 -12
- package/dist/mcp/tools/folder.js.map +1 -1
- package/dist/mcp/tools/get.d.ts.map +1 -1
- package/dist/mcp/tools/get.js +16 -6
- package/dist/mcp/tools/get.js.map +1 -1
- package/dist/mcp/tools/index.d.ts +1 -1
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +28 -1
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/list.d.ts.map +1 -1
- package/dist/mcp/tools/list.js +38 -14
- package/dist/mcp/tools/list.js.map +1 -1
- package/dist/mcp/tools/logs.d.ts.map +1 -1
- package/dist/mcp/tools/logs.js +15 -5
- package/dist/mcp/tools/logs.js.map +1 -1
- package/dist/mcp/tools/save.d.ts.map +1 -1
- package/dist/mcp/tools/save.js +14 -4
- package/dist/mcp/tools/save.js.map +1 -1
- package/dist/mcp/tools/save.test.js +3 -3
- package/dist/mcp/tools/save.test.js.map +1 -1
- package/dist/mcp/tools/search-docs.d.ts +3 -0
- package/dist/mcp/tools/search-docs.d.ts.map +1 -0
- package/dist/mcp/tools/search-docs.js +102 -0
- package/dist/mcp/tools/search-docs.js.map +1 -0
- package/package.json +6 -2
- package/skills/acquire-funifier-knowledge/SKILL.md +155 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +86 -0
- package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
- package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
- package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
- package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
- package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
- package/skills/funifier/SKILL.md +88 -0
- package/skills/funifier/references/configure-security.md +96 -0
- package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
- package/skills/funifier/references/create-aggregate.md +144 -0
- package/skills/funifier/references/create-challenge.md +116 -0
- package/skills/funifier/references/create-competition.md +98 -0
- package/skills/funifier/references/create-crossword.md +574 -0
- package/skills/funifier/references/create-custom-object.md +91 -0
- package/skills/funifier/references/create-custom-page.md +135 -0
- package/skills/funifier/references/create-folder.md +104 -0
- package/skills/funifier/references/create-lastmile.md +643 -0
- package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
- package/skills/funifier/references/create-level.md +94 -0
- package/skills/funifier/references/create-lottery.md +913 -0
- package/skills/funifier/references/create-mystery.md +769 -0
- package/skills/funifier/references/create-notification.md +75 -0
- package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
- package/skills/funifier/references/create-quiz.md +98 -0
- package/skills/funifier/references/create-scheduler.md +141 -0
- package/skills/funifier/references/create-story.md +636 -0
- package/skills/funifier/references/create-swap.md +95 -0
- package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
- package/skills/funifier/references/create-virtual-good.md +96 -0
- package/skills/funifier/references/create-webhook.md +72 -0
- package/skills/funifier/references/create-websocket.md +71 -0
- package/skills/funifier/references/create-widget.md +76 -0
- package/skills/funifier/references/debug.md +87 -0
- package/skills/funifier/references/help.md +81 -0
- package/skills/funifier/references/implement-frontend.md +106 -0
- package/skills/funifier/references/import-csv.md +75 -0
- package/skills/funifier/references/manage-player.md +82 -0
- package/skills/funifier/references/manage-team.md +76 -0
- package/skills/funifier/references/upload-file.md +91 -0
- package/skills/funifier-create-aggregate/SKILL.md +0 -127
- package/skills/funifier-create-challenge/SKILL.md +0 -88
- package/skills/funifier-create-custom-page/SKILL.md +0 -127
- package/skills/funifier-create-level/SKILL.md +0 -87
- package/skills/funifier-create-quiz/SKILL.md +0 -87
- package/skills/funifier-create-scheduler/SKILL.md +0 -127
- package/skills/funifier-create-virtual-good/SKILL.md +0 -87
- package/skills/funifier-debug/SKILL.md +0 -92
- package/skills/funifier-help/SKILL.md +0 -86
- package/skills/funifier-implement-frontend/SKILL.md +0 -90
- package/skills/funifier-index/SKILL.md +0 -58
|
@@ -1,163 +1,608 @@
|
|
|
1
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
12
|
+
---
|
|
9
13
|
|
|
10
|
-
##
|
|
14
|
+
## 1. Visão Geral
|
|
11
15
|
|
|
12
|
-
|
|
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
|
-
|
|
18
|
+
Papel arquitetural:
|
|
18
19
|
|
|
19
|
-
- **
|
|
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
|
-
|
|
24
|
+
Problemas que resolve:
|
|
22
25
|
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
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
|
-
|
|
31
|
+
Relação com outros módulos:
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
###
|
|
57
|
-
**Método:** DELETE
|
|
58
|
-
**Endpoint:** `/v3/quiz/:id`
|
|
313
|
+
### `GET /v3/quiz/{id}` — buscar quiz
|
|
59
314
|
|
|
60
|
-
|
|
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
|
-
###
|
|
65
|
-
|
|
66
|
-
|
|
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": "
|
|
527
|
+
"quiz": "prova_historia",
|
|
72
528
|
"type": "MULTIPLE_CHOICE",
|
|
73
|
-
"
|
|
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
|
-
"
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
"
|
|
96
|
-
"
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
##
|
|
598
|
+
## Checklist de Configuração
|
|
139
599
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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.
|