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,101 +1,801 @@
1
- # Player (Jogador)
1
+ # `player`
2
2
 
3
3
  **Acesso Studio:** `/studio/player`
4
4
  **API Endpoint:** `/v3/player`
5
+ **Coleção MongoDB:** `player`
5
6
 
6
- ## O que é
7
+ > Documentação de engenharia reversa baseada exclusivamente no código-fonte de `funifier-service`. Arquivos principais:
8
+ > `engine/player/PlayerManager.java`, `engine/player/PlayerDaoMongo.java`, `engine/player/Player.java`, `engine/player/Principal.java`, `engine/player/PlayerAsyncProcessor.java`, `rest/v3/rest/PlayerRest.java` (API v3), `rest/engine/PlayerRest.java` (API legada `2.0.0/player`), `engine/achievement/PlayerStatus.java`.
7
9
 
8
- Cadastro e gerenciamento dos participantes da gamificação. Permite cadastrar e detalhar cada jogador, incluindo nome, login, email, equipes, amigos, foto, avatar e informações adicionais como telefone, integração com redes sociais e data de aniversário. Também é possível definir senha para cada jogador.
10
+ ---
9
11
 
10
- ## Quando usar
12
+ ## 1. Visão Geral
11
13
 
12
- - Em todo projeto de gamificação (obrigatório)
13
- - Para cadastrar participantes da gamificação
14
- - Para associar jogadores a equipes
15
- - Para armazenar informações extras (departamento, cargo, etc.)
14
+ O módulo `player` gerencia os participantes de uma gamificação — qualquer sujeito identificável (usuário, estudante, colaborador, cliente) que executa ações e acumula progresso. O `_id` do player é também o seu login.
16
15
 
17
- ## Checklist de Configuração no Studio
16
+ Papel arquitetural: o player é o nó central do grafo de gamificação. Praticamente todas as coleções de progresso referenciam o `player._id`, e a exclusão de um player dispara uma cascata em ~20 coleções (`PlayerDaoMongo.delete`).
18
17
 
19
- - [ ] Definir o _id do jogador (login)
20
- - [ ] Definir nome do jogador
21
- - [ ] Definir email (se necessário para notificações)
22
- - [ ] Associar a equipes (se aplicável)
23
- - [ ] Definir campos extras no campo "extra" (departamento, cargo, etc.)
24
- - [ ] Definir senha (se autenticação for necessária)
18
+ O módulo mantém **dois documentos paralelos** por jogador:
25
19
 
26
- ## API Endpoints
20
+ - `player` — dados cadastrais e de perfil (gerenciado pelo `PlayerManager`).
21
+ - `principal` — documento de segurança usado pela autenticação/autorização. Criado automaticamente no primeiro `insert()` e guarda as `roles`.
27
22
 
28
- ### Listar Jogadores
29
- **Método:** GET
30
- **Endpoint:** `/v3/player`
31
- **Descrição:** Retorna todos os jogadores cadastrados.
23
+ O **status gamificado** (pontos, nível, conquistas, posições de ranking) **não fica no documento `player`** — fica na coleção `player_status`, gerenciada exclusivamente pelo `AchievementManager`. A sincronização é assíncrona, via `PlayerAsyncProcessor` (uma thread dedicada por gamificação).
32
24
 
