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,111 +1,1045 @@
1
- # Folder (Pasta)
1
+ # `folder`
2
2
 
3
3
  **Acesso Studio:** `/studio/folder`
4
4
  **API Endpoint:** `/v3/folder`
5
+ **Coleções MongoDB:** `folder`, `folder_content`, `folder_content_type`, `folder_log`
5
6
 
6
- ## O que é
7
+ ---
7
8
 
8
- Organização de conteúdos e monitoramento do progresso em cursos ou trilhas. Permite criar pastas virtuais para agrupar conteúdos como quizzes, desafios, materiais de estudo e jogos, facilitando o acompanhamento do progresso dos jogadores em jornadas de aprendizagem ou trilhas gamificadas.
9
+ ## 1. Visão Geral
9
10
 
10
- ## Quando usar
11
+ O módulo `folder` implementa um sistema de trilhas hierárquicas (cursos, módulos, aulas, conteúdos). Cinco artefatos formam o módulo:
11
12
 
12
- - Para estruturar cursos online
13
- - Para criar trilhas de capacitação
14
- - Para acompanhar progresso em treinamentos
15
- - Para organizar conteúdos em hierarquias (módulos aulas conteúdos)
13
+ - `folder` da árvore (pasta).
14
+ - `folder_content` vínculo entre uma pasta e um item de conteúdo (referência a documento em outra coleção, ou item próprio em coleção de formulário).
15
+ - `folder_content_type` schema do tipo de conteúdo (define a coleção real onde os dados ficam e se é `repository` ou `formulary`).
16
+ - `folder_log` registro de progresso do jogador em um `folder_content`.
17
+ - `Inside` — DTO transitório calculado em runtime (árvore + progresso); **não persiste em MongoDB**.
16
18
 
17
- ## Checklist de Configuração no Studio
19
+ O módulo resolve três problemas:
18
20
 
19
- - [ ] Criar estrutura hierárquica de pastas
20
- - [ ] Definir títulos das pastas
21
- - [ ] Associar conteúdos às pastas (quizzes, desafios, textos)
22
- - [ ] Definir tipos de conteúdo (folder_content_type)
23
- - [ ] Configurar pasta-pai (parent) para hierarquia
21
+ 1. **Organização hierárquica arbitrária** pastas aninhadas sem limite de profundidade, com ordem (`position`) e visibilidade (`active`) por pasta.
22
+ 2. **Vinculação de conteúdo** duas estratégias: `repository` (a pasta aponta para um item já existente em outra coleção) ou `formulary` (o item é criado e mantido junto ao vínculo, em uma coleção definida pelo `ContentType`).
23
+ 3. **Rastreamento de progresso por jogador** log granular por item (`folder_log`) e agregação recursiva por pasta (`Inside.percent/done/total`), com disparo de triggers `folder_log` e `folder_progress` por nível do breadcrumb.
24
24
 
25
- ## API Endpoints
25
+ **Limitações estruturais confirmadas no código:**
26
26
 
27
- ### Listar Pastas
28
- **Método:** GET
29
- **Endpoint:** `/v3/database/folder`
27
+ - Não há DAO separado: toda persistência é feita pelo próprio `FolderManager` via Jongo.
28
+ - Não há scheduler nem job de background — todo o processamento é síncrono dentro da requisição HTTP.
29
+ - O recurso `folder_content_type` **não tem endpoints próprios** em `FolderRest`. Sua manipulação é feita pela API genérica `/v3/database/folder_content_type`.
30
30
 
31
- ### Criar Pasta
32
- **Método:** POST
33
- **Endpoint:** `/v3/folder`
31
+ ---
32
+
33
+ ## 2. Arquitetura e Fluxos
34
+
35
+ ### 2.1 Componentes
36
+
37
+ | Camada | Classe | Responsabilidade |
38
+ |---|---|---|
39
+ | REST | `FolderRest` (`src/main/java/com/funifier/rest/v3/rest/FolderRest.java`) | Endpoints HTTP, auth via `AuthBean`, dispatch para `FolderManager`. |
40
+ | Domínio | `FolderManager` (`src/main/java/com/funifier/engine/folder/FolderManager.java`) | Toda a lógica de negócio, persistência e disparo de triggers. |
41
+ | Modelos | `Folder`, `Content`, `ContentType`, `FolderLog`, `UnlockPolicy`, `Inside` | POJOs serializados via Jackson (`@JsonIgnoreProperties(ignoreUnknown=true)`). |
42
+ | Integração | `ManagerFactory.getTriggerManager()` | Disparo dos triggers `folder_log` e `folder_progress`. |
43
+ | Infra | `ConnectionPool.createIndexes()` | Cria índices MongoDB no startup. |
44
+
45
+ Acesso: `FrontController.getInstance(authBean.getApiKey()).getManagerFactory().getFolderManager()`. Cada `apiKey` mapeia para uma instância isolada de `ManagerFactory` (isolamento por tenant).
46
+
47
+ ### 2.2 Pipeline `insertFolder` (`POST /v3/folder`)
48
+
49
+ `FolderManager.insertFolder(Folder folder)` — linhas 31–39:
50
+
51
+ 1. Se `folder == null` → retorna sem fazer nada.
52
+ 2. Obtém `MongoCollection` da coleção `folder`.
53
+ 3. Se `_id` for `null` ou só whitespace → gera `Guid.shortTimeMillis()`.
54
+ 4. Se `position == null` → `position = 0`.
55
+ 5. Se `active == null` → `active = true`.
56
+ 6. `c.save(folder)` — Jongo faz upsert por `_id`.
57
+
58
+ > Sem validação de campos obrigatórios. Sem disparo de trigger. Sem checagem de existência do `parent`. O endpoint serve tanto para criar quanto para atualizar (full-replace por `_id`).
59
+
60
+ ### 2.3 Pipeline `insertLog` (`POST /v3/folder/log`) — critical path
61
+
62
+ `FolderManager.insertLog(FolderLog log)` — linhas 556–646:
63
+
64
+ 1. Se `log == null` → retorna sem fazer nada.
65
+ 2. Busca `Content content = folder_content.findOne({_id: log.item})`.
66
+ 3. **Se `content == null` → retorna silenciosamente** (sem erro, sem save). O endpoint ainda devolve HTTP 201 com o `log` original.
67
+ 4. Se `log._id` for `null` ou whitespace → gera `Guid.newShortGuid()`.
68
+ 5. Procura log ativo existente: `folder_log.findOne({player: log.player, item: log.item, finished: {$exists: false}})`.
69
+ - Se **existe** → `log._id = current._id`; `log.started = current.started`.
70
+ - Se **não existe** → `log.started = now`.
71
+ 6. Normalização status/percent (linhas 574–584):
72
+ - `status == "done"` → `percent = 100.0`.
73
+ - `percent != null && percent >= 100` → `status = "done"`; `percent = 100.0`.
74
+ - `status == "done" && finished == null` → `finished = now`.
75
+ 7. Se `started == null` ainda assim → `started = now` (fallback de segurança).
76
+ 8. **Cálculo de diff** (linha 594):
77
+
78
+ ```java
79
+ double diff = (current == null) ? log.percent
80
+ : (current != null) ? log.percent - current.percent
81
+ : 0;
82
+ ```
83
+
84
+ - Auto-unboxing de `Double` para `double`. Se `log.percent == null` ou `current.percent == null`, lança `NullPointerException` **antes** de salvar (ver Seção 7).
85
+ 9. Se `diff > 0`:
86
+ - `path = breadcrumbFolder(content.parent)` (lista do raiz até o pai do content).
87
+ - `before = playerProgress(path.get(0)._id, log.player)` — snapshot do progresso da raiz **antes** do save.
88
+ 10. `triggerManager.execute(log._id, log, "folder_log", BEFORE_CREATE, log.player, null)`.
89
+ 11. `c.save(log)` — upsert do log.
90
+ 12. `triggerManager.execute(log._id, log, "folder_log", AFTER_CREATE, log.player, null)`.
91
+ 13. Se `diff > 0` (mesma condição de antes):
92
+ - `after = playerProgress(path.get(0)._id, log.player)` — snapshot **depois** do save.
93
+ - `Collections.reverse(path)` — inverte para `[content.parent, ..., raiz]`.
94
+ - Para cada `folder` no path: compara `folderAfter.percent` com `folderBefore.percent`. Se aumentou, dispara `triggerManager.execute(folder._id, payloadProgress, "folder_progress", AFTER_CREATE, log.player, null)`.
95
+
96
+ **Payload do trigger `folder_progress`:**
34
97
 
35
- **Exemplo de Body:**
36
98
  ```json
37
99
  {
38
- "title": "CoreDrives",
39
- "parent": "D0MmmNf"
100
+ "_id": "<folderId>",
101
+ "player": "<playerId>",
102
+ "folder": { /* Folder completo */ },
103
+ "progress": { /* Inside com percent/done/total */ },
104
+ "previous_percent": 60.0,
105
+ "current_percent": 75.0,
106
+ "time": "<Date now>"
40
107
  }
41
108
  ```
42
109
 
