funifier-mcp 0.2.26 → 0.2.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/.cursor/rules/funifier.mdc +38 -41
  2. package/.github/copilot-instructions.md +38 -41
  3. package/AGENTS.md +56 -49
  4. package/README.md +40 -22
  5. package/datasource-funifier-docs/.coverage.json +326 -0
  6. package/datasource-funifier-docs/.validation.json +593 -0
  7. package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
  8. package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
  9. package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
  10. package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
  11. package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
  12. package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
  13. package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
  14. package/datasource-funifier-docs/knowledge/index.md +4 -1
  15. package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
  16. package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
  17. package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
  18. package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
  19. package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
  20. package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
  21. package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
  22. package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
  23. package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
  24. package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
  25. package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
  26. package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
  27. package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
  28. package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
  29. package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
  30. package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
  31. package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
  32. package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
  33. package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
  34. package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
  35. package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
  36. package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
  37. package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
  38. package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
  39. package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
  40. package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
  41. package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
  42. package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
  43. package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
  44. package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
  45. package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
  46. package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
  47. package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
  48. package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
  49. package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
  50. package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
  51. package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
  52. package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
  53. package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
  54. package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
  55. package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
  56. package/dist/cli/init.d.ts.map +1 -1
  57. package/dist/cli/init.js +42 -1
  58. package/dist/cli/init.js.map +1 -1
  59. package/dist/cli/init.test.js +74 -3
  60. package/dist/cli/init.test.js.map +1 -1
  61. package/dist/cli/persona.d.ts +3 -0
  62. package/dist/cli/persona.d.ts.map +1 -0
  63. package/dist/cli/persona.js +25 -0
  64. package/dist/cli/persona.js.map +1 -0
  65. package/dist/mcp/bundle.js +119 -93
  66. package/dist/mcp/check-update.d.ts +5 -0
  67. package/dist/mcp/check-update.d.ts.map +1 -1
  68. package/dist/mcp/check-update.js +21 -10
  69. package/dist/mcp/check-update.js.map +1 -1
  70. package/dist/mcp/check-update.test.d.ts +2 -0
  71. package/dist/mcp/check-update.test.d.ts.map +1 -0
  72. package/dist/mcp/check-update.test.js +33 -0
  73. package/dist/mcp/check-update.test.js.map +1 -0
  74. package/dist/mcp/index.js +2 -2
  75. package/dist/mcp/index.js.map +1 -1
  76. package/dist/mcp/prompts/templates.d.ts.map +1 -1
  77. package/dist/mcp/prompts/templates.js +35 -0
  78. package/dist/mcp/prompts/templates.js.map +1 -1
  79. package/dist/mcp/resources/documentation.d.ts +1 -1
  80. package/dist/mcp/resources/documentation.d.ts.map +1 -1
  81. package/dist/mcp/resources/documentation.js +39 -3
  82. package/dist/mcp/resources/documentation.js.map +1 -1
  83. package/dist/mcp/tools/connect.d.ts.map +1 -1
  84. package/dist/mcp/tools/connect.js +18 -8
  85. package/dist/mcp/tools/connect.js.map +1 -1
  86. package/dist/mcp/tools/database.d.ts.map +1 -1
  87. package/dist/mcp/tools/database.js +59 -47
  88. package/dist/mcp/tools/database.js.map +1 -1
  89. package/dist/mcp/tools/database.test.js +2 -2
  90. package/dist/mcp/tools/database.test.js.map +1 -1
  91. package/dist/mcp/tools/delete.d.ts.map +1 -1
  92. package/dist/mcp/tools/delete.js +13 -3
  93. package/dist/mcp/tools/delete.js.map +1 -1
  94. package/dist/mcp/tools/execute.d.ts.map +1 -1
  95. package/dist/mcp/tools/execute.js +20 -9
  96. package/dist/mcp/tools/execute.js.map +1 -1
  97. package/dist/mcp/tools/folder.d.ts.map +1 -1
  98. package/dist/mcp/tools/folder.js +22 -12
  99. package/dist/mcp/tools/folder.js.map +1 -1
  100. package/dist/mcp/tools/get.d.ts.map +1 -1
  101. package/dist/mcp/tools/get.js +16 -6
  102. package/dist/mcp/tools/get.js.map +1 -1
  103. package/dist/mcp/tools/index.d.ts +1 -1
  104. package/dist/mcp/tools/index.d.ts.map +1 -1
  105. package/dist/mcp/tools/index.js +28 -1
  106. package/dist/mcp/tools/index.js.map +1 -1
  107. package/dist/mcp/tools/list.d.ts.map +1 -1
  108. package/dist/mcp/tools/list.js +38 -14
  109. package/dist/mcp/tools/list.js.map +1 -1
  110. package/dist/mcp/tools/logs.d.ts.map +1 -1
  111. package/dist/mcp/tools/logs.js +15 -5
  112. package/dist/mcp/tools/logs.js.map +1 -1
  113. package/dist/mcp/tools/save.d.ts.map +1 -1
  114. package/dist/mcp/tools/save.js +14 -4
  115. package/dist/mcp/tools/save.js.map +1 -1
  116. package/dist/mcp/tools/save.test.js +3 -3
  117. package/dist/mcp/tools/save.test.js.map +1 -1
  118. package/dist/mcp/tools/search-docs.d.ts +3 -0
  119. package/dist/mcp/tools/search-docs.d.ts.map +1 -0
  120. package/dist/mcp/tools/search-docs.js +102 -0
  121. package/dist/mcp/tools/search-docs.js.map +1 -0
  122. package/package.json +6 -2
  123. package/skills/acquire-funifier-knowledge/SKILL.md +155 -0
  124. package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
  125. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
  126. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
  127. package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
  128. package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
  129. package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
  130. package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +86 -0
  131. package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
  132. package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
  133. package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
  134. package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
  135. package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
  136. package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
  137. package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
  138. package/skills/funifier/SKILL.md +88 -0
  139. package/skills/funifier/references/configure-security.md +96 -0
  140. package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
  141. package/skills/funifier/references/create-aggregate.md +144 -0
  142. package/skills/funifier/references/create-challenge.md +116 -0
  143. package/skills/funifier/references/create-competition.md +98 -0
  144. package/skills/funifier/references/create-crossword.md +574 -0
  145. package/skills/funifier/references/create-custom-object.md +91 -0
  146. package/skills/funifier/references/create-custom-page.md +135 -0
  147. package/skills/funifier/references/create-folder.md +104 -0
  148. package/skills/funifier/references/create-lastmile.md +643 -0
  149. package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
  150. package/skills/funifier/references/create-level.md +94 -0
  151. package/skills/funifier/references/create-lottery.md +913 -0
  152. package/skills/funifier/references/create-mystery.md +769 -0
  153. package/skills/funifier/references/create-notification.md +75 -0
  154. package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
  155. package/skills/funifier/references/create-quiz.md +98 -0
  156. package/skills/funifier/references/create-scheduler.md +141 -0
  157. package/skills/funifier/references/create-story.md +636 -0
  158. package/skills/funifier/references/create-swap.md +95 -0
  159. package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
  160. package/skills/funifier/references/create-virtual-good.md +96 -0
  161. package/skills/funifier/references/create-webhook.md +72 -0
  162. package/skills/funifier/references/create-websocket.md +71 -0
  163. package/skills/funifier/references/create-widget.md +76 -0
  164. package/skills/funifier/references/debug.md +87 -0
  165. package/skills/funifier/references/help.md +81 -0
  166. package/skills/funifier/references/implement-frontend.md +106 -0
  167. package/skills/funifier/references/import-csv.md +75 -0
  168. package/skills/funifier/references/manage-player.md +82 -0
  169. package/skills/funifier/references/manage-team.md +76 -0
  170. package/skills/funifier/references/upload-file.md +91 -0
  171. package/skills/funifier-create-aggregate/SKILL.md +0 -127
  172. package/skills/funifier-create-challenge/SKILL.md +0 -88
  173. package/skills/funifier-create-custom-page/SKILL.md +0 -127
  174. package/skills/funifier-create-level/SKILL.md +0 -87
  175. package/skills/funifier-create-quiz/SKILL.md +0 -87
  176. package/skills/funifier-create-scheduler/SKILL.md +0 -127
  177. package/skills/funifier-create-virtual-good/SKILL.md +0 -87
  178. package/skills/funifier-debug/SKILL.md +0 -92
  179. package/skills/funifier-help/SKILL.md +0 -86
  180. package/skills/funifier-implement-frontend/SKILL.md +0 -90
  181. package/skills/funifier-index/SKILL.md +0 -58