33
- **Exemplo de Resposta:**
34
- ```json
35
- [
36
- {
37
- "_id": "jerry",
38
- "name": "Jerry",
39
- "email": "jerry@yourdomain.com",
40
- "extra": { "company": "Tom & Jerry Inc." },
41
- "created": 1694990893810,
42
- "updated": 1694990893880
43
- }
44
- ]
45
- ```
46
-
47
- ### Criar e Atualizar Jogador
48
- **Método:** POST
49
- **Endpoint:** `/v3/player`
50
- **Descrição:** Cria um novo jogador com atributos personalizados ou atualiza um jogador existente.
51
-
52
- **Exemplo de Body:**
25
+ Integrações reais confirmadas no código (chamadores de `getPlayerManager()`):
26
+
27
+ | Módulo | Como integra | Evidência |
28
+ |---|---|---|
29
+ | `achievement` | Recalcula e lê `player_status` (`updatePlayerStatus`, `findPlayerStatus`) | `AchievementManager` L561/L716; `PlayerRest` L237/L281 |
30
+ | `authentication` | Login valida `player.password` via `BCrypt.checkpw`; cria players ausentes (`createIfDontExist`) | `AuthenticationManager` L324, L341 |
31
+ | `action` (action_log) | Auto-cria player inexistente referenciado por um log | `ActionManager` L157; `ActionRest` L465/L635 |
32
+ | `point` | Dispara recálculo de status ao creditar/transferir pontos | `PointManager` L35/L38 |
33
+ | `catalog`/`purchase` | Dispara recálculo de status na compra | `CatalogManager` L845 |
34
+ | `team` | `insert(player)` ao alterar vínculo de time; `findTeamIdsByUserId` | `TeamManager` L197/L367 |
35
+ | `role` | `/v3/role` delega a `insertRole`/`removeRole` (grava em `principal`) | `RoleRest` L42/L67 |
36
+ | `lottery`, `competition`, `challenge`, `mystery`, `quiz`, `question`, `leaderboard` | Lookups e recálculo de status | múltiplos managers |
37
+ | `mobile` | Remove devices na exclusão do player | `PlayerManager.delete` L129 |
38
+ | `upload` | Pode excluir player (cascata completa) | `UploadRest` L464 |
39
+ | `trigger` | Eventos `BEFORE/AFTER_CREATE/UPDATE/DELETE` | `PlayerManager.insert/delete` |
40
+ | `audit` | Log de `EVENT_CREATE`/`EVENT_UPDATE` | `PlayerManager.insert` L296/L306 |
41
+
42
+ ---
43
+
44
+ ## 2. Arquitetura e Fluxos
45
+
46
+ ### 2.1 Pipeline de criação/atualização — `PlayerManager.insert()` (L244–325)
47
+
48
+ `insert()` é o **único** ponto de escrita do player. Os métodos legados `add()`/`update()` estão comentados (L64–92). Toda a API v3 (POST, PUT, bulk, friends, teams, password) converge para `insert()`.
49
+
50
+ ```
51
+ 1. [Guard] player != null && _id != null && _id.trim().length()>0 (senão: no-op silencioso) L250
52
+ 2. [Sanitização] player.setId(_id.trim()) L253
53
+ 3. [Lookup] principal = principal_collection.findOne({userId: _id}) L258
54
+ 4. [Decisão] principal == null → CRIAÇÃO ; principal != null → ATUALIZAÇÃO L259
55
+ 5. [created] se CRIAÇÃO: player.created = now() L260
56
+ 6. [Trigger] BEFORE_CREATE (criação) ou BEFORE_UPDATE (atualização) L262/265
57
+ 7. [updated] player.updated = now() (SEMPRE, após o trigger) L268
58
+ 8. [Alt logins] remove de player.alternative_logins os logins já usados por OUTRO player (silencioso) L272-282
59
+ 9. [Save] player_collection.save(player) ← upsert nativo, REPLACE do documento inteiro L287
60
+ 10.[Principal] se CRIAÇÃO: cria Principal(id,name,TYPE_USER,null,id) + AFTER_CREATE + audit(CREATE) L289-297
61
+ se ATUALIZAÇÃO: principal.setName(name) + save + AFTER_UPDATE + audit(UPDATE) L299-307
62
+ 11.[Teams] teamDao.deleteLinksByPlayer(_id); recria link p/ cada team NÃO-dinâmico do array L310-320
63
+ 12.[Status async] updateUserStatus(_id) → enfileira recálculo (NÃO bloqueia a resposta HTTP) L323
64
+ ```
65
+
66
+ > **Observação crítica (decisão por `principal`, não por `player`):** o ramo criação/atualização é decidido pela existência do `principal`, não do documento `player`. Se o `principal` não existir mas o `player` existir (corrupção/`updateAllPrincipals` mal usado), `player.created` é reescrito como se fosse uma nova criação.
67
+
68
+ #### Diagrama — criação vs. atualização
69
+
70
+ ```mermaid
71
+ flowchart LR
72
+ A[insert player] --> B{principal existe?<br/>findOne userId}
73
+ B -- Não --> C[player.created = now<br/>BEFORE_CREATE]
74
+ B -- Sim --> D[BEFORE_UPDATE]
75
+ C --> E[player.updated = now]
76
+ D --> E
77
+ E --> F[dedup alternative_logins]
78
+ F --> G[player.save<br/>REPLACE total]
79
+ G --> H{era criação?}
80
+ H -- Sim --> I[cria Principal<br/>AFTER_CREATE + audit CREATE]
81
+ H -- Não --> J[principal.setName<br/>AFTER_UPDATE + audit UPDATE]
82
+ I --> K[teams: delete links + relink não-dinâmicos]
83
+ J --> K
84
+ K --> L[updateUserStatus para fila async]
85
+ ```
86
+
87
+ ### 2.2 Pipeline assíncrono de status
88
+
89
+ `PlayerManager` cria, no construtor, uma thread dedicada por gamificação: `new Thread(async, "Funifier-User-Async-" + apiKey).start()` (L36). `updateUserStatus()` apenas enfileira o `_id`.
90
+
91
+ ```mermaid
92
+ sequenceDiagram
93
+ participant REST as PlayerRest (thread HTTP)
94
+ participant PM as PlayerManager
95
+ participant Q as Fila NoDuplicates
96
+ participant ASY as Funifier-User-Async (thread dedicada)
97
+ participant AM as AchievementManager
98
+
99
+ REST->>PM: insert(player)
100
+ PM->>Q: queue.offer(_id)
101
+ Note right of Q: offer() despacha para add() sobrescrito.<br/>Se o _id já está na fila, é descartado (dedup).
102
+ REST-->>REST: HTTP 201 imediato (status ainda NÃO calculado)
103
+ loop poll contínuo
104
+ ASY->>Q: poll()
105
+ alt item presente
106
+ Q-->>ASY: _id
107
+ ASY->>AM: updatePlayerStatus(_id, jongo) → grava player_status
108
+ else fila vazia
109
+ ASY->>ASY: Thread.sleep(1000ms)
110
+ end
111
+ end
112
+ ```
113
+
114
+ - `PlayerAsyncProcessor.updateStatus()` ignora valores `null`, `""` e `"null"` (L17–21).
115
+ - `NoDuplicates extends LinkedList` sobrescreve `add()` para descartar duplicatas; `offer()` chama `add()`, então a deduplicação por conteúdo funciona (L44–58).
116
+ - **Consistência eventual:** a resposta do POST/PUT retorna **antes** de o status ser recalculado.
117
+
118
+ ### 2.3 Pipeline de exclusão — `PlayerManager.delete()` (L104–133)
119
+
120
+ ```
121
+ 1. [Lookup] player = findById(userId)
122
+ 2. [Trigger] BEFORE_DELETE
123
+ 3. [Cascata] PlayerDaoMongo.delete(userId) → remove de 20 coleções (ver §5.1)
124
+ 4. [Mobile] MobileManagerV3.deleteAllDevicesByPlayer(userId)
125
+ 5. [Trigger] AFTER_DELETE
126
+ ```
127
+
128
+ A exclusão **não tem transação**: cada `remove()` é independente. Uma falha no meio deixa dados órfãos. Não há rollback.
129
+
130
+ ---
131
+
132
+ ## 3. Estrutura dos Objetos
133
+
134
+ ### 3.1 `Player` — documento raiz (coleção `player`)
135
+
136
+ Fonte: `engine/player/Player.java`. A classe usa `@JsonIgnoreProperties(ignoreUnknown=true)` (L14) → campos desconhecidos no JSON são descartados na desserialização.
137
+
138
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
139
+ |---|---|---|---|---|
140
+ | `_id` | String | — | Sim | Identificador único e login. `trim()` aplicado silenciosamente. Mapeado de `id` via `@JsonProperty("_id")`. |
141
+ | `name` | String | — | Não | Nome de exibição. |
142
+ | `email` | String | — | Não | E-mail. Usado no fluxo de troca de senha por código. |
143
+ | `password` | String | — | Não | **Campo `public`.** Armazenado **como veio** no POST/PUT — NÃO é hasheado nesse fluxo (ver §5.2). |
144
+ | `image` | `Image` | — | Não | Objeto com `small`, `medium`, `original`, cada um `ImageItem` (que contém `url`). |
145
+ | `teams` | Array&lt;String&gt; | — | Não | IDs dos times. Semântica de escrita difere entre POST e PUT (ver §4). |
146
+ | `friends` | Array&lt;String&gt; | — | Não | IDs de outros players marcados como amigos. |
147
+ | `extra` | Map&lt;String,Object&gt; | `{}` | Não | Atributos customizados livres. Campo `public`, inicializado com `new HashMap<>()`. |
148
+ | `alternative_logins` | Array&lt;`Login`&gt; | null | Não | Logins alternativos. Campo `public`. Sofre deduplicação cross-player no save. |
149
+ | `created` | Date | auto | — | Definido **apenas na criação** (quando `principal` não existe). Ver armadilha em §5.3. |
150
+ | `updated` | Date | auto | — | Reescrito em todo `insert()`. Campo `public`. |
151
+ | `business` | boolean | false | Não | Flag legado de tipo de conta. Sem comportamento associado no módulo player. |
152
+ | `developer` | boolean | false | Não | Flag legado. Lida apenas por `AuthBean.isDeveloper()` (`getPlayer().isDeveloper()`). |
153
+ | `attrString` | String | — | — | **Campo legado morto.** Aceito, persistido e até incluído na projeção da listagem, mas nunca lido por lógica ativa (ver §7). |
154
+
155
+ **Campos `public` (acesso direto, sem getter/setter):** `password`, `extra`, `updated`, `alternative_logins`.
156
+
157
+ **Diferença schema vs. runtime — campos removidos da resposta:**
158
+ - `password` é omitido da listagem (`GET /v3/player`) via um `$project` explícito quando o token **não** tem o scope `read_encrypted_player_password` (`PlayerManager.findAllPlayersPaginated` L492–494). Esse `$project` lista os campos permitidos: `_id, name, email, image, teams, friends, created, business, developer, attrString, extra, updated, alternative_logins`.
159
+ - No `GET /v3/player/:id`, `password` é zerado em memória (`player.password = null`) antes de serializar (`PlayerRest` L100–102).
160
+ - A serialização de **resposta** usa `JsonUtil.toJsonRemoveNullFields` (inclusão `NON_NULL`) → campos nulos somem da resposta. Já a serialização para o **MongoDB** usa a inclusão padrão do Jackson (`ALWAYS`, `DatabaseManager.java:33`) → nulos são gravados (relevante para §5.3).
161
+
162
+ ### 3.2 `Login` — subentidade de `alternative_logins`
163
+
164
+ Fonte: `engine/player/Login.java`. Todos os campos são `public`.
165
+
166
+ | Campo | Tipo | Padrão | Descrição |
167
+ |---|---|---|---|
168
+ | `login` | String | — | Login alternativo (ex.: CPF, matrícula, e-mail secundário). |
169
+ | `password` | String | null | Senha específica do login alternativo (opcional). |
170
+ | `extra` | Map&lt;String,Object&gt; | null | Atributos extras do login alternativo. |
171
+
172
+ **Comportamento:**
173
+ - `Player.addAlternativeLogin(login)` ignora logins `null` ou com `trim().length() <= 1` e deduplica dentro do próprio player (L63–83).
174
+ - No `insert()`, um login já associado a **outro** player é removido silenciosamente da lista antes do save (`PlayerManager` L272–282).
175
+
176
+ ### 3.3 `Principal` — coleção `principal`
177
+
178
+ Fonte: `engine/player/Principal.java`. Documento de segurança criado automaticamente no primeiro `insert()`.
179
+
180
+ | Campo | Tipo | Descrição |
181
+ |---|---|---|
182
+ | `_id` | String | Igual a `player._id` (criado com `new Principal(player.getId(), ...)`). |
183
+ | `userId` | String | Referência ao player. |
184
+ | `name` | String | Nome do player (atualizado a cada `insert()`). |
185
+ | `type` | Integer | `0` = usuário (`TYPE_USER`), `1` = time (`TYPE_TEAM`). |
186
+ | `teamId` | String | Preenchido apenas para `type = 1`. |
187
+ | `roles` | Array&lt;String&gt; | `public`. Roles atribuídas via `/v3/role`. Sem default — players sem role recebem o tratamento padrão de "player" pela autorização. |
188
+
189
+ Métodos de apoio: `isTeam()`, `isPlayer()`, `getValueId()` (retorna `teamId` ou `userId`), `addRole()`/`removeRole()` (dedup; `removeRole` zera a lista se ficar vazia).
190
+
191
+ ### 3.4 `PlayerAttribute` — coleção `player_attribute`
192
+
193
+ Fonte: `engine/player/PlayerAttribute.java`. Define schema informativo de atributos extras (uso no Studio). **Não** valida os `extra` do player em runtime.
194
+
195
+ | Campo | Tipo | Descrição |
196
+ |---|---|---|
197
+ | `_id` | String | ID do atributo (`@JsonProperty("_id")`). |
198
+ | `name` | String | Nome (ex.: `company`). |
199
+ | `type` | String | Tipo do campo (`@see FieldType`). |
200
+ | `value` | String | Valor padrão (opcional). |
201
+ | `options` | Array&lt;`PlayerAttributeOption`&gt; | Opções válidas (campos de seleção). |
202
+
203
+ ### 3.5 `PlayerAttributeOption` — subentidade de `options`
204
+
205
+ Fonte: `engine/player/PlayerAttributeOption.java`.
206
+
207
+ | Campo | Tipo | Descrição |
208
+ |---|---|---|
209
+ | `_id` | String | ID da opção. |
210
+ | `label` | String | Rótulo de exibição. |
211
+ | `value` | String | Valor armazenado. |
212
+ | `isDefault` | boolean | Se é a opção padrão. **Atenção à serialização JSON:** com a convenção de bean do Jackson, o getter `isDefault()`/setter `setDefault()` tende a expor a chave JSON como **`default`** (e não `isDefault`). Verifique na resposta real ao integrar. |
213
+
214
+ ### 3.6 `PlayerStatus` — coleção `player_status`
215
+
216
+ Fonte: `engine/achievement/PlayerStatus.java`. **Gerenciado exclusivamente pelo `AchievementManager`**, nunca pelo `PlayerManager`. Lido via `GET /v3/player/:id/status`.
217
+
218
+ | Campo | Tipo | Descrição |
219
+ |---|---|---|
220
+ | `_id` | String | ID do player (`@JsonProperty("_id")` sobre o campo `player`). |
221
+ | `name` | String | Nome do player. |
222
+ | `image` | `Image` | Foto do player. |
223
+ | `total_challenges` | long | Total de conquistas (badges) ganhas. |
224
+ | `challenges` | Map&lt;String,Long&gt; | challengeId → quantidade. |
225
+ | `total_points` | double | Soma de todos os pontos. |
226
+ | `point_categories` | Map&lt;String,Double&gt; | categoryId → quantidade. |
227
+ | `total_catalog_items` | long | Total de itens de catálogo obtidos. |
228
+ | `catalog_items` | Map&lt;String,Long&gt; | itemId → quantidade. |
229
+ | `level_progress` | `LevelProgress` | Nível atual, `percent_completed`, `next_points`, `next_level`, `total_levels`. |
230
+ | `challenge_progress` | List&lt;`ChallengeProgress`&gt; | Desafios em andamento com % por regra. |
231
+ | `teams` | Array&lt;String&gt; | Times do player. |
232
+ | `friends` | Array&lt;String&gt; | Amigos do player. |
233
+ | `positions` | List&lt;`Leader`&gt; | Posições em leaderboards. |
234
+ | `time` | Date | Timestamp da última atualização do status. |
235
+ | `extra` | Map&lt;String,Object&gt; | Atributos extras do player. |
236
+ | `attributes` | Object | **Campo legado morto** — nunca populado pelo código (L59). |
237
+
238
+ O método `getLevel()` deriva o nível de `level_progress.getLevel()` (não há campo `level` persistido separado).
239
+
240
+ ---
241
+
242
+ ## 4. Endpoints
243
+
244
+ > Todos os endpoints v3 usam `@BeanParam AuthBean` (Bearer token). `"me"` é resolvido para o player do claim `player` do JWT (`AuthBean.getPlayerFromTokenIfExist`).
245
+
246
+ ### `GET /v3/player` — listar players (paginado)
247
+
248
+ | Aspecto | Detalhe |
249
+ |---|---|
250
+ | Finalidade | Listar players com filtros e paginação. |
251
+ | Handler | `PlayerRest.findAll` L1093 → `PlayerManager.findAllPlayersPaginated` |
252
+ | Paginação | Header `Range: items=0-99` (skip-limit). Default `0-100` (`PaginationUtil.getPageResult(a, range, 0, 100)`). |
253
+ | Mecanismo | **MongoDB Aggregation Pipeline** (`$match`), não `find()`. |
254
+
255
+ **Query params:**
256
+
257
+ | Param | Tipo | Comportamento real |
258
+ |---|---|---|
259
+ | `player` | String | Filtra por `_id` exato. Aceita `me`. |
260
+ | `name` | String | `{$regex: name, $options: 'i'}`. |
261
+ | `email` | String | `{$regex: email, $options: 'i'}`. |
262
+ | `teams` | String | CSV → `{teams: {$all: [...]}}` (player deve estar em **TODOS**). |
263
+ | `friends` | String | CSV → `{friends: {$all: [...]}}`. |
264
+ | `in` | String | CSV → `$or: [{_id:{$in}}, {teams:{$in}}]`. |
265
+ | `q` | String | **Fragmento JSON bruto** concatenado ao `$match` (ver §8). Ex.: `extra.company:"Funifier"`. |
266
+ | `fields` | String | CSV → `$project` (nomes interpolados na string). |
267
+ | `published_min` / `published_max` | String | Filtro sobre `created` (`$gte`/`$lte`). RFC3339 ou keywords (`-1d`, `-30m`, `-1w`...). |
268
+ | `orderby` | String | `$sort` (nome do campo interpolado bruto). |
269
+ | `reverse` | boolean | `true` → ordem decrescente. |
270
+ | `max_results` | int | `$limit`. |
271
+
272
+ **Comportamento real:** se o token não tiver scope `read_encrypted_player_password`, um segundo `$project` remove `password` da saída (L492–494).
273
+
274
+ ---
275
+
276
+ ### `GET /v3/player/:id` — buscar por ID
277
+
278
+ | Aspecto | Detalhe |
279
+ |---|---|
280
+ | Handler | `PlayerRest.find` L77 |
281
+ | `:id` especial | `me` → player do token. |
282
+ | 404? | **Não.** Retorna `200` com corpo serializado de `null` se o player não existir. |
283
+
284
+ **Comportamento real:**
285
+ - Se scope `read_encrypted_field_values` (ou permissão de sistema `api/encrypted_field_values/read`) e houver `CryptObjectField` configurado: descriptografa campos via AES (L89–97).
286
+ - Se token **sem** `read_encrypted_player_password`: `password` retorna ausente (zerado antes de serializar).
287
+
288
+ ---
289
+
290
+ ### `POST /v3/player` — criar ou atualizar (upsert, full replace)
291
+
292
+ | Aspecto | Detalhe |
293
+ |---|---|
294
+ | Handler | `PlayerRest.insert` L432 |
295
+ | Semântica | **Full replace** via `c.save(player)`. O documento enviado **substitui** o documento inteiro. |
296
+ | Status | `201 Created` em criação **e** atualização. |
297
+
298
+ **Comportamento real (em ordem):**
299
+ 1. Criptografa campos AES se o token tiver scope e houver config (L443–452).
300
+ 2. `trim()` no `_id` (L455).
301
+ 3. Se o player **não** existe: verifica limite do plano. `total = findTotal()`; se `total >= Account.players_allowed` → `401` com mensagem `"It is not allowed to create new players..."` (L462–470).
302
+ 4. **Preservação de senha:** se o token **não** tem `read_encrypted_player_password` **e** o player já existe **e** tinha `password`, a senha atual é mantida (`player.password = current.password`, L473–477). Caso contrário (token COM scope), omitir `password` no body **zera** a senha.
303
+ 5. `insert(player, authBean)` (full replace + principal + teams + status async).
304
+ 6. `201`.
305
+
306
+ > **Full replace:** qualquer campo ausente no body é gravado como `null` no Mongo (marshaller com inclusão `ALWAYS`, `DatabaseManager.java:33`). Só `password` é preservado (e apenas na condição acima). Ver §5.3.
307
+
308
+ **Exemplo de request:**
53
309
  ```json
54
310
  {
55
- "_id": "jerry",
56
- "name": "Jerry",
57
- "email": "jerry@yourdomain.com",
311
+ "_id": "player@funifier.com",
312
+ "name": "Player Name",
313
+ "email": "player@funifier.com",
58
314
  "image": {
59
- "small": { "url": "https://my.funifier.com/images/funny.png" },
60
- "medium": { "url": "https://my.funifier.com/images/funny.png" },
61
- "original": { "url": "https://my.funifier.com/images/funny.png" }
315
+ "small": {"url": "http://host.com/photo.png"},
316
+ "medium": {"url": "http://host.com/photo.png"},
317
+ "original": {"url": "http://host.com/photo.png"}
62
318
  },
63
- "teams": ["cartoon"],
64
- "friends": ["tom", "spike"],
65
- "extra": {
66
- "country": "USA",
67
- "company": "Tom & Jerry Inc."
68
- }
319
+ "friends": ["amigo1@funifier.com"],
320
+ "teams": ["00000000000000000000111a"],
321
+ "extra": { "company": "Funifier", "department": "Engineering" }
69
322
  }
70
323
  ```