43
- ### Deletar Pasta
44
- **Método:** DELETE
45
- **Endpoint:** `/v3/folder/:id`
110
+ ### 2.4 Pipeline `playerProgress` (`POST /v3/folder/progress`)
46
111
 
47
- ### Obter Breadcrumb
48
- **Método:** POST
49
- **Endpoint:** `/v3/folder/breadcrumb`
112
+ `FolderManager.playerProgress(folder, player)` — linhas 132–149:
50
113
 
51
- **Exemplo de Body:**
52
- ```json
53
- {
54
- "folder": "CD1"
55
- }
114
+ 1. `Folder current = folder.findOne({_id: folder})`.
115
+ 2. Se `current == null` → retorna `null`.
116
+ 3. `Inside inside = insideFolder(folder)` — monta árvore estrutural (sem progresso).
117
+ 4. `inside.player = player`.
118
+ 5. `playerProgressCascade(inside, player, root=inside)` — popula `total`, `done`, `percent`, `time`, `is_unlocked` em toda a árvore.
119
+ 6. Se `inside.time == null` → `inside.time = getMaxTimeInside(inside, null)` (fallback com bugs latentes — ver Seção 7).
120
+ 7. Retorna `Inside`.
121
+
122
+ Após o `FolderManager.playerProgress`, **`FolderRest.progress` (linhas 119–129) sempre dispara o trigger `folder_progress`** para o folder consultado, mesmo sem mudança de percentual:
123
+
124
+ ```java
125
+ manager.getTriggerManager().execute(folder, progress, "folder_progress", Trigger.EVENT_AFTER_CREATE, player, null);
56
126
  ```
57
127
 
58
- ### Listar Tipos de Conteúdo
59
- **Método:** GET
60
- **Endpoint:** `/v3/database/folder_content_type`
128
+ Neste caminho `previous_percent == current_percent == inside.percent` — não há "antes/depois" no endpoint de leitura.
61
129
 
62
- ### Criar/Atualizar Tipo de Conteúdo
63
- **Método:** PUT
64
- **Endpoint:** `/v3/database/folder_content_type`
130
+ ### 2.5 Pipeline `playerProgressCascade` (recursivo)
65
131
 
66
- **Exemplo de Body:**
67
- ```json
68
- {
69
- "_id": "text",
70
- "input": "formulary",
71
- "form": [
72
- { "name": "title", "type": "string", "title": "Title" },
73
- { "name": "content", "type": "text", "title": "Content" }
74
- ],
75
- "title": "Text",
76
- "entity": "text__c"
77
- }
132
+ `FolderManager.playerProgressCascade(inside, player, root)` — linhas 151–234:
133
+
134
+ ```
135
+ contentIds = inside.getContentIds(cascade=true) // todos os ids de content na sub-árvore
136
+ inside.total = contentIds.size()
137
+ inside.done = 0
138
+ inside.percent = 0.0
139
+
140
+ se contentIds não vazio:
141
+ aggregate folder_log:
142
+ {$match:{ item:{$in:contentIds}, player:#, status:"done" }}
143
+ {$group:{ _id:"$item", time:{$max:"$finished"} }}
144
+ {$group:{ _id:null, done:{$sum:1}, time:{$max:"$time"} }}
145
+ → inside.done, inside.percent = (done/total)*100, inside.time
146
+
147
+ para cada item m em inside.items:
148
+ se m.folder == false (content):
149
+ log = folder_log.findOne({item:m._id, player:#, status:"done"})
150
+ se log:
151
+ m.percent = 100.0; m.time = log.finished
152
+ senão:
153
+ log2 = folder_log.findOne({item:m._id, player:#})
154
+ m.percent = (log2 != null && log2.percent != null) ? log2.percent : 0.0
155
+ senão se m.folder == true:
156
+ playerProgressCascade(m, player, root) // recursão
157
+
158
+ # pós-processamento: is_unlocked apenas para folders
159
+ onlyFolders = filtra items onde folder == true
160
+ para cada child em onlyFolders (com índice i):
161
+ policy = resolveUnlockPolicy(child._id)
162
+ se policy == null OR policy.type != "progress":
163
+ child.is_unlocked = true
164
+ continue
165
+ ref = policy.folder_ref ?: "prev"
166
+ min = policy.min_percent ?: 0
167
+ se ref == "prev":
168
+ se i == 0 → child.is_unlocked = true
169
+ senão → child.is_unlocked = (onlyFolders[i-1].percent ?: 0) >= min
170
+ senão (ref é folderId):
171
+ target = root.getInsideById(ref, cascade=true)
172
+ se target == null → child.is_unlocked = true
173
+ senão → child.is_unlocked = (target.percent ?: 0) >= min
174
+ ```
175
+
176
+ ### 2.6 Pipeline `deleteFolderCascade` (recursivo)
177
+
178
+ `FolderManager.deleteFolderCascade(id)` — linhas 324–364:
179
+
180
+ 1. Se `id` nulo/whitespace → retorna.
181
+ 2. `folder.remove({_id: id})`.
182
+ 3. Para cada `Content` em `folder_content.find({parent: id})`:
183
+ - Resolve `ContentType` por `content.type`.
184
+ - Se `type != null && type.input == "formulary"` → `db[type.entity].remove({_id: content.content})` (apaga o dado real).
185
+ - `folder_log.remove({item: content._id})`.
186
+ 4. `folder_content.remove({parent: id})` (após o loop).
187
+ 5. Para cada `Folder` em `folder.find({parent: id})` → recursão `deleteFolderCascade(child._id)`.
188
+
189
+ > Sem transação. Sem trigger. Sem checagem de `active` — sub-folders inativos também caem na cascata.
190
+
191
+ ### 2.7 Diagrama — fluxo de `insertLog`
192
+
193
+ ```mermaid
194
+ flowchart LR
195
+ A[POST /v3/folder/log] --> B{content existe?}
196
+ B -- Não --> Z[retorno silencioso<br/>HTTP 201 sem save]
197
+ B -- Sim --> C{log ativo<br/>player+item+!finished?}
198
+ C -- Sim --> D[reutiliza _id e started]
199
+ C -- Não --> E[started = now]
200
+ D --> F[normaliza<br/>status/percent]
201
+ E --> F
202
+ F --> G[diff = log.percent - currentPercent]
203
+ G --> H{diff > 0?}
204
+ H -- Sim --> I[breadcrumb +<br/>playerProgress before]
205
+ H -- Não --> J[BEFORE_CREATE folder_log]
206
+ I --> J
207
+ J --> K[c.save log]
208
+ K --> L[AFTER_CREATE folder_log]
209
+ L --> M{diff > 0?}
210
+ M -- Não --> END[fim]
211
+ M -- Sim --> N[playerProgress after<br/>+ reverse path]
212
+ N --> O[para cada folder no path:<br/>se after.percent &gt; before.percent<br/>→ folder_progress AFTER_CREATE]
213
+ O --> END
214
+ ```
215
+
216
+ ### 2.8 Diagrama — interação entre módulos no `insertLog`
217
+
218
+ ```mermaid
219
+ sequenceDiagram
220
+ participant C as Cliente
221
+ participant FR as FolderRest
222
+ participant FM as FolderManager
223
+ participant DB as MongoDB
224
+ participant TM as TriggerManager
225
+
226
+ C->>FR: POST /v3/folder/log
227
+ FR->>FM: insertLog(log)
228
+ FM->>DB: findOne folder_content {_id: item}
229
+ alt content não existe
230
+ FM-->>FR: retorno silencioso
231
+ FR-->>C: HTTP 201 (sem save)
232
+ else content existe
233
+ FM->>DB: findOne folder_log {player, item, !finished}
234
+ FM->>FM: normaliza status/percent
235
+ opt diff > 0
236
+ FM->>FM: breadcrumbFolder(content.parent)
237
+ FM->>FM: playerProgress(root, player) → before
238
+ end
239
+ FM->>TM: execute folder_log BEFORE_CREATE
240
+ FM->>DB: save(log)
241
+ FM->>TM: execute folder_log AFTER_CREATE
242
+ opt diff > 0
243
+ FM->>FM: playerProgress(root, player) → after
244
+ loop cada folder no path (do mais próximo ao raiz)
245
+ FM->>TM: execute folder_progress AFTER_CREATE
246
+ end
247
+ end
248
+ FM-->>FR: retorno
249
+ FR-->>C: HTTP 201 + log JSON
250
+ end
251
+ ```
252
+
253
+ ### 2.9 Diagrama — ciclo de vida de `FolderLog`
254
+
255
+ ```mermaid
256
+ stateDiagram-v2
257
+ [*] --> Iniciado: insertLog (content existe)<br/>started = now
258
+ Iniciado --> EmAndamento: insertLog com percent &lt; 100<br/>(reutiliza _id; finished não setado)
259
+ EmAndamento --> EmAndamento: insertLog com percent &lt; 100
260
+ Iniciado --> Concluido: insertLog com status="done"<br/>ou percent ≥ 100
261
+ EmAndamento --> Concluido: insertLog com status="done"<br/>ou percent ≥ 100
262
+ Concluido --> [*]: deleteLog<br/>(sem recálculo de progresso pai)
263
+ Iniciado --> [*]: deleteLog
264
+ EmAndamento --> [*]: deleteLog
265
+
266
+ note right of Iniciado: percent pode ser null<br/>finished == null<br/>idempotência ativa
267
+ note right of Concluido: percent = 100.0<br/>finished = now<br/>idempotência se quebra<br/>(novo log começa do zero)
268
+ ```
269
+
270
+ > **Idempotência:** "ativa" enquanto `finished` é nulo. Após `status="done"`, novos `insertLog` para o mesmo `(player, item)` criam um log paralelo (a query `{finished: {$exists:false}}` deixa de bater).
271
+
272
+ ---
273
+
274
+ ## 3. Estrutura dos Objetos
275
+
276
+ ### 3.1 `Folder` — coleção `folder`
277
+
278
+ Definido em `src/main/java/com/funifier/engine/folder/Folder.java`.
279
+
280
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
281
+ |---|---|---|---|---|
282
+ | `_id` | `String` | `Guid.shortTimeMillis()` | — | Auto-gerado se nulo/whitespace no insert. |
283
+ | `type` | `String` | — | Não | Categoria livre (ex: `"course"`, `"module"`, `"lesson"`). Usado também como `Inside.type` no retorno. |
284
+ | `parent` | `String` | — | Não | `_id` do folder pai. Ausência ou string vazia = nó raiz. |
285
+ | `title` | `String` | — | Não (sem validação) | Título exibível. |
286
+ | `position` | `Integer` (wrapper) | `0` no insert | — | Ordenação entre irmãos. Default `0` se nulo no insert. |
287
+ | `active` | `Boolean` (wrapper) | `true` no insert | — | Default `true` se nulo. `false` esconde da árvore (`insideFolder`/`moveFolder`); **não bloqueia `deleteFolderCascade`**. |
288
+ | `unlock_policy` | `UnlockPolicy` | — | Não | Política de desbloqueio condicional (ver 3.2). Herdada via `parent` quando ausente. |
289
+ | `extra` | `Map<String,Object>` | `new HashMap<>()` | Não | Campos livres. |
290
+
291
+ **Campos desconhecidos no JSON de entrada são silenciosamente descartados** (`@JsonIgnoreProperties(ignoreUnknown=true)`). Não existem campos deprecated ou comentados nesta classe.
292
+
293
+ **Comportamento de `active` por operação:**
294
+
295
+ | Operação | Considera `active`? |
296
+ |---|---|
297
+ | `insideFolder` / `insideFolderCascade` | `{$or:[{active:true},{active:{$exists:false}}]}` — esconde apenas `active:false` explícito. |
298
+ | `moveFolder` (irmãos) | Mesmo filtro acima — `active:false` não é renumerado nem movido. |
299
+ | `deleteFolderCascade` | **Ignora** `active`. Sub-folders inativos também são removidos. |
300
+ | `breadcrumbFolder` | Ignora — busca puramente por `_id`/`parent`. |
301
+ | `playerProgress` | Usa `insideFolder`, portanto herda o filtro. |
302
+
303
+ ### 3.2 `UnlockPolicy` — embutido em `Folder.unlock_policy`
304
+
305
+ Definido em `src/main/java/com/funifier/engine/folder/UnlockPolicy.java`.
306
+
307
+ | Campo | Tipo | Valores | Descrição |
308
+ |---|---|---|---|
309
+ | `type` | `String` | apenas `"progress"` é interpretado | Qualquer outro valor (ou `null`) → `is_unlocked = true`. |
310
+ | `folder_ref` | `String` | `"prev"` ou ID de folder | `"prev"` = folder irmão anterior na lista ordenada. Outro valor é tratado como ID de folder e procurado em toda a árvore via `root.getInsideById(ref, cascade=true)`. |
311
+ | `min_percent` | `Integer` (wrapper) | `0`–`100` | Percentual mínimo do alvo. Default `0` se nulo. Comparação é `target.percent >= min_percent` (inclusivo). |
312
+
313
+ **Resolução via `resolveUnlockPolicy(folderId)`** — linhas 236–248:
314
+
315
+ ```
316
+ f = folder.findOne({_id: folderId})
317
+ enquanto f != null:
318
+ se f.unlock_policy != null → return f.unlock_policy
319
+ se f.parent é nulo/whitespace → break
320
+ f = folder.findOne({_id: f.parent})
321
+ return null
322
+ ```
323
+
324
+ Sobe a árvore até achar o primeiro ancestral com política. Sem política em nenhum ancestral → `is_unlocked = true`.
325
+
326
+ ### 3.3 `Content` — coleção `folder_content`
327
+
328
+ Definido em `src/main/java/com/funifier/engine/folder/Content.java`.
329
+
330
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
331
+ |---|---|---|---|---|
332
+ | `_id` | `String` | `Guid.shortTimeMillis()` | — | Auto-gerado se nulo/whitespace no insert. |
333
+ | `type` | `String` | — | Sim (runtime) | `_id` do `ContentType`. Sem isso, `findContentData` retorna `null` e `title` automático não funciona. |
334
+ | `content` | `String` | — | Condicional | `_id` do item real em `ContentType.entity`. Obrigatório quando `input=repository`. Em `formulary` aponta para o documento criado por outro fluxo. |
335
+ | `parent` | `String` | — | Sim (runtime) | `_id` do `Folder` pai. Sem validação de existência. |
336
+ | `title` | `String` | — | Não | Calculado automaticamente em `insertContent` quando nulo, OU sempre quando `ContentType.input == "formulary"`. |
337
+ | `extra` | `Map<String,Object>` | `new HashMap<>()` | Não | Campos livres. |
338
+ | `position` | `int` (primitivo) | `0` | — | Sempre presente (primitivo). Inválido enviar `null`. |
339
+
340
+ > **Diferença em relação a `Folder`:** `Content` **não tem campo `active`** — não pode ser escondido sem ser deletado. `moveContent` consequentemente **não filtra por `active`** (e nem poderia).
341
+
342
+ **Auto-cálculo de `title` em `insertContent`** (linhas 399–415):
343
+
344
+ ```
345
+ ContentType ct = folder_content_type.findOne({_id: content.type})
346
+ se content.title == null OU (ct != null && ct.input == "formulary"):
347
+ raw = findContentData(content, jongo) // busca em ct.entity
348
+ se raw != null:
349
+ title = raw["title"] ?: raw["name"] ?: String.valueOf(raw["_id"])
350
+ content.title = title
78
351
  ```