@@ -1,390 +1,1045 @@
1
- # Folder (Trilha / Curso)
1
+ # `folder`
2
2
 
3
3
  **Acesso Studio:** `/studio/folder`
4
- **API Base:** `/v3/folder`
4
+ **API Endpoint:** `/v3/folder`
5
+ **Coleções MongoDB:** `folder`, `folder_content`, `folder_content_type`, `folder_log`
5
6
 
6
- ## O que é
7
+ ---
8
+
9
+ ## 1. Visão Geral
10
+
11
+ O módulo `folder` implementa um sistema de trilhas hierárquicas (cursos, módulos, aulas, conteúdos). Cinco artefatos formam o módulo:
7
12
 
8
- Sistema de trilhas de aprendizagem e cursos. Permite criar hierarquias de pastas com conteúdos associados (quizzes, vídeos, textos, desafios), monitorar o progresso de cada jogador em cada nível da hierarquia, e desbloquear conteúdo condicionalmente com base no progresso anterior.
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**.
9
18
 
10
- ## Quando usar
19
+ O módulo resolve três problemas:
11
20
 
12
- - Para estruturar cursos com módulos e aulas
13
- - Para criar trilhas de capacitação/treinamento
14
- - Para acompanhar o progresso individual de jogadores em jornadas
15
- - Para organizar conteúdos em hierarquias com controle de ordem e desbloqueio
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.
16
24
 