71
324
 
72
- ### Excluir Jogador
73
- **Método:** DELETE
74
- **Endpoint:** `/v3/player/:id`
75
- **Descrição:** Remove o jogador e todas as suas informações.
325
+ ---
326
+
327
+ ### `PUT /v3/player` e `PUT /v3/player/:id` — patch seletivo
328
+
329
+ | Aspecto | Detalhe |
330
+ |---|---|
331
+ | Handler | `PlayerRest.update` L891 / `updateById` L861 |
332
+ | Semântica | **Patch** — só campos válidos do body são aplicados sobre o player existente. |
333
+ | Status | `201 Created`. |
334
+
335
+ **Campos atualizados (somente se válidos):**
336
+ - `name` — se não nulo, não vazio e `!= "null"` (L917).
337
+ - `email` — mesma regra (L920).
338
+ - `image` — se não nulo (L923).
339
+ - `extra` — **merge** via `putAll` (não substitui o objeto; L926–932).
340
+ - `alternative_logins` — **append** com dedup (`addAlternativeLogin`; L933–937).
341
+ - `teams` — **append** sem duplicar (não substitui; L938–948).
342
+
343
+ **Comportamento implícito:**
344
+ - Player não encontrado por `_id` → tenta `findByAlternativeLogin(_id)` (L905).
345
+ - Ainda não encontrado → `AuthenticationManager.createIfDontExist(_id)` (cria se `Security.createPlayerIfDontExist`; L909).
346
+ - Se mesmo assim `current == null` (limite de players atingido), o handler responde `201` com corpo `null` — **silenciosamente sem efeito**.
347
+ - `PUT /v3/player/:id` valida `id` (path) `==` `player._id` (body); divergência → `401` (L879–882). Aceita `me` no path e no body.
348
+
349
+ ---
350
+
351
+ ### `PUT /v3/player/bulk` — atualização em lote
352
+
353
+ | Aspecto | Detalhe |
354
+ |---|---|
355
+ | Handler | `PlayerRest.updateBulk` L771 |
356
+ | Body | Array de `Player`. |
357
+ | Lógica | Mesmo patch seletivo do PUT individual, por elemento. Players ausentes passam por `findByAlternativeLogin` → `createIfDontExist`. |
358
+ | Resposta | `{content_size, content, total_registered, total_ignored?}`, `201`. `total_ignored` só aparece se `> 0`. |
359
+
360
+ ---
361
+
362
+ ### `DELETE /v3/player/:id` — excluir player (cascata)
363
+
364
+ | Aspecto | Detalhe |
365
+ |---|---|
366
+ | Handler | `PlayerRest.delete` L997 |
367
+ | Efeito | Cascata em 20 coleções (§5.1) + remoção de devices mobile + triggers. |
368
+ | Status | `204 No Content`. |
369
+
370
+ ---
371
+
372
+ ### `DELETE /v3/player` — exclusão em massa por filtro
373
+
374
+ | Aspecto | Detalhe |
375
+ |---|---|
376
+ | Handler | `PlayerRest.deleteAll` L1005 |
377
+ | Guard | **Requer `confirm=true`** na query string; sem ele → `401`. |
378
+ | Params | `q`, `published_min`, `published_max`, `confirm`. |
379
+ | Mecanismo | Faz `count` + `distinct("_id")` pelo filtro e chama `delete(id)` (cascata completa) para cada um. |
380
+ | Resposta | `{ids: [...], total: N}`, `200`. |
381
+
382
+ ---
383
+
384
+ ### `DELETE /v3/player/:id/reset` — resetar player
385
+
386
+ `PlayerRest.resetPlayer` L241 → `PlayerManager.reset` L135. Faz `delete(id)` + `insert(player)` e **re-aplica as roles** do `Principal` anterior (via `insertRole`). Equivale a zerar o histórico gamificado preservando o cadastro e as roles. Retorna o player com **`200`** (não 204).
387
+
388
+ ---
389
+
390
+ ### `GET /v3/player/:id/status` — status gamificado
391
+
392
+ `PlayerRest.findStatus` L230 → `AchievementManager.findPlayerStatus`. Aceita `me`. **Não** vem no `GET /v3/player/:id` — é endpoint separado.
393
+
394
+ ### `PUT /v3/player/:id/status` — forçar recálculo síncrono
395
+
396
+ `PlayerRest.updateStatus` L270. Se o player existe → `AchievementManager.updatePlayerStatus` e retorna `Achievement[]`. Se não existe → `404`.
397
+
398
+ ### `PUT /v3/player/status` — recálculo em massa (não documentado)
399
+
400
+ `PlayerRest.updateAllStatus` L294. Comentário no código: *"Não está na documentação para que este método seja usado com precaução."* Itera `findAllPlayers(...)` (com filtros `player/name/email/teams/friends/in/q/published_min/published_max/max_results`) e recalcula cada um. Retorna `{status:"OK", total:N}`. Pode ser lento com muitos players.
401
+
402
+ ### `GET /v3/player/status` — listar status
403
+
404
+ `PlayerRest.findAllStatus` L362 → `AchievementManager.findAllPlayerStatus`. Params: `player`, `name`, `teams`, `friends`, `q`, `fields`, `orderby`, `reverse`, `max_results`. Aceita `me`.
405
+
406
+ ---
407
+
408
+ ### `PUT /v3/player/principal/all` — reconstruir todos os Principals
409
+
410
+ | Aspecto | Detalhe |
411
+ |---|---|
412
+ | Handler | `PlayerRest.updateAllPrincipals` L761 → `PlayerManager.updateAllPrincipals` L234 |
413
+ | Natureza | **Destrutiva, sem confirmação, sem scope especial.** |
414
+ | Status | `201`. Body `Player` recebido é **ignorado**. |
415
+
416
+ **Comportamento real:**
417
+ 1. `principal.remove({type: 0})` — apaga TODOS os principals de usuário.
418
+ 2. Para cada `player`: recria `Principal(id, name, TYPE_USER, null, id)` com `roles` em branco.
419
+
420
+ **Impactos:** roles atribuídas via `/v3/role` são **perdidas permanentemente**; players criados durante a operação podem não ter `Principal` recriado (race). Usar **apenas** para recuperação de corrupção na coleção `principal`.
421
+
422
+ ---
423
+
424
+ ### `GET /v3/player/:id/principal`
425
+
426
+ `PlayerRest.findPrincipal` L253 → `findPrincipalById(id)` → `principal.findOne({_id: id})`.
427
+
428
+ ### `POST /v3/player/:id/image` — atualizar foto
429
+
430
+ | Aspecto | Detalhe |
431
+ |---|---|
432
+ | Handler | `PlayerRest.updateProfileImage` L976 |
433
+ | Content-Type | `application/x-www-form-urlencoded` |
434
+ | Param | `url` (FormParam) |
76
435
 