79
352
 
80
- ## Casos de Uso Comuns
353
+ > Em `formulary`, o título do `Content` é **sempre sobrescrito** pelo título do item real — qualquer `title` enviado pelo cliente é descartado.
81
354
 
82
- - **Curso Online:** Pasta raiz "Curso de Vendas" → Subpastas "Módulo 1", "Módulo 2" → Conteúdos (textos, vídeos, quizzes)
83
- - **Trilha de Capacitação:** Pastas representam etapas; cada etapa contém materiais e avaliações
84
- - **Catálogo de Produtos:** Pastas como categorias; conteúdos como fichas de produto
85
- - **Onboarding:** Pastas representam fases do onboarding; progresso monitora completude
355
+ ### 3.4 `ContentType` coleção `folder_content_type`
86
356
 
87
- ## Progresso do Jogador
357
+ Definido em `src/main/java/com/funifier/engine/folder/ContentType.java`.
88
358
 
89
- O Folder monitora automaticamente o progresso do jogador dentro da árvore de diretórios. Isso permite:
359
+ | Campo | Tipo | Descrição |
360
+ |---|---|---|
361
+ | `_id` | `String` | Identificador (ex: `"video"`, `"quiz"`). |
362
+ | `entity` | `String` | Nome da coleção MongoDB que armazena o item real (ex: `"video__c"`). |
363
+ | `title` | `String` | Label de exibição (não é usado nas decisões internas). |
364
+ | `image` | `String` | URL de ícone. |
365
+ | `input` | `String` | `"repository"` ou `"formulary"`. Apenas essas duas constantes são interpretadas pelo código. |
366
+ | `slug` | `String` | Slug para URL. |
367
+ | `attributes` | `List<HashMap>` | Definição do formulário para `input=formulary`. Não é validada pelo backend de folder. |
90
368
 