17
- ## Collections MongoDB
25
+ **Limitações estruturais confirmadas no código:**
18
26
 
19
- | Collection | Descrição |
20
- |------------|-----------|
21
- | `folder` | Estrutura de pastas (raiz e sub-pastas) |
22
- | `folder_content` | Vínculo entre pasta e item de conteúdo |
23
- | `folder_content_type` | Schema de um tipo de conteúdo (video, text, quiz…) |
24
- | `folder_log` | Histórico de progresso do jogador por item |
27
+ - Não 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`.
25
30
 
26
31
  ---
27
32
 
28
- ## Entidades
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`:**
97
+
98
+ ```json
99
+ {
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>"
107
+ }
108
+ ```
29
109
 
30
- ### Folder
110
+ ### 2.4 Pipeline `playerProgress` (`POST /v3/folder/progress`)
31
111
 
32
- Representa uma pasta ou módulo da trilha.
112
+ `FolderManager.playerProgress(folder, player)` linhas 132–149:
33
113
 
34
- | Campo | Tipo | Obrigatório | Descrição |
35
- |-------|------|-------------|-----------|
36
- | `_id` | string | Não (auto) | ID único |
37
- | `title` | string | **Sim** | Nome da pasta |
38
- | `type` | string | Não | Categoria da pasta (livre) |
39
- | `parent` | string | Não | ID da pasta pai (ausente = raiz) |
40
- | `position` | number | Não (default 0) | Ordem de exibição entre irmãos |
41
- | `active` | boolean | Não (default true) | false = oculta da árvore |
42
- | `unlock_policy` | UnlockPolicy | Não | Regra de desbloqueio condicional |
43
- | `extra` | object | Não | Campos livres adicionais |
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`.
44
121
 
45
- ### UnlockPolicy
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:
46
123
 
47
- Define quando uma pasta fica disponível para o jogador.
124
+ ```java
125
+ manager.getTriggerManager().execute(folder, progress, "folder_progress", Trigger.EVENT_AFTER_CREATE, player, null);
126
+ ```
48
127
 
49
- | Campo | Tipo | Valores | Descrição |
50
- |-------|------|---------|-----------|
51
- | `type` | string | `"progress"` | Único tipo suportado |
52
- | `folder_ref` | string | `"prev"` ou ID | `"prev"` = pasta irmã anterior; ID = pasta específica |
53
- | `min_percent` | number | 0–100 | Percentual mínimo exigido |
128
+ Neste caminho `previous_percent == current_percent == inside.percent` não há "antes/depois" no endpoint de leitura.
54
129
 
55
- **Herança:** o servidor sobe na árvore até encontrar um `unlock_policy`. Se uma pasta não tem policy própria, herda do pai.
130
+ ### 2.5 Pipeline `playerProgressCascade` (recursivo)
56
131
 
57
- ### FolderContent
132
+ `FolderManager.playerProgressCascade(inside, player, root)` — linhas 151–234:
58
133
 
59
- Vínculo entre uma pasta e um item de conteúdo.
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
+ ```
60
252
 