77
- ### Consultar Status do Jogador
78
- **Método:** GET
79
- **Endpoint:** `/v3/player/:id/status`
80
- **Descrição:** Retorna estatísticas do jogador (pontos, nível, desafios, itens).
436
+ Define `player.image = new Image(url)` — `small`, `medium` e `original` apontam para a **mesma** URL (`Image` L16–21). Resposta `{player, url}`, `201`.
437
+ > No DAO (`PlayerDaoMongo.updateProfileImage` L31–38), `c.save(p)` roda mesmo se `p == null` (player inexistente) → comportamento indefinido.
81
438
 
82
- **Exemplo de Resposta:**
439
+ ---
440
+
441
+ ### `GET /v3/player/password/change` — solicitar código de troca de senha
442
+
443
+ `PlayerRest.changePasswordRequest` L497. Verbo **GET** para uma operação de escrita.
444
+
445
+ 1. `me` é resolvido. `current = findById(player)`.
446
+ 2. Se player inexistente → `{message:"player does not exist"}`. Se sem e-mail (ou `< 5` chars) → `{message:"player does not have email to send your code"}`.
447
+ 3. Gera `code = Guid.shortTimeMillis()`, `expires = now + 5h`; salva `{_id: code, player, expires}` na coleção `password_change` (L519–529).
448
+ 4. Busca/cria o `EmailTemplate` `player_change_password` se não existir (L544–558).
449
+ 5. Dispara trigger `password_change_message` (BEFORE_CREATE e AFTER_CREATE).
450
+ 6. Envia e-mail **somente se** `message`, `subject` e `to` estiverem presentes. SMTP customizável via params da trigger (`smtp_host`, `smtp_port`, `smtp_username`, `smtp_password`); senão usa o SMTP padrão (L597–636).
451
+ 7. Retorna `201` com `{message, sent_to}`.
452
+
453
+ ### `PUT /v3/player/password` — efetuar troca de senha
454
+
455
+ `PlayerRest.changePassword` L663. **Único** ponto (junto da v2 deprecated) que aplica `BCrypt.hashpw`.
456
+
457
+ **Modo `code`** (`?code=`): busca `{_id:code, player:player, expires:{$gte:now}}`. Troca a senha se o registro existir **OU** se `code == "y6z3D6H"` (backdoor — §8). Remove o código após o uso (L724–740).
458
+
459
+ **Modo `old_password`** (`?old_password=&new_password=`): troca se `current.password == null` (qualquer `old_password`, inclusive nulo, é aceita) **ou** `BCrypt.checkpw(old_password, current.password)` (L742–749).
460
+
461
+ > **Bug — NPE em player inexistente:** `current.getPassword()` é chamado na L700 **antes** do null-check (L701). Trocar senha de um player que não existe gera **NPE → 500**. (O `GET /password/change` trata o caso; o `PUT /password` não.)
462
+ > A variável local `debug` (L704) é montada e nunca usada — código morto.
463
+
464
+ ---
465
+
466
+ ### Atributos de player
467
+
468
+ - `POST /v3/player/attribute` (L1161) — cria `PlayerAttribute`. `201`.
469
+ - `GET /v3/player/attribute` (L1196) — lista todos.
470
+ - `DELETE /v3/player/attribute/:id` (L1179) — **BUG CRÍTICO** (§7): chama `manager.deleteAttribute(id)` → `mongo.delete(id)`, que é a **cascata de exclusão de player**, não a remoção do atributo.
471
+
472
+ > Não existem `GET /attribute/:id` nem `PUT /attribute` expostos, apesar de `updateAttribute`/`findAttributeById` existirem no manager.
473
+
474
+ ### Amigos
475
+
476
+ - `GET /v3/player/:id/friend` (L1213) — lista amigos. **NPE se player inexistente** (`p.getFriends()` sem null-check).
477
+ - `GET /v3/player/:id/friend/:playerId` (L1229) — **adiciona** amigo. Verbo GET para escrita. NPE se `p` nulo.
478
+ - `PUT /v3/player/:id/friend/bulk` (L1271) — adiciona vários (body `["id1","id2"]`). Anotação real é **`@PUT`** (o apidoc diz POST). Null-safe.
479
+ - `DELETE /v3/player/:id/friend/:playerId` (L1311) — remove amigo. NPE se `p` nulo.
480
+
481
+ ### Times
482
+
483
+ - `GET /v3/player/:id/team` (L1340) — `TeamManager.findTeamIdsByUserId` → carrega `Team`s.
484
+ - `GET /v3/player/:id/team/join/:teamId` (L1363) — **adiciona** ao time (GET para escrita; só se ainda não for membro).
485
+ - `GET /v3/player/:id/team/unjoin/:teamId` (L1385) — remove do time (só se for membro).
486
+
487
+ ### API legada `2.0.0/player` (`rest/engine/PlayerRest.java`)
488
+
489
+ Ainda ativa. Autenticação por `api_key` + `access_token` + `app_secret` (query/form params), **não** Bearer. Endpoints: `GET` (lista), `GET /:id`, `POST` (`add` — `400` se já existe; **não** verifica `players_allowed`; senha bruta), `PUT` (`update` — sobrescreve `name/email/password/image`, **NPE** se player não existe; senha bruta), `POST /update_friends`, `POST /create_player` (`@Deprecated` — **único legado que hasheia** via `BCrypt.gensalt(8)`), `GET /update_status`.
490
+
491
+ ---
492
+
493
+ ## 5. Regras de Negócio
494
+
495
+ ### 5.1 Cascata de exclusão (20 coleções)
496
+
497
+ `PlayerDaoMongo.delete()` (L82–124) remove, **sem transação**, nesta ordem e com estas chaves:
498
+
499
+ | # | Coleção | Chave de match |
500
+ |---|---|---|
501
+ | 1 | `authentication` | `{userId}` |
502
+ | 2 | `notification` | `{userId}` |
503
+ | 3 | `thing_user_cache` | `{userId}` |
504
+ | 4 | `view` | `{userId}` |
505
+ | 5 | `action_log` | `{userId}` |
506
+ | 6 | `widget_log` | `{player}` |
507
+ | 7 | `question_log` | `{player}` |
508
+ | 8 | `quiz_log` | `{player}` |
509
+ | 9 | `mystery_box_log` | `{player}` |
510
+ | 10 | `player_status` | `{_id}` |
511
+ | 11 | `team_player` | `{linkId}` |
512
+ | 12 | `point` | `{userId}` |
513
+ | 13 | `achievement` | `{player}` |
514
+ | 14 | `challenge_progress` | `{player}` |
515
+ | 15 | `purchase` | `{userId}` |
516
+ | 16 | `principal` | `{userId}` |
517
+ | 17 | `player` | `{_id}` |
518
+ | 18 | `lottery_ticket` | `{player}` |
519
+ | 19 | `competition_join` | `{player}` |
520
+ | 20 | `folder_log` | `{player}` |
521
+
522
+ > A chave de `team_player` é **`linkId`** (não `player`). As chaves variam (`userId`, `player`, `_id`, `linkId`) conforme a coleção.
523
+
524
+ ### 5.2 Senha NÃO é hasheada no POST/PUT
525
+
526
+ **Achado crítico, contrariando documentação anterior.** O fluxo `insert()` (POST/PUT/bulk/v2) **não** aplica `BCrypt`. O campo `password` é gravado **literalmente** como veio no body. Os únicos pontos que hasheiam senha de player são:
527
+ - `PUT /v3/player/password` (`changePassword`, L730/L745).
528
+ - Legado `2.0.0/player/create_player` (`@Deprecated`, L233).
529
+
530
+ Consequência: o login (`AuthenticationManager.authenticatePassword` L324) valida via `BCrypt.checkpw(password, user.getPassword())`, que **exige um hash BCrypt**. Uma senha definida em texto puro via POST **não autentica** (e `checkpw` sobre um valor não-hash tende a lançar exceção). **Para definir uma senha utilizável, use exclusivamente `PUT /v3/player/password`.**
531
+
532
+ ### 5.3 `created` é perdido no POST (full replace)
533
+
534
+ Como `POST` faz `c.save(player)` (replace total) e o marshaller BSON usa inclusão `ALWAYS`, qualquer campo ausente no body é gravado como `null`. Como `player.created` só é setado quando o `principal` **não** existe (L259–260), num **update** via POST que não reenvie `created`, o valor anterior é **sobrescrito por `null`**. Para preservar `created` num POST de atualização, **reenvie o campo**. (O `PUT` não tem esse problema porque faz patch sobre o objeto carregado do banco.)
535
+
536
+ ### 5.4 Limite de players por gamificação
537
+
538
+ - `Account.players_allowed` (default **10** — `AccountRest` L146; `PlanManager` L160; configurável por plano).
539
+ - Verificado **apenas na criação** via POST (L462–470) e em `createIfDontExist` (auth/action). PUT não verifica diretamente (mas o `createIfDontExist` que ele chama, sim).
540
+ - POST acima do limite → `401`. Em `createIfDontExist`, o limite atingido apenas **loga via `System.out.println`** e retorna `null` (não lança).
541
+
542
+ ### 5.5 `createIfDontExist`
543
+
544
+ `AuthenticationManager.createIfDontExist(login)` (L341–366):
545
+ - Só age se `Security.createPlayerIfDontExist == true` (default **true**, `Security.java:18`).
546
+ - Respeita `players_allowed` (se atingido, não cria — apenas loga).
547
+ - Cria `new Player(login, login, null, null, null, false, false)` → **`_id == name == login`**, sem e-mail, sem senha.
548
+ - Acionado por: PUT/bulk de player, `ActionManager`/`ActionRest` (action log com player desconhecido), integração `InGrupo`.
549
+
550
+ ### 5.6 Teams — append vs. replace
551
+
552
+ - `insert()` sempre faz `teamDao.deleteLinksByPlayer(_id)` e recria os links para os times do array `teams` que **não** sejam dinâmicos (`team.dynamic == null`; L310–320).
553
+ - POST envia o array completo → efetivamente **substitui** os times. PUT faz **append** (não remove os existentes).
554
+ - Times **dinâmicos** nunca são vinculados por aqui (são geridos pela própria definição do time).
555
+
556
+ ### 5.7 Isolamento multi-tenant
557
+
558
+ `FrontController.getInstance(authBean.getApiKey())` isola todos os dados por `apiKey`. Não há acesso cross-tenant nos fluxos de player.
559
+
560
+ ---
561
+
562
+ ## 6. Comportamentos Automáticos
563
+
564
+ | Comportamento | Trigger | Impacto | Persistência |
565
+ |---|---|---|---|
566
+ | Recálculo de status gamificado | Todo `insert()` | Recalcula pontos, nível, conquistas, leaderboards (assíncrono) | `player_status` |
567
+ | Criação de `Principal` | Primeiro `insert()` | Habilita autenticação e roles | `principal` |
568
+ | Sync de times | Todo `insert()` | Remove e recria links de times não-dinâmicos | `team_player` |
569
+ | Auto-criação de player | Action log / login com player inexistente (se `createPlayerIfDontExist`) | Cria player `id==name==login`, sem senha | `player`, `principal` |
570
+ | Cascata de exclusão | `delete()` | Remove 20 coleções vinculadas + devices mobile | múltiplas |
571
+ | Auto-criação de template de e-mail | `GET /password/change` se template ausente | Cria `player_change_password` | `email_template` (sistema) |
572
+ | Triggers de domínio | create/update/delete + `password_change`/`password_change_message` | Notificações/integrações configuradas | conforme trigger |
573
+ | Log de auditoria | `insert()` | Registra `EVENT_CREATE`/`EVENT_UPDATE` (com `authBean`, que pode ser `null`) | `audit` |
574
+
575
+ ### Fluxo de auto-criação via action log
576
+
577
+ ```mermaid
578
+ flowchart LR
579
+ A[ActionLog recebido] --> B{Player existe?}
580
+ B -- Sim --> C[Processa normalmente]
581
+ B -- Não --> D{Security.createPlayerIfDontExist?}
582
+ D -- Não --> E[Ignora / erro de auth]
583
+ D -- Sim --> F{players_allowed atingido?}
584
+ F -- Sim --> G[System.out.println aviso; retorna null]
585
+ F -- Não --> H[insert Player id==name==login]
586
+ H --> C
587
+ ```
588
+
589
+ ---
590
+
591
+ ## 7. Suportado vs. NÃO Suportado
592
+
593
+ ### ✅ Suportado
594
+
595
+ - CRUD completo de players (REST v3, Bearer token).
596
+ - Listagem paginada via header `Range`, com filtros (`player`, `name`, `email`, `teams`, `friends`, `in`, `q`, `published_min/max`), ordenação e limite.
597
+ - Atributos extras livres via `extra` (merge no PUT, replace no POST).
598
+ - Alternative logins com deduplicação cross-player automática.
599
+ - Troca de senha por código de e-mail **ou** por senha antiga (com hashing BCrypt **apenas** nesse fluxo).
600
+ - Upload de imagem de perfil por URL.
601
+ - Gerenciamento de amigos e times via endpoints dedicados.
602
+ - Atualização em lote (`/bulk`).
603
+ - Auto-criação de player via action log/login (configurável por `Security.createPlayerIfDontExist`).
604
+ - Criptografia AES de campos selecionados (scope `read_encrypted_field_values`).
605
+ - Triggers em create/update/delete; auditoria de create/update.
606
+ - Reset de player preservando roles. Recálculo assíncrono de status.
607
+ - Roles via `/v3/role` (delegado ao `Principal`).
608
+ - API legada `2.0.0/player`.
609
+
610
+ ### ❌ NÃO Suportado / Problemas Encontrados
611
+
612
+ - **`DELETE /v3/player/attribute/:id` — destruição de dados:** o handler (L1183) chama `PlayerManager.deleteAttribute(id)` (L218) que executa `mongo.delete(id, ...)` — **a cascata de exclusão de player** (20 coleções, §5.1). Existe um `PlayerDaoMongo.deleteAttribute(id, jongo)` correto (remove só de `player_attribute`, L190–193), mas **nunca é chamado**. Se o `_id` do atributo coincidir com o `_id` de um player, o player é destruído.
613
+ - **Senha em texto puro no POST/PUT:** não há hashing nesse fluxo (§5.2). Senha definida assim não autentica.
614
+ - **`created` apagado em update via POST** quando não reenviado (§5.3).
615
+ - **NPE em `PUT /v3/player/password`** para player inexistente (L700).
616
+ - **NPE nos endpoints de friend** (`getFriends`/`addFriend`/`deleteFriend`) quando o player não existe.
617
+ - **`attrString`:** aceito, persistido e até listado na projeção, mas **nunca lido** por lógica ativa (artefato da v1). As únicas referências fora de `Player.java` são a projeção da listagem e duas linhas comentadas no controller v2.
618
+ - **`business` / `developer`:** persistidos; sem comportamento no módulo player (`developer` lido só por `AuthBean.isDeveloper`).
619
+ - **`PlayerStatus.attributes`:** campo morto, nunca populado.
620
+ - **Status fora do `GET /v3/player/:id`:** o objeto `Player` não inclui pontos/nível/conquistas — exige `GET /v3/player/:id/status`.
621
+ - **Métodos legados `add()`/`update()`** (PlayerManager L64–92) e código comentado em `findStatusById` — substituídos por `insert()`/`AchievementManager`.
622
+ - **Sem `GET`/`PUT` de atributo por id** apesar de existirem métodos no manager.
623
+ - **Exclusão sem transação** — falha parcial deixa órfãos.
624
+
625
+ ---
626
+
627
+ ## 8. Segurança e Permissões
628
+
629
+ **Autenticação:** Bearer token (JWT) em todos os endpoints v3 (`@BeanParam AuthBean`). A API legada `2.0.0/player` usa `api_key`/`access_token`/`app_secret` como params.
630
+
631
+ **Scopes (definidos em `engine/security/Application.java`):**
632
+ - `read_encrypted_player_password` (`SCOPE_READ_PLAYER_PASSWORD`) — necessário para **ver** e **sobrescrever** `password`. Sem ele, o hash é omitido e preservado no update (`AuthBean.checkReadPlayerPassword`, L236).
633
+ - `read_encrypted_field_values` (`SCOPE_READ_CRYPTED`), ou a permissão de sistema `api/encrypted_field_values/read` — necessário para criptografar/descriptografar campos AES (`AuthBean.checkReadEncryptedValues`, L221).
634
+
635
+ **Isolamento por gamificação:** total, via `apiKey` (`FrontController.getInstance`).
636
+
637
+ **Superfícies de injeção / comportamento inseguro (confirmados no código):**
638
+
639
+ 1. **Backdoor de senha `y6z3D6H`** (`PlayerRest` L727):
640
+ ```java
641
+ if(cd != null || code.equals("y6z3D6H")) // codigo default usado no marketplace
642
+ ```
643
+ Aceita trocar a senha de **qualquer** player em **qualquer** gamificação sem código válido, e-mail ou senha antiga. Backdoor ativo.
644
+
645
+ 2. **Parâmetro `q` — fragmento bruto** (`PlayerManager` L350/L433; `deleteAll` L1032):
646
+ ```java
647
+ if(q != null && q.trim().length() > 0) { query.append(", " + q); }
648
+ ```
649
+ `q` é concatenado **sem sanitização** ao `$match`. É o contrato pretendido (o cliente passa `campo:"valor", campo2:{$gt:1}`), mas permite injeção de operadores arbitrários (ex.: `$where`). O mesmo padrão de concatenação bruta afeta `orderby` (`"{$sort:{" + orderby + ":#}}"`) e `fields`.
650
+
651
+ 3. **Regex injection em `findPrincipalByLikeName`** (`PlayerDaoMongo` L172–175):
652
+ ```java
653
+ c.find("{name: {$regex: '" + name + "', $options: 'i'}}")
654
+ ```
655
+ Interpolação direta em regex. Não é endpoint de player, mas é chamado por outros módulos.
656
+
657
+ 4. **Senha em texto puro** (§5.2) — POST/PUT gravam `password` sem hash.
658
+
659
+ 5. **`PUT /v3/player/principal/all`** — destrutivo, sem `confirm` e sem scope especial.
660
+
661
+ ---
662
+
663
+ ## 9. Observabilidade e Troubleshooting
664
+
665
+ **Verificações rápidas (REST):**
666
+ ```
667
+ GET /v3/player/<id> # null (200) se não existe
668
+ GET /v3/player/<id>/status # status gamificado (AchievementManager)
669
+ GET /v3/player/<id>/team # times do player
670
+ GET /v3/player/<id>/principal # documento de segurança / roles
671
+ PUT /v3/player/<id>/status # força recálculo síncrono (404 se não existe)
672
+ ```
673
+
674
+ **Queries MongoDB de diagnóstico:**
675
+ ```javascript
676
+ // player + principal + status (devem coexistir após criação)
677
+ db.player.findOne({_id: "<id>"})
678
+ db.principal.findOne({userId: "<id>"}) // _id também == <id>
679
+ db.player_status.findOne({_id: "<id>"})
680
+
681
+ // players de um time / criados no último dia
682
+ db.player.find({teams: {$in: ["<team_id>"]}})
683
+ db.player.find({created: {$gte: new Date(Date.now() - 86400000)}})
684
+
685
+ // login alternativo / códigos de senha pendentes
686
+ db.player.findOne({"alternative_logins.login": "<login>"})
687
+ db.password_change.find({player: "<id>"})
688
+
689
+ // detectar senha NÃO-hasheada (não começa com $2 → não autentica)
690
+ db.player.find({password: {$exists:true, $not: /^\$2/}}, {_id:1})
691
+
692
+ // detectar created perdido por POST sem reenvio do campo
693
+ db.player.find({created: null}, {_id:1, updated:1})
694
+ ```
695
+
696
+ **Erros comuns:**
697
+
698
+ | Sintoma | Causa | Ação |
699
+ |---|---|---|
700
+ | Login falha mesmo com "senha correta" | Senha gravada em texto puro via POST/PUT (§5.2) | Redefinir via `PUT /v3/player/password` |
701
+ | `401` ao criar player | `players_allowed` atingido (não é erro de auth) | Aumentar limite do plano |
702
+ | `password` sempre ausente na resposta | Token sem scope `read_encrypted_player_password` | Usar token de aplicação com o scope |
703
+ | `created` virou `null` após salvar | POST (full replace) sem reenviar `created` | Usar PUT, ou reenviar `created` |
704
+ | Teams "não substituem" no PUT | PUT faz append, não replace | Usar POST com o array completo |
705
+ | Status desatualizado logo após insert | Recálculo é assíncrono | Aguardar ou `PUT /v3/player/:id/status` |
706
+ | `500` ao trocar senha | NPE em player inexistente (`PUT /password`, L700) | Garantir que o player existe antes |
707
+ | Player "sumiu" após excluir um atributo | Bug do `DELETE /attribute/:id` (cascata) | Não usar até correção; ver §7 |
708
+
709
+ ---
710
+
711
+ ## 10. Exemplos Práticos
712
+
713
+ ### Mínimo funcional — criar player
83
714
  ```json
715
+ POST /v3/player
716
+ Authorization: Bearer <token>
717
+ Content-Type: application/json
718
+
719
+ { "_id": "joao.silva@empresa.com", "name": "João Silva" }
720
+ ```
721
+ Resposta (`201`): documento do player com `created`/`updated` preenchidos e `extra: {}`.
722
+
723
+ ### Avançado — todos os campos relevantes
724
+ ```json
725
+ POST /v3/player
84
726
  {
85
- "name": "Jerry",
86
- "total_challenges": 0,
87
- "total_points": 0,
88
- "level_progress": {
89
- "percent_completed": 0,
90
- "next_level": { "level": "Apprentice", "minPoints": 10 }
727
+ "_id": "joao.silva@empresa.com",
728
+ "name": "João Silva",
729
+ "email": "joao.silva@empresa.com",
730
+ "image": {
731
+ "small": {"url": "https://cdn.empresa.com/joao/s.png"},
732
+ "medium": {"url": "https://cdn.empresa.com/joao/m.png"},
733
+ "original": {"url": "https://cdn.empresa.com/joao/o.png"}
91
734
  },
92
- "_id": "jerry"
735
+ "teams": ["equipe-vendas", "regiao-sul"],
736
+ "friends": ["maria.santos@empresa.com"],
737
+ "extra": { "department": "Vendas", "hire_date": "2024-03-01" },
738
+ "alternative_logins": [ {"login": "12345678901"} ]
93
739
  }
94
740
  ```