91
- - Saber que um jogador completou X% de um curso
92
- - Registrar action logs ao progredir (aciona técnicas de jogos)
93
- - Criar desafios vinculados à completude de um folder (ex: "Complete o curso para ganhar 500 XP")
369
+ **Constantes no código:**
370
+
371
+ ```java
372
+ ContentType.INPUT_REPOSITORY = "repository"
373
+ ContentType.INPUT_FORMULARY = "formulary"
374
+ ```
375
+
376
+ **Impacto no delete:** quando `input == "formulary"`, `deleteContent`/`deleteFolderCascade` removem **também** o documento real em `ContentType.entity` (`{_id: content.content}`). Em `repository`, apenas o vínculo é removido.
377
+
378
+ > **Não há endpoints REST para `ContentType` em `FolderRest`.** Use a API genérica `/v3/database/folder_content_type` para CRUD.
379
+
380
+ ### 3.5 `FolderLog` — coleção `folder_log`
381
+
382
+ Definido em `src/main/java/com/funifier/engine/folder/FolderLog.java`.
383
+
384
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
385
+ |---|---|---|---|---|
386
+ | `_id` | `String` | `Guid.newShortGuid()` | — | Auto-gerado se nulo/whitespace. Sobrescrito pelo `_id` do log ativo existente quando há idempotência. |
387
+ | `item` | `String` | — | Sim | `_id` do `Content`. Sem isso, `insertLog` aborta silenciosamente. |
388
+ | `player` | `String` | — | Sim | ID do jogador. |
389
+ | `status` | `String` | — | Não | Única constante interpretada: `STATUS_DONE = "done"`. Outros valores são preservados mas ignorados pela lógica. |
390
+ | `started` | `Date` | `now` no insert | — | Preenchido automaticamente; preservado de log ativo existente. |
391
+ | `finished` | `Date` | `now` quando `status="done"` | — | Preenchido apenas quando o log fica `done`. Logs em andamento têm `finished == null` — esta é a chave operacional de idempotência. |
392
+ | `percent` | `Double` (wrapper) | — | Não | `0.0`–`100.0`. **Pode ser `null`** — gatilho de NPE em `insertLog` (Seção 7). |
393
+ | `extra` | `HashMap<String,Object>` | — | Não | Campos livres. Não é inicializado (pode ser `null`). |
394
+
395
+ **Constantes:**
396
+
397
+ ```java
398
+ FolderLog.STATUS_DONE = "done"
399
+ ```
400
+
401
+ **Normalização aplicada pelo servidor em `insertLog`:**
402
+
403
+ | Entrada | Resultado |
404
+ |---|---|
405
+ | `status == "done"` | `percent = 100.0` (qualquer valor enviado é sobrescrito) |
406
+ | `percent != null && percent >= 100` | `status = "done"`, `percent = 100.0` |
407
+ | `status == "done" && finished == null` | `finished = now` |
408
+ | `started == null` no final do pipeline | `started = now` |
409
+
410
+ **Chave operacional de idempotência:** `(player, item, finished:{$exists:false})`. Existe no máximo um log "ativo" por par `(player, item)`. Após `status="done"` (i.e., `finished` setado), o próximo `insertLog` criará um log novo.
411
+
412
+ ### 3.6 `Inside` — DTO de resposta (não persistido)
413
+
414
+ Definido em `src/main/java/com/funifier/engine/folder/Inside.java`.
415
+
416
+ | Campo | Tipo | Descrição |
417
+ |---|---|---|
418
+ | `_id` | `String` | `_id` do nó (folder ou content). |
419
+ | `folder` | `boolean` (primitivo) | `true` = pasta; `false` = conteúdo. |
420
+ | `title` | `String` | Vem de `Folder.title` ou `Content.title`. |
421
+ | `type` | `String` | Vem de `Folder.type` ou `Content.type` (ContentType id no caso de conteúdo). |
422
+ | `content` | `String` | Apenas em conteúdos — copia `Content.content` (id do item real). |
423
+ | `items` | `List<Inside>` | Filhos ordenados: **folders primeiro** (todos), **depois conteúdos** (todos). Cada grupo ordenado por `position`. |
424
+ | `player` | `String` | Setado apenas no `playerProgress`. |
425
+ | `total` | `Integer` | Total de conteúdos na sub-árvore (contagem recursiva). |
426
+ | `done` | `Integer` | Conteúdos com pelo menos um `folder_log` `status="done"` para o player. |
427
+ | `percent` | `Double` | Folder: `(done/total)*100`. Content: `100.0` se há log `done`, senão `log.percent ?: 0.0`. |
428
+ | `time` | `Date` | Maior `finished` entre os logs `done` da sub-árvore. Em folders sem nenhum log `done`, cai no fallback `getMaxTimeInside` (com bugs — ver Seção 7). |
429
+ | `is_unlocked` | `Boolean` | `true`/`false` para folders; **`null` para conteúdos**. |
430
+
431
+ **Métodos utilitários internos (package-private):**
432
+
433
+ - `getContentIds(boolean cascade)` — coleta `_id` de conteúdos descendentes; se `cascade=false`, apenas os conteúdos diretos do `inside`.
434
+ - `getInsideById(String id, boolean cascade)` — busca o próprio `Inside` ou um filho/descendente por `_id`. Retorna o **primeiro match** (DFS, conteúdo antes de descer em folder filho que casa pelo id — ver código linhas 70–90).
435
+
436
+ ---
437
+
438
+ ## 4. Endpoints
439
+
440
+ Todos os endpoints exigem `Bearer token` (via `AuthBean`). O `apiKey` extraído do token resolve o `FrontController` (tenant) e o `ManagerFactory` correspondente.
441
+
442
+ ### 4.1 `POST /v3/folder`
443
+
444
+ | Aspecto | Detalhe |
445
+ |---|---|
446
+ | Finalidade | Criar ou atualizar pasta (upsert por `_id`). |
447
+ | Body | `Folder` JSON. |
448
+ | Comportamento | Full replace via `MongoCollection.save()`. Defaults aplicados quando `position` ou `active` forem `null`. |
449
+ | Status | `201 Created`. |
450
+ | Triggers | Nenhum. |
451
+
452
+ **Exemplo:**
453
+
454
+ ```http
455
+ POST /v3/folder
456
+ Authorization: Bearer <token>
457
+ Content-Type: application/json
458
+
459
+ { "title": "Módulo 1 — Introdução", "parent": "trail001", "position": 0 }
460
+ ```
461
+
462
+ ```http
463
+ HTTP/1.1 201 Created
464
+ { "_id": "ABC123xyz", "title": "Módulo 1 — Introdução", "parent": "trail001", "position": 0, "active": true }
465
+ ```
466
+
467
+ ### 4.2 `GET /v3/folder/{id}`
468
+
469
+ | Aspecto | Detalhe |
470
+ |---|---|
471
+ | Finalidade | Buscar pasta por `_id`. |
472
+ | Resposta | `200 OK` com `Folder` ou body vazio se `findOne` retornar `null`. |
473
+
474
+ ### 4.3 `GET /v3/folder/{id}/move?direction=up|down`
475
+
476
+ | Aspecto | Detalhe |
477
+ |---|---|
478
+ | Finalidade | Mover folder uma posição para cima/baixo entre seus irmãos `active=true`. |
479
+ | Status | `204 No Content`. |
480
+
481
+ **Algoritmo (`moveFolder` linhas 251–295):**
482
+
483
+ 1. `findOne({_id: id})` — se `null`, no-op.
484
+ 2. Lista todos os irmãos `{parent: o.parent, active:true|inexistente}` ordenados por `position`.
485
+ 3. **Renumera todos os irmãos** sequencialmente `0, 1, 2…` e salva cada um (mesmo os que não mudam).
486
+ 4. Localiza índice do alvo; identifica `antes` (`index-1`) e `depois` (`index+1`).
487
+ 5. Se `direction == "up"` e `index > 0`: troca `position` do alvo com o de `antes`, salva ambos.
488
+ 6. Se `direction == "down"` e `index < size-1`: troca com `depois`.
489
+ 7. Qualquer outro valor de `direction` (ou `null`): apenas a renumeração (passo 3) é aplicada.
490
+
491
+ > Efeito colateral: o endpoint **sempre re-grava todos os irmãos** mesmo quando o alvo já está nas pontas. Não é idempotente em termos de I/O.
492
+
493
+ ### 4.4 `DELETE /v3/folder/{id}`
494
+
495
+ | Aspecto | Detalhe |
496
+ |---|---|
497
+ | Finalidade | Deletar pasta com cascata total (ver 2.6). |
498
+ | Status | `204 No Content`. |
499
+ | Triggers | Nenhum em `folder` / `folder_content`; **`folder_log` também não** (cascata deleta logs por `remove`, sem passar pelo `deleteLog`). |
500
+
501
+ > Irreversível. Sem transação — uma falha no meio deixa estado inconsistente.
502
+
503
+ ### 4.5 `DELETE /v3/folder?q=<query>`
504
+
505
+ `FolderRest.deleteAll` linhas 71–80. Executa:
506
+
507
+ ```java
508
+ distinct("_id").query("{" + q + "}")
509
+ ```
510
+
511
+ …concatenando o parâmetro `q` diretamente. Depois itera os IDs e chama `deleteFolder` para cada. Risco de injeção MongoDB descrito na Seção 8.
512
+
513
+ ### 4.6 `POST /v3/folder/inside`
514
+
515
+ | Aspecto | Detalhe |
516
+ |---|---|
517
+ | Finalidade | Retornar árvore estrutural (sem progresso). |
518
+ | Body | `{ "folder": "<id ou vazio>" }` |
519
+ | Resposta | `Inside` recursivo. |
520
+
521
+ **Comportamento por valor de `folder`:**
522
+
523
+ | Input | Comportamento |
524
+ |---|---|
525
+ | ID existente | Retorna `Inside` cuja raiz corresponde ao folder; `items` contém sub-folders (filtrados por `active`) e contents. |
526
+ | ID inexistente (não-vazio) | Retorna `Inside` raiz "vazio" (apenas `folder=true`, `items=[]`) — a query por `parent: <id>` não acha nada. |
527
+ | `null` ou `""` | Retorna **todos os folders raiz** (`{parent: {$exists: false}}`) **e** os `folder_content` raiz (registros de `folder_content` sem `parent`, se existirem). |
528
+
529
+ **Ordem dos `items`:** sempre `[folder_0, folder_1, …, content_0, content_1, …]`. Pastas e conteúdos **não são intercalados** por `position`.
530
+
531
+ ### 4.7 `POST /v3/folder/breadcrumb`
532
+
533
+ | Aspecto | Detalhe |
534
+ |---|---|
535
+ | Finalidade | Retornar caminho do raiz até a pasta informada. |
536
+ | Body | `{ "folder": "<id>" }` |
537
+ | Resposta | `List<Folder>` ordenada `[raiz, ..., alvo]`. |
538
+
539
+ **Algoritmo (`breadcrumbFolder` linhas 46–57):**
540
+
541
+ ```
542
+ list = []
543
+ f = folder.findOne({_id: id})
544
+ list.add(f)
545
+ enquanto f.parent != null && !f.parent.trim().isEmpty():
546
+ f = folder.findOne({_id: f.parent})
547
+ list.add(f)
548
+ Collections.reverse(list)
549
+ return list
550
+ ```
551
+
552
+ > **NPE garantida** se `id` não existir (`f` será `null`, e `f.parent` rompe na primeira iteração). Mesmo problema se algum `parent` no caminho não existir no banco.
553
+
554
+ ### 4.8 `POST /v3/folder/progress`
555
+
556
+ | Aspecto | Detalhe |
557
+ |---|---|
558
+ | Finalidade | Calcular o progresso do jogador na árvore + disparar trigger. |
559
+ | Body | `{ "folder": "<id>", "player": "<playerId>" }` |
560
+ | Resposta | `Inside` com `percent/done/total/time/is_unlocked` preenchidos. |
561
+
562
+ `FolderRest.progress` (linhas 104–133) sempre dispara `folder_progress` (`EVENT_AFTER_CREATE`) ao final, com `previous_percent == current_percent == inside.percent`. Use o trigger via `insertLog` quando precisar de "antes/depois" reais.
563
+
564
+ > **NPE confirmada** quando `folder` não existe: `FolderManager.playerProgress` retorna `null`, mas `FolderRest.progress` linhas 125–126 fazem `progress.put("previous_percent", inside.percent)` — quebra antes de devolver resposta.
565
+
566
+ ### 4.9 `POST /v3/folder/log`
567
+
568
+ | Aspecto | Detalhe |
569
+ |---|---|
570
+ | Finalidade | Registrar/atualizar progresso de um conteúdo (ver pipeline 2.3). |
571
+ | Body | `FolderLog` JSON. |
572
+ | Status | `201 Created` (mesmo quando o log **não** é salvo por falta de content). |
573
+ | Triggers | `folder_log` BEFORE/AFTER `CREATE`; `folder_progress` AFTER `CREATE` por cada folder cujo `percent` aumentou. |
574
+
575
+ **Falha silenciosa:** se `log.item` não existir em `folder_content`, retorna 201 com o body original, sem persistir nada (`FolderManager.insertLog` linhas 561–562: o `if(content != null)` engole tudo).
576
+
577
+ **Query de idempotência:**
578
+
579
+ ```javascript
580
+ db.folder_log.findOne({ player: "<player>", item: "<item>", finished: {$exists: false} })
581
+ ```
94
582
 