61
- | Campo | Tipo | Obrigatório | Descrição |
62
- |-------|------|-------------|-----------|
63
- | `_id` | string | Não (auto) | ID único |
64
- | `type` | string | **Sim** | `_id` do `FolderContentType` |
65
- | `content` | string | Condicional | `_id` do item real (apenas quando `input=repository`) |
66
- | `parent` | string | **Sim** | `_id` da pasta pai |
67
- | `title` | string | Não | Auto-populado do item real se omitido |
68
- | `extra` | object | Não | Campos livres |
69
- | `position` | number | Não (default 0) | Ordem de exibição |
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
+ ```
70
269
 
71
- ### FolderContentType
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).
72
271
 
73
- Define o schema de um tipo de conteúdo.
272
+ ---
74
273
 
75
- | Campo | Tipo | Obrigatório | Descrição |
76
- |-------|------|-------------|-----------|
77
- | `_id` | string | **Sim** | Identificador (ex: `"video"`, `"text"`, `"quiz"`) |
78
- | `entity` | string | **Sim** | Nome da coleção MongoDB onde os dados reais ficam |
79
- | `input` | string | **Sim** | `"repository"` ou `"formulary"` (ver abaixo) |
80
- | `title` | string | Não | Label de exibição |
81
- | `image` | string | Não | URL de ícone |
82
- | `slug` | string | Não | Slug para URL |
83
- | `attributes` | array | Não | Campos do formulário (quando `input=formulary`) |
274
+ ## 3. Estrutura dos Objetos
84
275
 
85
- **`input=repository`:** o conteúdo referencia um item já existente em outra coleção via `content`.
86
- **`input=formulary`:** o conteúdo cria um novo item inline na coleção `entity`. ⚠️ Ao deletar o `FolderContent`, o dado real também é deletado.
276
+ ### 3.1 `Folder` coleção `folder`
87
277
 
88
- ### FolderLog
278
+ Definido em `src/main/java/com/funifier/engine/folder/Folder.java`.
89
279
 
90
- Registo de progresso de um jogador em um conteúdo.
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. |
91
290
 
92
- | Campo | Tipo | Obrigatório | Descrição |
93
- |-------|------|-------------|-----------|
94
- | `_id` | string | Não (auto) | ID único |
95
- | `item` | string | **Sim** | `_id` do `FolderContent` |
96
- | `player` | string | **Sim** | ID do jogador |
97
- | `status` | string | Não | `"done"` para concluído |
98
- | `percent` | number | Não | 0.0–100.0 |
99
- | `started` | Date | Não (auto) | Preenchido automaticamente |
100
- | `finished` | Date | Não (auto) | Preenchido quando concluído |
101
- | `extra` | object | Não | Campos livres (ex: `date`) |
291
+ **Campos desconhecidos no JSON de entrada são silenciosamente descartados** (`@JsonIgnoreProperties(ignoreUnknown=true)`). Não existem campos deprecated ou comentados nesta classe.
102
292
 
103
- ### Inside (resposta de inside/progress)
293
+ **Comportamento de `active` por operação:**
104
294
 
105
- da árvore retornado por `POST /v3/folder/inside` e `POST /v3/folder/progress`.
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. |
106
302
 
107
- | Campo | Tipo | Descrição |
108
- |-------|------|-----------|
109
- | `_id` | string | ID do nó |
110
- | `folder` | boolean | `true`=pasta, `false`=conteúdo |
111
- | `title` | string | Título |
112
- | `type` | string | Tipo |
113
- | `content` | string | ID do item real (apenas em conteúdos) |
114
- | `items` | Inside[] | Filhos (sub-pastas e conteúdos), ordenados por `position` |
115
- | `player` | string | ID do jogador (apenas em `progress`) |
116
- | `total` | number | Total de conteúdos na sub-árvore (apenas em `progress`) |
117
- | `done` | number | Conteúdos concluídos (apenas em `progress`) |
118
- | `percent` | number | % de conclusão (apenas em `progress`) |
119
- | `time` | Date | Data da última conclusão na sub-árvore |
120
- | `is_unlocked` | boolean | Se a pasta está desbloqueada (null para conteúdos) |
303
+ ### 3.2 `UnlockPolicy` embutido em `Folder.unlock_policy`
121
304
 
122
- ---
305
+ Definido em `src/main/java/com/funifier/engine/folder/UnlockPolicy.java`.
123
306
 
124
- ## API Endpoints
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). |
125
312
 
126
- ### Folder
313
+ **Resolução via `resolveUnlockPolicy(folderId)`** — linhas 236–248:
127
314
 
128
- #### Criar Pasta
129
- **POST** `/v3/folder`
130
- ```json
131
- { "title": "Módulo 1", "parent": "raiz_id", "position": 0 }
132
315
  ```
133
- Resposta: `Folder` criado com `_id` gerado.
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
+ ```
134
323
 