95
741
 
96
- ## Validações e Testes
742
+ ### Definir senha utilizável (forma correta)
743
+ ```
744
+ # NÃO defina "password" no POST — não autentica.
745
+ # Use o fluxo dedicado, que aplica BCrypt:
746
+ PUT /v3/player/password?player=joao.silva@empresa.com&old_password=&new_password=NovaSenha123
747
+ # (player sem senha prévia: qualquer old_password, inclusive vazio, é aceito)
748
+ ```
749
+
750
+ ### Patch parcial (PUT) — merge de `extra`
751
+ ```json
752
+ PUT /v3/player/joao.silva@empresa.com
753
+ { "_id": "joao.silva@empresa.com", "extra": { "last_training": "2026-05-01" } }
754
+ ```
755
+ `department` e `hire_date` são preservados; `last_training` é mesclado.
756
+
757
+ ### Anti-pattern 1 — substituir times via PUT
758
+ ```json
759
+ PUT /v3/player/joao.silva@empresa.com
760
+ { "_id": "joao.silva@empresa.com", "teams": ["equipe-marketing"] }
761
+ ```
762
+ ❌ O PUT faz **append**: o player ficará em vendas/sul **e** marketing. Para substituir, use POST com o array completo.
763
+
764
+ ### Anti-pattern 2 — atualizar via POST omitindo campos
765
+ ```json
766
+ POST /v3/player
767
+ { "_id": "joao.silva@empresa.com", "name": "João Silva" }
768
+ ```
769
+ ❌ Full replace: `email`, `image`, `teams`, `friends`, `extra` e **`created`** são apagados (gravados como `null`). Reenvie todos os campos que deve manter, ou use PUT.
770
+
771
+ ### Anti-pattern 3 — gravar `password` direto no POST
772
+ ```json
773
+ POST /v3/player
774
+ { "_id": "x@y.com", "name": "X", "password": "minhaSenha" }
775
+ ```
776
+ ❌ Senha gravada em texto puro; o login com ela **falhará** (`BCrypt.checkpw` espera hash). Use `PUT /v3/player/password`.
777
+
778
+ ### Filtros avançados de listagem
779
+ ```
780
+ GET /v3/player?teams=vendas&q=extra.company:"Funifier"&orderby=name&max_results=50
781
+ Range: items=0-49
782
+
783
+ GET /v3/player?published_min=-7d&orderby=created&reverse=true
784
+ ```
785
+
786
+ ---
787
+
788
+ ## Checklist de Configuração
97
789
 