95
- ## Integração com Outras Técnicas
583
+ ### 4.10 `GET /v3/folder/log/{id}`
584
+
585
+ `findLog`. Retorna `FolderLog` ou body vazio se não encontrado.
586
+
587
+ ### 4.11 `DELETE /v3/folder/log/{id}`
588
+
589
+ `FolderManager.deleteLog` linhas 653–660:
590
+
591
+ 1. `findOne({_id: id})` → `log`.
592
+ 2. `triggerManager.execute(log._id, log, "folder_log", BEFORE_DELETE, log.player, null)`.
593
+ 3. `c.remove({_id: id})`.
594
+ 4. `triggerManager.execute(..., AFTER_DELETE, ...)`.
595
+
596
+ > **NPE confirmada** quando `id` não existe: `log` será `null` e o passo 2 quebra em `log._id`. **Não recalcula** progresso das pastas pai.
597
+
598
+ ### 4.12 `POST /v3/folder/content`
599
+
600
+ | Aspecto | Detalhe |
601
+ |---|---|
602
+ | Finalidade | Criar vínculo `Content` entre pasta e item. |
603
+ | Body | `Content` JSON. |
604
+ | Status | `201 Created`. |
605
+ | Triggers | Nenhum. |
606
+
607
+ Auto-cálculo de `title` ver 3.3. Sem validação de existência de `parent` (folder) nem do item real apontado por `content`.
608
+
609
+ ### 4.13 `POST /v3/folder/content/aggregate`
610
+
611
+ | Aspecto | Detalhe |
612
+ |---|---|
613
+ | Finalidade | Listar `folder_content` via pipeline MongoDB customizado, com paginação e enriquecimento opcional do dado real. |
614
+ | Body | `List<Object>` — estágios adicionais do aggregate (já vêm depois do `$match` interno). |
615
+ | Headers | `Range: items=0-N` (paginação via `PaginationUtil`, default `0-100`). |
616
+
617
+ **Query params:**
618
+
619
+ | Param | Tipo | Descrição |
620
+ |---|---|---|
621
+ | `folder` | `String` | Se não vazio: filtro de escopo. |
622
+ | `cascade` | `boolean` | `true` → expande para todos os contents na sub-árvore (`insideFolder(folder).getContentIds(true)`); `false` → apenas contents diretos. |
623
+ | `append_data` | `boolean` | `true` → para cada item retornado, adiciona campo `data` com o documento real da coleção `ContentType.entity` (via `findContentData(id)`). |
624
+
625
+ **Pipeline construído (`findAllContentAggregate` linhas 493–544):**
626
+
627
+ ```
628
+ estágio 0 (sempre):
629
+ {$match: { ... } }
630
+ ├── se folder não vazio && cascade → {_id: {$in: [ids]}}
631
+ ├── se folder não vazio && !cascade → {parent: folder}
632
+ └── se folder vazio → {} (match-tudo)
633
+ estágios 1+ (do body) → adicionados via Aggregate.and(FunifierMarshaller.toString(stage))
634
+ ```
635
+
636
+ Resultado é paginado por `PaginationUtil.getPageResultThrowsException(a, range, 0, 100)`.
637
+
638
+ > Estágios injetados pelo cliente (`$lookup`, `$out`, `$merge`, etc.) são executados sem validação. Ver Seção 8.
639
+
640
+ ### 4.14 `GET /v3/folder/content/{id}`
641
+
642
+ Retorna `Content` ou body vazio.
643
+
644
+ ### 4.15 `GET /v3/folder/content/{id}/data`
645
+
646
+ Retorna o documento real em `ContentType.entity` por `{_id: content.content}`. Retorna body vazio se:
647
+
648
+ - `content == null` (ID inexistente), ou
649
+ - `content.type` ou `content.content` `null`, ou
650
+ - `ContentType` ou item real não existirem.
651
+
652
+ `findContentData` engole essas condições e retorna `null`.
653
+
654
+ ### 4.16 `GET /v3/folder/content/{id}/move?direction=up|down`
655
+
656
+ Mesmo algoritmo de `moveFolder`, na coleção `folder_content`, **sem filtro por `active`** (`Content` não tem o campo). Status `204`.
657
+
658
+ > Diferença importante em relação a `moveFolder`: `Content.position` é `int` primitivo, não há null-guards (linhas 460–471) — direto `atual.position - 1`.
659
+
660
+ ### 4.17 `DELETE /v3/folder/content/{id}`
661
+
662
+ `FolderManager.deleteContent` linhas 477–490:
663
+
664
+ 1. `Content content = folder_content.findOne({_id: id})`.
665
+ 2. `ContentType type = folder_content_type.findOne({_id: content.type})`.
666
+ 3. Se `type != null && type.input == "formulary"` → `db[type.entity].remove({_id: content.content})`.
667
+ 4. `folder_content.remove({_id: id})`.
668
+ 5. `folder_log.remove({item: id})`.
669
+
670
+ > **NPE confirmada** quando `id` não existe (`content == null`) — passo 2 quebra em `content.type`. **Sem trigger**. **Sem recálculo** de progresso.
671
+
672
+ ### 4.18 `DELETE /v3/folder/content?q=<query>`
673
+
674
+ Mesmo padrão de `4.5` na coleção `folder_content`. `q` interpolado sem sanitização. Itera os IDs e chama `deleteContent` para cada.
675
+
676
+ ---
677
+
678
+ ## 5. Regras de Negócio
679
+
680
+ ### 5.1 Idempotência ativa do log de progresso
681
+
682
+ Enquanto `finished == null`, qualquer `insertLog` com mesmo `(player, item)` atualiza o log existente preservando `_id` e `started`. Quando o log é fechado (`status="done"` → `finished` setado), a chave de idempotência deixa de bater — chamadas posteriores criam **um novo** log.
683
+
684
+ ### 5.2 Normalização cruzada de status e percent
685
+
686
+ Servidor força a consistência:
687
+
688
+ - `status="done"` ⇒ `percent=100.0` (sobrescreve qualquer valor).
689
+ - `percent>=100` ⇒ `status="done"`, `percent=100.0`.
690
+ - Outros valores de `status` não são interpretados — apenas armazenados.
691
+
692
+ ### 5.3 Herança de `unlock_policy`
693
+
694
+ Sobe a árvore (`f.parent`) até achar o primeiro ancestral com `unlock_policy != null`. Sem nenhum ancestral com política → `is_unlocked = true`.
695
+
696
+ ### 5.4 Tipos não reconhecidos de política desbloqueiam
697
+
698
+ Em `playerProgressCascade`: se `policy.type == null` ou `policy.type != "progress"` → `is_unlocked = true; continue`. Não há outros tipos suportados.
699
+
700
+ ### 5.5 Primeiro folder em `prev` é sempre desbloqueado
701
+
702
+ Quando `folder_ref == "prev"` e o índice do folder na lista `onlyFolders` é `0`, `is_unlocked = true` independentemente de `min_percent` — não há "irmão anterior".
703
+
704
+ ### 5.6 `min_percent` é comparação inclusiva (`>=`)
705
+
706
+ `is_unlocked = (target.percent ?: 0) >= min` — `target.percent` `null` é tratado como `0.0`.
707
+
708
+ ### 5.7 Visibilidade vs deleção (`active`)
709
+
710
+ `active=false` esconde o folder da árvore de leitura e do `moveFolder`, mas **não impede a remoção em cascata** quando um ancestral é deletado.
711
+
712
+ ### 5.8 Progresso conta contents distintos com `status="done"`
713
+
714
+ O aggregate em `playerProgressCascade` agrupa por `item` antes de somar — mesmo que existam N logs com `status="done"` para o mesmo content (logs históricos após reset/recriação), eles contam **1 vez**.
715
+
716
+ ### 5.9 Triggers `folder_progress` disparam do mais próximo ao raiz, só quando o percent aumenta
717
+
718
+ Ordem após `Collections.reverse(path)`: `[content.parent, ..., raiz]`. O disparo é condicional a `folderAfter.percent > folderBefore.percent`. Logs `done` que **não mudam** o percent do folder (por exemplo, reapertar `done` no mesmo content) **não disparam** o trigger.
719
+
720
+ ### 5.10 Trigger `folder_progress` do endpoint `/progress` é incondicional
721
+
722
+ Independente de mudança, `POST /v3/folder/progress` dispara o trigger uma vez por chamada com `previous_percent == current_percent`. Útil para "ping" do progresso, ruim para handlers que assumem mudança.
723
+
724
+ ### 5.11 Multi-tenant
725
+
726
+ Cada `apiKey` resolve um `FrontController` próprio, com `ManagerFactory` e `JongoConnection` próprios. Não há cross-tenant query no módulo.
727
+
728
+ ---
729
+
730
+ ## 6. Comportamentos Automáticos
731
+
732
+ | Comportamento | Trigger | Impacto | Persistência |
733
+ |---|---|---|---|
734
+ | Auto-`_id` em `Folder` | `insertFolder` com `_id` nulo/vazio | `Guid.shortTimeMillis()` | Sim |
735
+ | Auto-`_id` em `Content` | `insertContent` com `_id` nulo/vazio | `Guid.shortTimeMillis()` | Sim |
736
+ | Auto-`_id` em `FolderLog` | `insertLog` com `_id` nulo/vazio (e sem log ativo) | `Guid.newShortGuid()` | Sim |
737
+ | Default `position=0` | `insertFolder` com `position` nulo | Pasta na posição 0 | Sim |
738
+ | Default `active=true` | `insertFolder` com `active` nulo | Pasta visível | Sim |
739
+ | Auto-`title` de `Content` | `insertContent` com `title==null` OU `ContentType.input=formulary` | Resolve `title`/`name`/`_id` do item real | Sim |
740
+ | Reutilização de `_id` do log ativo | `insertLog` com log existente `(player,item,!finished)` | Mantém histórico contínuo | Sim |
741
+ | Auto-`started` | `insertLog` sem log ativo | `started = now` | Sim |
742
+ | Auto-`finished` | `insertLog` com `status="done"` e `finished==null` | `finished = now` | Sim |
743
+ | Normalização `status`↔`percent` | `insertLog` | Coerência entre os dois campos | Sim |
744
+ | Trigger `folder_log` BEFORE/AFTER CREATE | `insertLog` (apenas se content existe) | Executa regras externas | Não (somente externo) |
745
+ | Trigger `folder_log` BEFORE/AFTER DELETE | `deleteLog` (apenas se id existe) | Executa regras externas | Não |
746
+ | Trigger `folder_progress` por breadcrumb | `insertLog` quando `diff>0` e `folderAfter.percent > folderBefore.percent` | Um trigger por folder no path, do mais próximo ao raiz | Não |
747
+ | Trigger `folder_progress` do endpoint | `POST /v3/folder/progress` | Disparo único, incondicional, no folder consultado | Não |
748
+ | Cascata de `formulary` | `deleteContent` ou `deleteFolderCascade` | Remove dado real em `ContentType.entity` | Sim (destrutivo) |
749
+ | Renumeração total de irmãos | `moveFolder` / `moveContent` | Salva **todos** os irmãos com posições sequenciais | Sim |
750
+
751
+ ### 6.1 Diagrama — encadeamento automático em `insertLog`
752
+
753
+ ```mermaid
754
+ flowchart LR
755
+ A[insertLog] --> B[normalização status/percent]
756
+ B --> C{content<br/>existe?}
757
+ C -- Não --> X[abort silencioso]
758
+ C -- Sim --> D[trigger folder_log<br/>BEFORE_CREATE]
759
+ D --> E[save]
760
+ E --> F[trigger folder_log<br/>AFTER_CREATE]
761
+ F --> G{diff &gt; 0?}
762
+ G -- Não --> Z[fim]
763
+ G -- Sim --> H[breadcrumb invertido]
764
+ H --> I[para cada folder do path]
765
+ I --> J{folderAfter.percent<br/>&gt; folderBefore.percent?}
766
+ J -- Não --> I
767
+ J -- Sim --> K[trigger folder_progress<br/>AFTER_CREATE]
768
+ K --> I
769
+ I --> Z
770
+ ```
771
+
772
+ ---
773
+
774
+ ## 7. Suportado vs NÃO Suportado
775
+
776
+ ### ✅ Suportado
777
+
778
+ - Hierarquia arbitrária de pastas com aninhamento ilimitado.
779
+ - Duas estratégias de vínculo de conteúdo (`repository`, `formulary`).
780
+ - Reordenação de folders e contents via `move?direction=up|down`.
781
+ - Cálculo recursivo de progresso por jogador com aggregate MongoDB.
782
+ - Desbloqueio condicional de pastas por percent (`unlock_policy.type="progress"`), com referência a folder específico ou `prev`.
783
+ - Herança de `unlock_policy` via ancestrais (`resolveUnlockPolicy`).
784
+ - Idempotência operacional de logs em andamento via `(player, item, !finished)`.
785
+ - Triggers `folder_log` BEFORE/AFTER CREATE e BEFORE/AFTER DELETE.
786
+ - Triggers `folder_progress` por folder no breadcrumb (apenas quando o percent realmente aumenta).
787
+ - Trigger `folder_progress` incondicional ao consultar progresso (`POST /v3/folder/progress`).
788
+ - Listagem de conteúdos via aggregate customizado com `cascade` e `append_data`.
789
+ - Deleção em cascata de pastas (folders, contents, logs, dados `formulary`).
790
+ - Auto-cálculo de `title` de `Content` baseado no item real.
791
+
792
+ ### ❌ NÃO Suportado
793
+
794
+ - **`GET /v3/folder` (listagem geral)** — não existe rota em `FolderRest`. Use `/v3/database/folder?q=...`.
795
+ - **`PUT /v3/folder/{id}`** — não existe. `POST /v3/folder` cobre create+update (upsert).
796
+ - **Endpoints CRUD para `folder_content_type`** — gestão é via `/v3/database/folder_content_type`.
797
+ - **Intercalação de folders e contents por `position`** — `insideFolder` sempre retorna **folders antes de contents**, ambos ordenados por `position` dentro do próprio grupo.
798
+ - **Filtragem de contents por `active`** — `Content` não tem `active`; um content só some quando deletado.
799
+ - **Transações atômicas** — nenhum dos pipelines (insert, delete, cascata) usa transação MongoDB. Falha parcial deixa estado inconsistente.
800
+ - **Triggers para `folder` e `folder_content`** — `insertFolder`/`deleteFolder`/`insertContent`/`deleteContent` não disparam triggers.
801
+ - **Recálculo de progresso ao deletar log** — `deleteLog` é cirúrgico; nenhum folder pai é re-agregado.
802
+ - **Validação de referências** — nenhum método valida que `parent`, `type` ou `content` apontam para documentos existentes (com exceção de `insertLog` que requer `content` existir).
803
+ - **Scheduler/job de background** — não há `Runner`/`Scheduler`/job que processe folder.
804
+ - **`type != "progress"` em `unlock_policy`** — outros valores resultam em `is_unlocked=true` (passa direto).
805
+ - **Recurso `folder_log` pago/`paginado` via `/v3/folder`** — listagem só por `/v3/database/folder_log`.
806
+
807
+ ### 7.1 Bugs e edge cases confirmados
808
+
809
+ | Local | Comportamento |
810
+ |---|---|
811
+ | `FolderManager.insertLog` linha 594 | `double diff = log.percent` (auto-unbox). Se `log.percent == null` (sem `status="done"` e sem `percent`), lança `NullPointerException` **antes** de salvar o log. Mesmo problema com `current.percent` em logs antigos. |
812
+ | `FolderManager.breadcrumbFolder` linhas 49–53 | `id` inexistente → `findOne` retorna `null` → `folder.parent` NPEs. Mesmo erro se algum `parent` referenciado não existir. |
813
+ | `FolderManager.deleteLog` linhas 655–657 | `id` inexistente → `log == null` → `log._id` NPEs antes do trigger. |
814
+ | `FolderManager.deleteContent` linhas 481–482 | `id` inexistente → `content == null` → `content.type` NPEs. |
815
+ | `FolderRest.progress` linhas 125–126 | `folder` inexistente → `FolderManager.playerProgress` retorna `null` → `inside.percent` NPEs. |
816
+ | `FolderManager.getMaxTimeInside` linhas 297–315 | (1) Quando `m.time == null` no primeiro item content, `MAX = m.time = null` (porque a condição `max==null` curto-circuita o `m.time.after(max)`), e o resto da iteração herda `null`. (2) Em recursão de folder, `MAX = getMaxTimeInside(m, max)` usa o **parâmetro** `max`, não o `MAX` acumulado — descarta valores anteriores. |
817
+ | `FolderManager.findFolder` (e similares) | Quando `_id` não existe, retorna `null`. `FolderRest.find` serializa via `JsonUtil.toJsonRemoveNullFields(null)` → resposta com body vazio mas `200 OK`. Não há `404` no módulo. |
818
+ | `FolderManager.insertLog` | Quando `log.item` aponta para um content inexistente, retorna sem salvar e sem erro — **HTTP 201 silencioso** (Seção 2.3). |
819
+ | `DELETE /v3/folder?q=` / `/content?q=` | Concatenação direta de `q` no `query("{" + q + "}")` (Seção 8). |
820
+
821
+ ---
822
+
823
+ ## 8. Segurança e Permissões
824
+
825
+ **Autenticação:** Bearer token via `@BeanParam AuthBean`. O `apiKey` (extraído do token) é o seletor de tenant em `FrontController.getInstance(authBean.getApiKey())`.
826
+
827
+ **Isolamento por tenant:** cada `FrontController` mantém seu próprio `ManagerFactory` e `Jongo`/`MongoCollection`. Não há query cross-tenant em `FolderManager`.
828
+
829
+ **Superfícies de injeção MongoDB confirmadas no código:**
830
+
831
+ 1. **`DELETE /v3/folder?q=<query>`** — `FolderRest.java:75`:
832
+
833
+ ```java
834
+ jongo.getCollection(Entity.FOLDER.collection)
835
+ .distinct("_id").query("{" + q + "}").as(String.class);
836
+ ```
837
+
838
+ `q` é concatenado bruto. Um cliente autenticado pode injetar operadores `$where`, `$or`, `$regex` etc. e disparar `deleteFolder` (cascata destrutiva) em todos os IDs retornados.
839
+
840
+ 2. **`DELETE /v3/folder/content?q=<query>`** — `FolderRest.java:230`: mesmo padrão na coleção `folder_content`.
841
+
842
+ 3. **`POST /v3/folder/content/aggregate` (body)** — `FolderManager.java:517`:
843
+
844
+ ```java
845
+ a.and(FunifierMarshaller.toString(aggregations.get(i)));
846
+ ```
847
+
848
+ Cada item do body é serializado e empurrado como estágio adicional do pipeline. Não há validação ou whitelist — `$lookup`, `$out`, `$merge`, `$function` (em Mongo recente) podem ser executados.
849
+
850
+ **Recomendações operacionais:**
851
+
852
+ - Restringir o uso desses endpoints a chaves administrativas; nunca expor diretamente ao cliente final.
853
+ - Considerar um proxy / WAF que filtre `q` por regex de operadores perigosos.
854
+ - Logs de auditoria devem capturar `q` integral e payload do aggregate.
855
+
856
+ ---
857
+
858
+ ## 9. Observabilidade e Troubleshooting
859
+
860
+ ### 9.1 Índices MongoDB
861
+
862
+ Criados em `ConnectionPool.createIndexes()` (linhas 184–197):
863
+
864
+ | Coleção | Campo(s) | Direção |
865
+ |---|---|---|
866
+ | `folder` | `parent` | `1` |
867
+ | `folder_content` | `parent` | `1` |
868
+ | `folder_content` | `position` | `1` |
869
+ | `folder_log` | `item` | `1` |
870
+ | `folder_log` | `player` | `1` |
871
+ | `folder_log` | `status` | `1` |
872
+ | `folder_log` | `time` | `1` |
873
+
874
+ > **Faltam índices úteis:** `folder_log` não tem índice composto `(player, item)` nem `(player, item, finished)` — a query de idempotência (executada em todo `insertLog`) faz scan parcial.
875
+
876
+ ### 9.2 Comandos de diagnóstico
877
+
878
+ ```http
879
+ # Existe a pasta?
880
+ GET /v3/folder/<id>
881
+
882
+ # Listar pastas raiz
883
+ GET /v3/database/folder?q={"parent":{"$exists":false}}
884
+
885
+ # Listar conteúdos diretos de uma pasta
886
+ GET /v3/database/folder_content?q={"parent":"<folder_id>"}
887
+
888
+ # Logs de progresso de um jogador
889
+ GET /v3/database/folder_log?q={"player":"<player_id>"}
890
+
891
+ # Log ativo (em andamento)
892
+ GET /v3/database/folder_log?q={"player":"<p>","item":"<i>","finished":{"$exists":false}}
893
+
894
+ # Verificar tipo de conteúdo
895
+ GET /v3/database/folder_content_type?q={"_id":"<type_id>"}
896
+
897
+ # Calcular progresso completo do jogador
898
+ POST /v3/folder/progress
899
+ {"folder":"<root_folder_id>","player":"<player_id>"}
900
+
901
+ # Árvore estrutural (sem progresso)
902
+ POST /v3/folder/inside
903
+ {"folder":"<root_folder_id>"}
904
+ ```
905
+
906
+ ### 9.3 Sintomas vs causas
907
+
908
+ | Sintoma | Causa provável |
909
+ |---|---|
910
+ | `POST /v3/folder/log` retorna `201` mas log não aparece em `folder_log` | `log.item` não existe em `folder_content`; pipeline aborta silenciosamente. |
911
+ | `POST /v3/folder/log` lança `500` (NPE) | `log.percent == null` e `status != "done"`. Ou `current.percent` de um log antigo é `null`. |
912
+ | `POST /v3/folder/breadcrumb` lança `500` (NPE) | `folder` inexistente, ou algum `parent` referenciado não existe no banco. |
913
+ | `POST /v3/folder/progress` lança `500` (NPE) | `folder` inexistente — `FolderRest` não trata `inside == null`. |
914
+ | `DELETE /v3/folder/log/{id}` lança `500` (NPE) | `id` inexistente. |
915
+ | `DELETE /v3/folder/content/{id}` lança `500` (NPE) | `id` inexistente. |
916
+ | `inside.percent` em folder folha está sempre `0.0` | Não há `folder_log` com `status="done"` para o player nesses contents; ou logs foram inseridos com `item` apontando para `Content._id` errado. |
917
+ | `inside.time` zerado mas há `done` | Provavelmente caiu no fallback `getMaxTimeInside` que tem dois bugs latentes (Seção 7). |
918
+ | Folder não aparece em `/inside` | `active == false` explícito. |
919
+ | `is_unlocked == true` em folder bloqueado | Ancestor sem `unlock_policy`, ou `policy.type` diferente de `"progress"`. |
920
+ | Trigger `folder_progress` não dispara após `insertLog` | (a) `content.parent` resolve em breadcrumb inválido; (b) o percent do folder **não aumentou** (mudança apenas no content individual sem cruzar threshold). |
921
+ | Após `insertLog`, `started` aparece muito antigo | Há log ativo `!finished` para o mesmo `(player,item)`. Esperado. |
922
+ | Após `deleteLog`, progresso da pasta não muda | Por design: `deleteLog` não recalcula folders pai. Re-consulte `POST /v3/folder/progress`. |
923
+
924
+ ---
925
+
926
+ ## 10. Exemplos Práticos
927
+
928
+ ### 10.1 Exemplo mínimo — trilha com um módulo e um conteúdo
929
+
930
+ ```http
931
+ # 1. Criar pasta raiz
932
+ POST /v3/folder
933
+ { "title": "Trilha de Onboarding" }
934
+ # → 201 { "_id":"trail001","title":"Trilha de Onboarding","position":0,"active":true }
935
+
936
+ # 2. Criar módulo filho
937
+ POST /v3/folder
938
+ { "title": "Módulo 1 — Boas-vindas", "parent":"trail001", "position":0 }
939
+ # → 201 { "_id":"mod001","parent":"trail001","position":0,"active":true }
940
+
941
+ # 3. Definir tipo de conteúdo (via API de database — não há endpoint próprio)
942
+ POST /v3/database/folder_content_type
943
+ { "_id":"video", "entity":"video__c", "input":"repository", "title":"Vídeo" }
944
+
945
+ # 4. Vincular conteúdo ao módulo
946
+ POST /v3/folder/content
947
+ { "type":"video", "content":"vid001", "parent":"mod001", "position":0 }
948
+ # → 201 { "_id":"fc001","type":"video","content":"vid001","parent":"mod001","title":"<auto>","position":0 }
949
+
950
+ # 5. Registrar conclusão pelo jogador
951
+ POST /v3/folder/log
952
+ { "item":"fc001", "player":"player123", "status":"done" }
953
+ # → 201 { "_id":"log001","item":"fc001","player":"player123","status":"done","percent":100.0,"started":"...","finished":"..." }
954
+
955
+ # 6. Consultar progresso completo
956
+ POST /v3/folder/progress
957
+ { "folder":"trail001", "player":"player123" }
958
+ # → 200 { "_id":"trail001","folder":true,"player":"player123","total":1,"done":1,"percent":100.0,"is_unlocked":true,"items":[ ... ] }
959
+ ```
960
+
961
+ ### 10.2 Exemplo avançado — desbloqueio condicional por progresso de irmão anterior
962
+
963
+ ```http
964
+ # Mod 1 (sem policy — sempre desbloqueado por ser primeiro irmão)
965
+ POST /v3/folder
966
+ { "_id":"mod001", "title":"Módulo 1", "parent":"trail001", "position":0 }
967
+
968
+ # Mod 2 — bloqueado até Mod 1 atingir 80%
969
+ POST /v3/folder
970
+ { "_id":"mod002", "title":"Módulo 2", "parent":"trail001", "position":1,
971
+ "unlock_policy": { "type":"progress", "folder_ref":"prev", "min_percent":80 } }
972
+
973
+ # Mod 3 — bloqueado até Mod 1 (específico) atingir 100%
974
+ POST /v3/folder
975
+ { "_id":"mod003", "title":"Módulo 3", "parent":"trail001", "position":2,
976
+ "unlock_policy": { "type":"progress", "folder_ref":"mod001", "min_percent":100 } }
977
+
978
+ # Filhos de Mod 3 herdam essa policy via resolveUnlockPolicy se não tiverem uma própria.
979
+ ```
980
+
981
+ ### 10.3 Exemplo — aggregate de conteúdos com dado real
982
+
983
+ ```http
984
+ # Todos os contents da trilha (cascade), enriquecidos com `data`, primeiros 50
985
+ POST /v3/folder/content/aggregate?folder=trail001&cascade=true&append_data=true
986
+ Range: items=0-49
987
+ []
988
+ # → { "results":[ { "_id":"fc001","type":"video","content":"vid001","parent":"mod001","data":{ ... item real ... } }, ... ], "pagination":{...} }
989
+
990
+ # Mesma chamada com pipeline extra (ex: limitar a contents de tipo "video")
991
+ POST /v3/folder/content/aggregate?folder=trail001&cascade=true
992
+ Range: items=0-49
993
+ [ { "$match": { "type": "video" } } ]
994
+ ```
995
+
996
+ ### 10.4 Anti-patterns — o que NÃO fazer
997
+
998
+ ```http
999
+ # ❌ Inserir log direto na coleção via /v3/database — bypassa
1000
+ # idempotência, normalização status/percent, breadcrumb, triggers folder_progress.
1001
+ POST /v3/database/folder_log
1002
+ { "item":"fc001", "player":"p1", "status":"done" }
1003
+
1004
+ # Use sempre:
1005
+ POST /v3/folder/log
1006
+ { "item":"fc001", "player":"p1", "status":"done" }
1007
+
1008
+ # ❌ DELETE em massa sem revisar `q`
1009
+ DELETE /v3/folder?q={"$where":"true"} # remove TODAS as pastas
1010
+
1011
+ # ❌ Enviar log sem `percent` e sem `status="done"` quando não há log ativo
1012
+ POST /v3/folder/log
1013
+ { "item":"fc001", "player":"p1" }
1014
+ # Resultado: NullPointerException no servidor (Seção 7). Sempre envie ou
1015
+ # `status="done"` ou um `percent` numérico.
1016
+
1017
+ # ❌ Renomear/mover folder por update direto da coleção `folder`
1018
+ POST /v3/database/folder
1019
+ { "_id":"mod002", "parent":"<novo>", "position":3 }
1020
+ # Posições de irmãos ficam furadas porque `moveFolder` é quem normaliza —
1021
+ # Use POST /v3/folder/{id}/move?direction=up|down repetidamente.
1022
+
1023
+ # ❌ Confiar em `POST /v3/folder/progress` como gatilho de mudança
1024
+ # Ele dispara `folder_progress` mesmo sem mudança (previous_percent ==
1025
+ # current_percent). Handlers que reagem a esse evento precisam comparar
1026
+ # valores ou só agir em payloads vindos do `insertLog`.
1027
+ ```
96
1028
 