135
- #### Buscar Pasta
136
- **GET** `/v3/folder/:id`
324
+ Sobe a árvore até achar o primeiro ancestral com política. Sem política em nenhum ancestral → `is_unlocked = true`.
137
325
 
138
- #### Listar Pastas
139
- **GET** `/v3/database/folder`
326
+ ### 3.3 `Content` — coleção `folder_content`
140
327
 
141
- #### Deletar Pasta ⚠️ CASCATA
142
- **DELETE** `/v3/folder/:id`
328
+ Definido em `src/main/java/com/funifier/engine/folder/Content.java`.
143
329
 
144
- > Remove recursivamente: sub-pastas, folder_content, folder_log, e se `ContentType.input = "formulary"` — os dados reais na coleção `entity`. **Irreversível.**
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`. |
145
339
 
146
- #### Deletar por Query
147
- **DELETE** `/v3/folder?q=parent:"<id>"`
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).
148
341
 
149
- #### Reordenar Pasta
150
- **GET** `/v3/folder/:id/move?direction=up` ou `direction=down`
342
+ **Auto-cálculo de `title` em `insertContent`** (linhas 399–415):
151
343
 
152
- #### Árvore Completa
153
- **POST** `/v3/folder/inside`
154
- ```json
155
- { "folder": "<id>" }
156
344
  ```
157
- Retorna árvore `Inside` completa (pastas + conteúdos), ordenada por `position`.
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
351
+ ```
158
352
 