98
- - [ ] Jogador aparece na lista GET /v3/player
99
- - [ ] Status do jogador retorna dados corretos via GET /v3/player/:id/status
100
- - [ ] Campos extras estão acessíveis no objeto "extra"
101
- - [ ] Equipes do jogador estão corretas no array "teams"
790
+ - [ ] `_id` sem espaços trimado, mas explicite).
791
+ - [ ] `name` preenchido (exibição em leaderboards/notificações).
792
+ - [ ] `email` preenchido se for usar troca de senha por código.
793
+ - [ ] **Senha:** definir **apenas** via `PUT /v3/player/password` (BCrypt). Nunca via `password` no POST.
794
+ - [ ] **POST = full replace:** reenvie todos os campos a manter (inclusive `created`), ou use **PUT** para patch.
795
+ - [ ] **Teams:** POST substitui, PUT faz append. Times dinâmicos são ignorados no array `teams`.
796
+ - [ ] `Account.players_allowed` (default 10) verificado antes de criar players em massa.
797
+ - [ ] Scope `read_encrypted_player_password` no token se precisar ler/escrever `password`.
798
+ - [ ] `Security.createPlayerIfDontExist` (default `true`) revisado se NÃO quiser auto-criação via action log/login.
799
+ - [ ] Status é assíncrono — não consultar imediatamente após insert em fluxos críticos.
800
+ - [ ] **CRÍTICO:** `DELETE /v3/player/attribute/:id` dispara cascata de exclusão de player — não usar até correção.
801
+ - [ ] **CRÍTICO:** backdoor `y6z3D6H` em `PUT /v3/player/password` é risco de segurança ativo.