97
- | Técnica | Integração |
98
- |---------|-----------|
99
- | **Challenge** | Desafio completado quando jogador termina todos os itens de um folder |
100
- | **Quiz** | Quizzes podem ser conteúdos dentro de um folder |
101
- | **Point** | Pontos concedidos ao progredir no folder |
102
- | **Achievement** | Conquista desbloqueada ao completar 100% do folder |
1029
+ ---
103
1030
 
104
- ## Validações e Testes
1031
+ ## Checklist de Configuração
105
1032
 
106
- - [ ] Pasta aparece na lista
107
- - [ ] Hierarquia pai-filho funciona
108
- - [ ] Breadcrumb retorna caminho correto
109
- - [ ] Conteúdos são associados corretamente
110
- - [ ] Progresso do jogador é calculado corretamente
111
- - [ ] Action log é gerado ao progredir
1033
+ - [ ] Definir `FolderContentType` para cada tipo de conteúdo via `POST /v3/database/folder_content_type` antes de criar `folder_content`.
1034
+ - [ ] Escolher `input`: `repository` (item já existe em outra coleção) **ou** `formulary` (gerenciado junto ao vínculo).
1035
+ - [ ] Criar estrutura de pastas raiz → módulos → sub-pastas com `position` sequencial (`0,1,2…`).
1036
+ - [ ] Em `folder_content`, garantir que `parent` (folder) e `content` (item real) já existam — o servidor não valida.
1037
+ - [ ] Para desbloqueio condicional, definir `unlock_policy` apenas no nível mais alto necessário; descendentes herdam via `resolveUnlockPolicy`.
1038
+ - [ ] Usar **sempre** `POST /v3/folder/log` para registrar progresso; nunca `POST /v3/database/folder_log`.
1039
+ - [ ] Ao enviar `folder_log`, garantir `status="done"` **ou** `percent` numérico — `null` em ambos lança NPE.
1040
+ - [ ] Verificar a árvore com `POST /v3/folder/inside` antes de publicar; com `POST /v3/folder/progress` para validar `is_unlocked`.
1041
+ - [ ] Antes de `DELETE /v3/folder/{id}` ou `/content/{id}`: revisar a sub-árvore — operação é cascata, irreversível, e em `formulary` deleta o dado real também.
1042
+ - [ ] Nunca expor `DELETE /v3/folder?q=` e `DELETE /v3/folder/content?q=` ao cliente final — `q` é interpolado bruto no MongoDB.
1043
+ - [ ] Logar e auditar uso de `POST /v3/folder/content/aggregate` — estágios são passados sem validação.
1044
+ - [ ] Se progresso parecer "travado" após `deleteLog`, lembre: não há recálculo automático; chame `POST /v3/folder/progress` para atualizar `is_unlocked` no front.
1045
+ - [ ] Para handlers de trigger `folder_progress`, distinguir origem: payload de `insertLog` tem `previous_percent != current_percent`; payload de `POST /v3/folder/progress` tem ambos iguais.