159
- #### Progresso do Jogador
160
- **POST** `/v3/folder/progress`
161
- ```json
162
- { "folder": "<id>", "player": "<playerId>" }
353
+ > Em `formulary`, o título do `Content` é **sempre sobrescrito** pelo título do item real — qualquer `title` enviado pelo cliente é descartado.
354
+
355
+ ### 3.4 `ContentType` — coleção `folder_content_type`
356
+
357
+ Definido em `src/main/java/com/funifier/engine/folder/ContentType.java`.
358
+
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. |
368
+
369
+ **Constantes no código:**
370
+
371
+ ```java
372
+ ContentType.INPUT_REPOSITORY = "repository"
373
+ ContentType.INPUT_FORMULARY = "formulary"
163
374
  ```
164
- Retorna árvore `Inside` com `total`, `done`, `percent`, `time`, `is_unlocked` por nó. Também **dispara o trigger `folder_progress`**.
165
375
 
166
- #### Breadcrumb
167
- **POST** `/v3/folder/breadcrumb`
168
- ```json
169
- { "folder": "<id>" }
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"
170
399
  ```
171
- Retorna array de `Folder` do raiz até o nó: `[root, ..., target]`.
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).
172
435
 
173
436
  ---
174
437
 
175
- ### Folder Content
438
+ ## 4. Endpoints
176
439
 
177
- #### Adicionar Conteúdo
178
- **POST** `/v3/folder/content`
179
- ```json
180
- { "type": "video", "content": "<video_id>", "parent": "<folder_id>" }
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 }
181
465
  ```
182
466
 
183
- #### Buscar Conteúdo
184
- **GET** `/v3/folder/content/:id`
467
+ ### 4.2 `GET /v3/folder/{id}`
185
468
 
186
- #### Buscar Dados Reais do Conteúdo
187
- **GET** `/v3/folder/content/:id/data`
188
- Retorna o objeto real da coleção `entity` associada ao `FolderContentType`.
469
+ | Aspecto | Detalhe |
470
+ |---|---|
471
+ | Finalidade | Buscar pasta por `_id`. |
472
+ | Resposta | `200 OK` com `Folder` ou body vazio se `findOne` retornar `null`. |
189
473
 
190
- #### Listar Conteúdos (com aggregate)
191
- **POST** `/v3/folder/content/aggregate?folder=<id>&cascade=true&append_data=true`
192
- Body: pipeline MongoDB opcional (array).
193
- - `cascade=true`: inclui conteúdos de sub-pastas
194
- - `append_data=true`: adiciona campo `data` com o objeto real de cada conteúdo
474
+ ### 4.3 `GET /v3/folder/{id}/move?direction=up|down`
195
475
 
196
- #### Listar Conteúdos Diretos
197
- **GET** `/v3/database/folder_content`
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`. |
198
480
 
199
- #### Reordenar Conteúdo
200
- **GET** `/v3/folder/content/:id/move?direction=up` ou `direction=down`
481
+ **Algoritmo (`moveFolder` linhas 251–295):**
201
482
 
202
- #### Deletar Conteúdo
203
- **DELETE** `/v3/folder/content/:id`
204
- > Se `ContentType.input = "formulary"`, também deleta o dado real. Deleta todos os `folder_log` do conteúdo.
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.
205
490
 
206
- #### Deletar por Query
207
- **DELETE** `/v3/folder/content?q=parent:"<id>"`
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.
208
492
 
209
- ---
493
+ ### 4.4 `DELETE /v3/folder/{id}`
210
494
 
211
- ### Folder Content Type
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`). |
212
500
 
213
- #### Criar / Atualizar Tipo
214
- **PUT** `/v3/database/folder_content_type`
215
- ```json
216
- {
217
- "_id": "video",
218
- "entity": "video__c",
219
- "input": "repository",
220
- "title": "Vídeo"
221
- }
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 + "}")
222
509
  ```
223
510
 
224
- #### Listar Tipos
225
- **GET** `/v3/database/folder_content_type`
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.
226
512
 
227
- ---
513
+ ### 4.6 `POST /v3/folder/inside`
228
514
 
229
- ### Folder Log
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):**
230
540
 
231
- #### Registrar Progresso ⚠️ Via endpoint REST (não via database direto)
232
- **POST** `/v3/folder/log`
233
- ```json
234
- { "item": "<content_id>", "player": "player123", "status": "done" }
235
541
  ```
236
- Ou com percentual parcial:
237
- ```json
238
- { "item": "<content_id>", "player": "player123", "percent": 60.0 }
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
239
550
  ```
240
551
 
241
- > **Importante:** use sempre `/v3/folder/log` para registrar progresso, nunca insira direto na collection `folder_log`. O endpoint contém lógica de negócio crítica (idempotência, cálculo de progresso, triggers).
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.
242
553
 
243
- #### Buscar Log
244
- **GET** `/v3/folder/log/:id`
554
+ ### 4.8 `POST /v3/folder/progress`
245
555
 
246
- #### Deletar Log
247
- **DELETE** `/v3/folder/log/:id`
248
- Dispara `BEFORE_DELETE` e `AFTER_DELETE` em triggers de `folder_log`.
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. |
249
561
 
250
- #### Listar Logs (consulta)
251
- **GET** `/v3/database/folder_log`
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.
252
563
 
253
- ---
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
+ ```
582
+
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
+ ```
254
635
 
255
- ## Comportamentos Críticos de Segurança
636
+ Resultado é paginado por `PaginationUtil.getPageResultThrowsException(a, range, 0, 100)`.
256
637
 
257
- ### 1. DELETE em Cascata (deleteFolder)
258
- `DELETE /v3/folder/:id` é uma operação destrutiva em cascata:
259
- 1. Deleta todos os sub-folders recursivamente
260
- 2. Deleta todos os `folder_content` da pasta
261
- 3. Deleta todos os `folder_log` dos conteúdos
262
- 4. Se `FolderContentType.input = "formulary"`: deleta o dado real na coleção `entity`
638
+ > Estágios injetados pelo cliente (`$lookup`, `$out`, `$merge`, etc.) são executados sem validação. Ver Seção 8.
263
639
 
264
- **⚠️ Sempre confirmar com o usuário antes de executar. Irreversível.**
640
+ ### 4.14 `GET /v3/folder/content/{id}`
265
641
 
266
- ### 2. insertLog é Idempotente
267
- O servidor busca log existente por `(player, item)` sem `finished`. Se encontrado, atualiza em vez de criar novo. Nunca gera duplicata para um conteúdo em andamento.
642
+ Retorna `Content` ou body vazio.
268
643
 
269
- ### 3. Auto-Status
270
- - `status: "done"` → `percent = 100.0` + `finished = now` (automático)
271
- - `percent >= 100` → `status = "done"` + `finished = now` (automático)
644
+ ### 4.15 `GET /v3/folder/content/{id}/data`
272
645
 
273
- ### 4. Herança de unlock_policy
274
- O servidor percorre a árvore para cima até encontrar um `unlock_policy`. Pastas sem policy própria herdam do pai.
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.
275
675
 
276
676
  ---
277
677
 
278
- ## Trigger Events
678
+ ## 5. Regras de Negócio
279
679
 
280
- ### Coleção `folder_log`
281
- | Evento | Quando |
282
- |--------|--------|
283
- | `BEFORE_CREATE` | Antes de inserir/atualizar log |
284
- | `AFTER_CREATE` | Após inserir/atualizar log |
285
- | `BEFORE_DELETE` | Antes de deletar log |
286
- | `AFTER_DELETE` | Após deletar log |
680
+ ### 5.1 Idempotência ativa do log de progresso
287
681
 
288
- ### Coleção `folder_progress` (evento: `AFTER_CREATE`)
289
- Disparado para **cada pasta no breadcrumb** (do mais próximo ao raiz) sempre que o progresso aumenta. Payload:
290
- ```json
291
- {
292
- "_id": "<folderId>",
293
- "player": "<playerId>",
294
- "folder": { "_id": "...", "title": "Módulo 1" },
295
- "progress": { "percent": 60.0, "done": 3, "total": 5 },
296
- "previous_percent": 40.0,
297
- "current_percent": 60.0,
298
- "time": "<timestamp>"
299
- }
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
300
770
  ```
301
771
 
302
772
  ---
303
773
 
304
- ## Uso com MCP
305
-
306
- | Operação | Tool | Parâmetros |
307
- |---------|------|-----------|
308
- | Criar/atualizar pasta | `funifier_save` | `type: "folder"` |
309
- | Deletar pasta (cascata) | `funifier_delete` | `type: "folder"` |
310
- | Adicionar conteúdo | `funifier_save` | `type: "folder-content"` |
311
- | Deletar conteúdo | `funifier_delete` | `type: "folder-content"` |
312
- | Definir tipo de conteúdo | `funifier_save` | `type: "folder-content-type"` |
313
- | Registrar progresso | `funifier_save` | `type: "folder-log"` |
314
- | Deletar log | `funifier_delete` | `type: "folder-log"` |
315
- | Ver árvore completa | `funifier_folder` | `operation: "inside"` |
316
- | Ver progresso do jogador | `funifier_folder` | `operation: "progress"` |
317
- | Ver breadcrumb | `funifier_folder` | `operation: "breadcrumb"` |
318
- | Consultar logs/conteúdos | `funifier_database` | `collection: "folder_log"` etc. |
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). |
319
820
 
320
821
  ---
321
822
 
322
- ## Exemplo Completo: Criar Trilha de Vendas
823
+ ## 8. Segurança e Permissões
323
824
 
324
- ### 1. Criar pasta raiz
325
- ```json
326
- funifier_save type=folder
327
- { "title": "Trilha de Vendas" }
328
- ```
825
+ **Autenticação:** Bearer token via `@BeanParam AuthBean`. O `apiKey` (extraído do token) é o seletor de tenant em `FrontController.getInstance(authBean.getApiKey())`.
329
826
 
330
- ### 2. Criar módulo dentro da trilha
331
- ```json
332
- funifier_save type=folder
333
- { "title": "Módulo 1 - Prospecção", "parent": "<trilha_id>", "position": 0 }
334
- ```
827
+ **Isolamento por tenant:** cada `FrontController` mantém seu próprio `ManagerFactory` e `Jongo`/`MongoCollection`. Não há query cross-tenant em `FolderManager`.
335
828
 
336
- ### 3. Criar segundo módulo com unlock_policy
337
- ```json
338
- funifier_save type=folder
339
- {
340
- "title": "Módulo 2 - Fechamento",
341
- "parent": "<trilha_id>",
342
- "position": 1,
343
- "unlock_policy": { "type": "progress", "folder_ref": "prev", "min_percent": 80 }
344
- }
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>"}
345
904
  ```
346
905
 
347
- ### 4. Definir tipo de conteúdo
348
- ```json
349
- funifier_save type=folder-content-type
350
- { "_id": "video", "entity": "video__c", "input": "repository", "title": "Vídeo" }
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":[ ... ] }
351
959
  ```
352
960
 
353
- ### 5. Adicionar conteúdo ao módulo
354
- ```json
355
- funifier_save type=folder-content
356
- { "type": "video", "content": "<video_id>", "parent": "<modulo1_id>" }
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.
357
979
  ```
358
980
 
359
- ### 6. Registrar progresso do jogador
360
- ```json
361
- funifier_save type=folder-log
362
- { "item": "<content_id>", "player": "player123", "status": "done" }
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" } } ]
363
994
  ```
364
995
 
365
- ### 7. Consultar progresso completo
366
- ```json
367
- funifier_folder operation=progress
368
- { "folder": "<trilha_id>", "player": "player123" }
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`.
369
1027
  ```
370
1028
 
371
1029
  ---
372
1030
 
373
- ## Integração com Outras Técnicas
374
-
375
- | Técnica | Integração |
376
- |---------|-----------|
377
- | **Challenge** | Criar desafio vinculado à completion do folder (rule: `folder_progress` com `percent >= 100`) |
378
- | **Point** | Conceder pontos via trigger `folder_progress` quando `current_percent >= 100` |
379
- | **Achievement** | Conquista ao completar 100% de um folder |
380
- | **Quiz** | Quiz como `FolderContent` dentro de um folder (via `folder-content-type` com `input=repository`) |
381
-
382
1031
  ## Checklist de Configuração
383
1032
 
384
- - [ ] Criar estrutura de pastas (raiz + sub-pastas com position)
385
- - [ ] Definir FolderContentTypes necessários
386
- - [ ] Adicionar FolderContent às pastas
387
- - [ ] Configurar unlock_policy nas pastas que precisam de desbloqueio condicional
388
- - [ ] Verificar árvore com `funifier_folder operation=inside`
389
- - [ ] Testar progresso com `funifier_folder operation=progress`
390
- - [ ] Configurar triggers em `folder_log` ou `folder_progress` se necessário
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.