funifier-mcp 0.2.25 → 0.2.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/.cursor/rules/funifier.mdc +38 -41
  2. package/.github/copilot-instructions.md +38 -41
  3. package/AGENTS.md +56 -49
  4. package/README.md +40 -22
  5. package/datasource-funifier-docs/.coverage.json +326 -0
  6. package/datasource-funifier-docs/.validation.json +593 -0
  7. package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
  8. package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
  9. package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
  10. package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
  11. package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
  12. package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
  13. package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
  14. package/datasource-funifier-docs/knowledge/index.md +5 -2
  15. package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
  16. package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
  17. package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
  18. package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
  19. package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
  20. package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
  21. package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
  22. package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
  23. package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
  24. package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
  25. package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
  26. package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
  27. package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
  28. package/datasource-funifier-docs/knowledge/modules/folder.md +1011 -77
  29. package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
  30. package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
  31. package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
  32. package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
  33. package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
  34. package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
  35. package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
  36. package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
  37. package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
  38. package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
  39. package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
  40. package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
  41. package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
  42. package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
  43. package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
  44. package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
  45. package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
  46. package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
  47. package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
  48. package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
  49. package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
  50. package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
  51. package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
  52. package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
  53. package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
  54. package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
  55. package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
  56. package/dist/cli/init.d.ts.map +1 -1
  57. package/dist/cli/init.js +42 -1
  58. package/dist/cli/init.js.map +1 -1
  59. package/dist/cli/init.test.js +74 -3
  60. package/dist/cli/init.test.js.map +1 -1
  61. package/dist/cli/persona.d.ts +3 -0
  62. package/dist/cli/persona.d.ts.map +1 -0
  63. package/dist/cli/persona.js +25 -0
  64. package/dist/cli/persona.js.map +1 -0
  65. package/dist/core/api-client.d.ts +21 -1
  66. package/dist/core/api-client.d.ts.map +1 -1
  67. package/dist/core/api-client.js +154 -1
  68. package/dist/core/api-client.js.map +1 -1
  69. package/dist/core/constants.d.ts +14 -0
  70. package/dist/core/constants.d.ts.map +1 -1
  71. package/dist/core/constants.js +14 -0
  72. package/dist/core/constants.js.map +1 -1
  73. package/dist/core/types/Folder.d.ts +16 -0
  74. package/dist/core/types/Folder.d.ts.map +1 -0
  75. package/dist/core/types/Folder.js +3 -0
  76. package/dist/core/types/Folder.js.map +1 -0
  77. package/dist/core/types/FolderContent.d.ts +10 -0
  78. package/dist/core/types/FolderContent.d.ts.map +1 -0
  79. package/dist/core/types/FolderContent.js +3 -0
  80. package/dist/core/types/FolderContent.js.map +1 -0
  81. package/dist/core/types/FolderContentType.d.ts +10 -0
  82. package/dist/core/types/FolderContentType.d.ts.map +1 -0
  83. package/dist/core/types/FolderContentType.js +3 -0
  84. package/dist/core/types/FolderContentType.js.map +1 -0
  85. package/dist/core/types/FolderLog.d.ts +11 -0
  86. package/dist/core/types/FolderLog.d.ts.map +1 -0
  87. package/dist/core/types/FolderLog.js +3 -0
  88. package/dist/core/types/FolderLog.js.map +1 -0
  89. package/dist/core/types/index.d.ts +4 -0
  90. package/dist/core/types/index.d.ts.map +1 -1
  91. package/dist/core/types/index.js +4 -0
  92. package/dist/core/types/index.js.map +1 -1
  93. package/dist/mcp/bundle.js +121 -87
  94. package/dist/mcp/check-update.d.ts +2 -0
  95. package/dist/mcp/check-update.d.ts.map +1 -0
  96. package/dist/mcp/check-update.js +44 -0
  97. package/dist/mcp/check-update.js.map +1 -0
  98. package/dist/mcp/index.js +5 -2
  99. package/dist/mcp/index.js.map +1 -1
  100. package/dist/mcp/resources/documentation.d.ts +1 -1
  101. package/dist/mcp/resources/documentation.d.ts.map +1 -1
  102. package/dist/mcp/resources/documentation.js +39 -3
  103. package/dist/mcp/resources/documentation.js.map +1 -1
  104. package/dist/mcp/tools/_char-guard.js +1 -1
  105. package/dist/mcp/tools/_char-guard.js.map +1 -1
  106. package/dist/mcp/tools/_fetch-current.d.ts +1 -1
  107. package/dist/mcp/tools/_fetch-current.d.ts.map +1 -1
  108. package/dist/mcp/tools/_fetch-current.js +12 -0
  109. package/dist/mcp/tools/_fetch-current.js.map +1 -1
  110. package/dist/mcp/tools/connect.d.ts.map +1 -1
  111. package/dist/mcp/tools/connect.js +18 -8
  112. package/dist/mcp/tools/connect.js.map +1 -1
  113. package/dist/mcp/tools/database.d.ts.map +1 -1
  114. package/dist/mcp/tools/database.js +59 -47
  115. package/dist/mcp/tools/database.js.map +1 -1
  116. package/dist/mcp/tools/database.test.js +2 -2
  117. package/dist/mcp/tools/database.test.js.map +1 -1
  118. package/dist/mcp/tools/delete.d.ts.map +1 -1
  119. package/dist/mcp/tools/delete.js +33 -3
  120. package/dist/mcp/tools/delete.js.map +1 -1
  121. package/dist/mcp/tools/execute.d.ts.map +1 -1
  122. package/dist/mcp/tools/execute.js +20 -9
  123. package/dist/mcp/tools/execute.js.map +1 -1
  124. package/dist/mcp/tools/folder.d.ts +4 -0
  125. package/dist/mcp/tools/folder.d.ts.map +1 -0
  126. package/dist/mcp/tools/folder.js +68 -0
  127. package/dist/mcp/tools/folder.js.map +1 -0
  128. package/dist/mcp/tools/get.d.ts.map +1 -1
  129. package/dist/mcp/tools/get.js +16 -6
  130. package/dist/mcp/tools/get.js.map +1 -1
  131. package/dist/mcp/tools/index.d.ts +1 -1
  132. package/dist/mcp/tools/index.d.ts.map +1 -1
  133. package/dist/mcp/tools/index.js +5 -1
  134. package/dist/mcp/tools/index.js.map +1 -1
  135. package/dist/mcp/tools/list.d.ts.map +1 -1
  136. package/dist/mcp/tools/list.js +38 -14
  137. package/dist/mcp/tools/list.js.map +1 -1
  138. package/dist/mcp/tools/logs.d.ts.map +1 -1
  139. package/dist/mcp/tools/logs.js +15 -5
  140. package/dist/mcp/tools/logs.js.map +1 -1
  141. package/dist/mcp/tools/save.d.ts.map +1 -1
  142. package/dist/mcp/tools/save.js +26 -4
  143. package/dist/mcp/tools/save.js.map +1 -1
  144. package/dist/mcp/tools/save.test.js +192 -1
  145. package/dist/mcp/tools/save.test.js.map +1 -1
  146. package/dist/mcp/tools/search-docs.d.ts +3 -0
  147. package/dist/mcp/tools/search-docs.d.ts.map +1 -0
  148. package/dist/mcp/tools/search-docs.js +102 -0
  149. package/dist/mcp/tools/search-docs.js.map +1 -0
  150. package/package.json +6 -2
  151. package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
  152. package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
  153. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
  154. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
  155. package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
  156. package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
  157. package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
  158. package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
  159. package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
  160. package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
  161. package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
  162. package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
  163. package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
  164. package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
  165. package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
  166. package/skills/funifier/SKILL.md +88 -0
  167. package/skills/funifier/references/configure-security.md +96 -0
  168. package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
  169. package/skills/funifier/references/create-aggregate.md +144 -0
  170. package/skills/funifier/references/create-challenge.md +116 -0
  171. package/skills/funifier/references/create-competition.md +98 -0
  172. package/skills/funifier/references/create-crossword.md +574 -0
  173. package/skills/funifier/references/create-custom-object.md +91 -0
  174. package/skills/funifier/references/create-custom-page.md +135 -0
  175. package/skills/funifier/references/create-folder.md +104 -0
  176. package/skills/funifier/references/create-lastmile.md +643 -0
  177. package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
  178. package/skills/funifier/references/create-level.md +94 -0
  179. package/skills/funifier/references/create-lottery.md +913 -0
  180. package/skills/funifier/references/create-mystery.md +769 -0
  181. package/skills/funifier/references/create-notification.md +75 -0
  182. package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
  183. package/skills/funifier/references/create-quiz.md +98 -0
  184. package/skills/funifier/references/create-scheduler.md +141 -0
  185. package/skills/funifier/references/create-story.md +636 -0
  186. package/skills/funifier/references/create-swap.md +95 -0
  187. package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
  188. package/skills/funifier/references/create-virtual-good.md +96 -0
  189. package/skills/funifier/references/create-webhook.md +72 -0
  190. package/skills/funifier/references/create-websocket.md +71 -0
  191. package/skills/funifier/references/create-widget.md +76 -0
  192. package/skills/funifier/references/debug.md +87 -0
  193. package/skills/funifier/references/help.md +81 -0
  194. package/skills/funifier/references/implement-frontend.md +106 -0
  195. package/skills/funifier/references/import-csv.md +75 -0
  196. package/skills/funifier/references/manage-player.md +82 -0
  197. package/skills/funifier/references/manage-team.md +76 -0
  198. package/skills/funifier/references/upload-file.md +91 -0
  199. package/datasource-funifier-docs/.search-index.json +0 -17318
  200. package/datasource-funifier-docs/.skills-map.json +0 -73
  201. package/skills/funifier-create-aggregate/SKILL.md +0 -127
  202. package/skills/funifier-create-challenge/SKILL.md +0 -88
  203. package/skills/funifier-create-custom-page/SKILL.md +0 -127
  204. package/skills/funifier-create-level/SKILL.md +0 -87
  205. package/skills/funifier-create-quiz/SKILL.md +0 -87
  206. package/skills/funifier-create-scheduler/SKILL.md +0 -127
  207. package/skills/funifier-create-virtual-good/SKILL.md +0 -87
  208. package/skills/funifier-debug/SKILL.md +0 -92
  209. package/skills/funifier-help/SKILL.md +0 -86
  210. package/skills/funifier-implement-frontend/SKILL.md +0 -90
  211. package/skills/funifier-index/SKILL.md +0 -58
@@ -1,28 +1,751 @@
1
- # Staging (Staging)
1
+ # `staging`
2
2
 
3
- **Acesso Studio:** `/studio/integration/staging`
3
+ **Acesso Studio:** `/studio/integration/staging` (rótulo de UI — ver nota arquitetural)
4
+ **API Endpoint:** `/v3/gamification` (sub-rotas `/clone/*` e `/data/*`)
5
+ **Coleção MongoDB:** não existe coleção `staging`. As operações atuam sobre a coleção `game` (banco do sistema Funifier) e sobre os bancos individuais por gamification (cada banco é nomeado pelo `apiKey` da gamification).
4
6
 
5
- ## O que é
7
+ > **Nota arquitetural — confirmada no código:** não existe rota `/v3/staging` no backend `funifier-service`. A busca por `staging` em `src/main/**.java` retorna **uma única ocorrência de produção**: o comentário `//staging : send selected data` em `GamificationRest.java:340`, imediatamente acima do endpoint `POST /v3/gamification/data/copy`. O módulo Studio chamado `staging` é, do ponto de vista do backend, um subconjunto de endpoints do recurso `/v3/gamification` (`clone/{id}`, `clone/remote`, `data/copy`, `data/diff`). O caminho `/studio/integration/staging` é o rótulo da UI do Studio (frontend) e **não é derivável deste repositório**.
6
8
 
7
- Gerenciamento de ambientes de homologação e produção, com clonagem e migração de dados. Permite criar e gerenciar ambientes de homologação (staging) separados do ambiente de produção. Possibilita clonar o ambiente produtivo para testes e migrar dados entre servidores.
9
+ **Arquivos-fonte analisados:**
8
10
 
9
- ## Quando usar
11
+ | Arquivo | Papel |
12
+ | --- | --- |
13
+ | `src/main/java/com/funifier/rest/v3/rest/GamificationRest.java` | Resource JAX-RS (`@Path("v3/gamification")`) |
14
+ | `src/main/java/com/funifier/engine/game/GameManager.java` | Manager — clone, copy, diff, copyDatabase, delete |
15
+ | `src/main/java/com/funifier/engine/game/GameDaoMongo.java` | DAO Mongo da coleção `game` |
16
+ | `src/main/java/com/funifier/engine/game/Game.java` | Entidade raiz (gamification) |
17
+ | `src/main/java/com/funifier/engine/game/GameDataCopy.java` | DTO de payload para copy/diff |
18
+ | `src/main/java/com/funifier/engine/packages/Component.java` | DTO de item individual de copy/diff |
19
+ | `src/main/java/com/funifier/engine/game/GameUsage.java` | Subentidade de métricas de uso |
20
+ | `src/main/java/com/funifier/rest/v3/AuthBean.java` | Resolução de credenciais, proxy e scope |
21
+ | `src/main/java/com/funifier/engine/util/Entity.java` | Mapeamento enum → nome de coleção |
22
+ | `src/main/java/com/funifier/engine/player/Principal.java` | `TYPE_USER = 0` |
23
+ | `src/main/java/com/funifier/engine/account/Account.java` | Lista `games[]` e `games_allowed` |
10
24
 
11
- - Para testar novas funcionalidades sem impactar produção
12
- - Para validar atualizações antes do go-live
13
- - Para migrar dados entre servidores de diferentes regiões
14
- - Para criar ambientes espelho de produção
25
+ > **NÃO localizado no código** (documentado na seção 7): não há Service dedicado (`StagingService`), Repository próprio, Scheduler/Job, enum de status, nem pipeline de aggregation específico de `staging`. Toda a lógica vive em `GamificationRest` + `GameManager`.
15
26
 
16
- ## Checklist de Configuração no Studio
27
+ ---
17
28
 
18
- - [ ] Criar ambiente de staging
19
- - [ ] Clonar dados de produção para staging
20
- - [ ] Testar novas funcionalidades em staging
21
- - [ ] Migrar alterações aprovadas para produção
29
+ ## 1. Visão Geral
22
30
 
23
- ## Validações e Testes
31
+ O módulo `staging` fornece operações para tratar gamifications como **ambientes** (produção, homologação, backup) e mover configuração/dados entre eles. São quatro operações reais, todas no recurso `/v3/gamification`:
24
32
 
25
- - [ ] Ambiente de staging é criado corretamente
26
- - [ ] Dados são clonados sem perda
27
- - [ ] Alterações em staging não afetam produção
28
- - [ ] Migração para produção funciona
33
+ 1. **Clone** (`POST /clone/{id}`) — cópia integral de uma gamification (banco MongoDB inteiro), em três modos: mesma conta, conta diferente no mesmo servidor, ou servidor remoto.
34
+ 2. **Clone remoto recepção** (`POST /clone/remote`) — endpoint interno chamado pelo servidor de origem durante o clone remoto.
35
+ 3. **Cópia seletiva de dados** (`POST /data/copy`) copia documentos ou coleções inteiras de uma gamification para outra, na mesma conta.
36
+ 4. **Diff de dados** (`POST /data/diff`) — compara documentos entre duas gamifications da mesma conta e devolve um JSON Patch (RFC 6902).
37
+
38
+ **Papel arquitetural:**
39
+ - `clone` opera **no nível do banco MongoDB** (cópia de banco inteiro via `copydb` ou `$merge` por coleção).
40
+ - `data/copy` e `data/diff` operam **coleção-a-coleção** e são restritos à **mesma conta** (`accountId`).
41
+ - **Não há entidade "ambiente" persistida.** Não existe modelo de staging no banco. A relação origem→destino é informada em cada requisição (`source`/`target` ou path param + body).
42
+
43
+ **Casos de uso típicos:**
44
+ - Criar uma gamification de homologação a partir da produção.
45
+ - Promover configurações específicas (`action`, `challenge`, `level`) entre ambientes.
46
+ - Inspecionar diferenças de configuração antes de uma migração.
47
+ - Migrar uma gamification para outro servidor/região Funifier.
48
+
49
+ **Módulos/dependências relacionados:** `AccountManager` (validação de credenciais e limite de gamifications), `ManagerFactory` (obtém a conexão Jongo de cada gamification), `SchedulerManager`/`StaticManager`/`S3Service` (apenas no `delete`), `Guid` (geração de apiKey).
50
+
51
+ ---
52
+
53
+ ## 2. Arquitetura e Fluxos
54
+
55
+ ### 2.1 Pipeline — Clone local, mesma conta
56
+
57
+ `GamificationRest.clone()` (linha 272) → `GameManager.clone()` (linha 184):
58
+
59
+ ```
60
+ 1. [Auth] AccountManager.isAppSecretAllowed(apiKey, appSecret) GamificationRest.java:276
61
+ 2. [Guard] proxyEndpoint == null/vazio → modo LOCAL GamificationRest.java:281
62
+ 3. [Guard] gamification.accountId == null || == conta atual → mesma conta :285
63
+ 4. [Limit] AccountManager.allowCreateMoreGamifications(account) :286 (senão 401)
64
+ 5. GameManager.clone(id, gamification, removeLogs) :287
65
+ 5.1 source = findByApiKey(id) GameManager.java:185
66
+ 5.2 gamification.accountId = accountId ?: source.accountId :188
67
+ 5.3 gamification.apiKey = apiKey ?: Guid.newShortGuid() :189
68
+ 5.4 add(gamification) → salva em `game` + atualiza Account :190
69
+ 5.5 copyDatabase(source.apiKey, gamification.apiKey) :194
70
+ 5.6 se removeLogs == true (default): drop de coleções de runtime :196-214
71
+ 6. HTTP 201 com a nova Game GamificationRest.java:288
72
+ ```
73
+
74
+ **`copyDatabase()` (linha 582) — detecção de versão do MongoDB:**
75
+
76
+ ```
77
+ v = versão do MongoDB via comando admin "buildinfo" GameManager.java:589-593
78
+ se v[0] < 4 OU (v[0] == 4 E v[1] < 2):
79
+ comando admin "copydb" {fromdb, todb} ← deprecated :603-607
80
+ senão:
81
+ para cada coleção do banco origem:
82
+ aggregate "{ $merge: { into: { db: toApiKey, coll: <col> } }}" :616
83
+ ```
84
+
85
+ > **Comportamento silencioso / falha:** se o `buildinfo` lançar exceção, `v` permanece `null` (capturado em try/catch vazio, `GameManager.java:594-596`) e a linha `if(v[0] < 4 ...)` (`:599`) lança **NullPointerException**. Não há fallback.
86
+
87
+ ### 2.2 Pipeline — Clone local, conta diferente
88
+
89
+ Mesma rota, com header `X-Proxy-Authorization` e `gamification.accountId` diferente da conta atual (`GamificationRest.java:296-312`):
90
+
91
+ ```
92
+ 1. [Auth] isAppSecretAllowed(apiKey, appSecret) ← conta de ORIGEM :276
93
+ 2. [Auth] isAppSecretAllowed(proxyApiKey, proxyAppSecret) ← conta de DESTINO :298
94
+ 3. [Limit] allowCreateMoreGamifications(proxyAccount) :301 (senão 401)
95
+ 4. GameManager.clone(id, gamification, removeLogs) :302 (mesmo pipeline 2.1)
96
+ ```
97
+
98
+ > As credenciais de proxy são extraídas do header `X-Proxy-Authorization` por `AuthBean.getProxyApiKey()` / `getProxyAppSecret()` (`AuthBean.java:127-160`).
99
+
100
+ ### 2.3 Pipeline — Clone remoto (outro servidor)
101
+
102
+ Mesma rota, com `X-Proxy-Authorization` **e** `X-Proxy-Endpoint` (`GamificationRest.java:315-318`) → `GameManager.cloneRemoteSend()` (linha 231):
103
+
104
+ ```
105
+ 1. gamification.apiKey = apiKey ?: source.apiKey ← mantém o MESMO apiKey da origem! GameManager.java:244
106
+ 2. PASSO 1 — registrar a gamification remota: :245
107
+ cloneRemoteRegisterGamificationHelper() → POST {endpoint}/v3/gamification/clone/remote
108
+ body: { operation: "create_gamification", data: <Game> } :367-395
109
+ 3. PASSO 2 — enviar coleções de estrutura: :249-280
110
+ blacklist = ["system.indexes"] (+ coleções de log se removeLogs) :251-267
111
+ para cada coleção NÃO blacklistada:
112
+ cloneRemoteSaveCollectionHelper() → lê em lotes e POSTa por lote :275
113
+ buffer flush quando list.size() > 500 (ou seja, a cada 501 docs) :302
114
+ POST {endpoint}/v3/gamification/clone/remote
115
+ body: { operation: "save_collection", apikey, collection, data: [...] } :304-317
116
+ 4. Retorna { all: [coleções], out: [respostas OK], err: [erros] } :282-284
117
+ ```
118
+
119
+ > **Acumulação de erros:** cada lote é envolto em try/catch (`:312`, `:345`); falhas vão para `err[]` e o processo **continua**. Nenhuma exceção interrompe o clone remoto — falhas parciais são silenciosas exceto pelo array `err`.
120
+
121
+ #### Diagrama — Interação no clone remoto (`cloneRemoteSend`)
122
+
123
+ ```mermaid
124
+ sequenceDiagram
125
+ participant C as Cliente
126
+ participant S as Servidor Origem<br/>(cloneRemoteSend)
127
+ participant R as Servidor Remoto<br/>(cloneRemoteReceive)
128
+ C->>S: POST /clone/{id}<br/>X-Proxy-Endpoint + X-Proxy-Authorization
129
+ S->>R: POST /clone/remote<br/>{operation: create_gamification, data}
130
+ R-->>S: {status: OK} ou 401
131
+ loop Para cada coleção não-blacklistada
132
+ loop Lotes de até 501 documentos
133
+ S->>R: POST /clone/remote<br/>{operation: save_collection, apikey, collection, data[]}
134
+ R-->>S: resposta (OK → out[], erro → err[])
135
+ end
136
+ end
137
+ S-->>C: 201 { all, out, err }
138
+ ```
139
+
140
+ ### 2.4 Pipeline — Recepção de clone remoto (`/clone/remote`)
141
+
142
+ `GamificationRest.cloneRemote()` (linha 330) → `GameManager.cloneRemoteReceive()` (linha 400):
143
+
144
+ ```
145
+ [Auth] isAppSecretAllowed(apiKey, appSecret) GamificationRest.java:331
146
+ System.out.println(JsonUtil.toJson(source)) ← LOGA O PAYLOAD INTEIRO GameManager.java:402
147
+ operation == "create_gamification": :405-420
148
+ gamification.accountId = accountId ?: account_da_requisicao :409
149
+ current = findByApiKey(gamification.apiKey) :412
150
+ se current != null E current.accountId != account → 401 :413-415
151
+ add(gamification) :417
152
+ operation == "save_collection": :421-451
153
+ current = findByApiKey(apikey) :431
154
+ se current != null E current.accountId != account → 401 :432-434
155
+ [!] collection é lida como String crua (source.get("collection")) :427 (sem validação)
156
+ para cada doc: marshaller.marshall(o) → BasicDBObject.parse → c.save(doc) :441-447
157
+ ```
158
+
159
+ ### 2.5 Pipeline — Cópia seletiva (`/data/copy`)
160
+
161
+ `GamificationRest.dataCopy()` (linha 344) → `GameManager.copy()` (linha 457):
162
+
163
+ ```
164
+ 1. [Auth] isAppSecretAllowed GamificationRest.java:347
165
+ 2. [Guard] proxyEndpoint == null/vazio (senão NO-OP, retorna {}) :350
166
+ 3. sourceGame = findByApiKey(data.source); targetGame = findByApiKey(data.target) :352-353
167
+ 4. [Guard] source != null E target != null E target.accountId == source.accountId :356
168
+ (senão NO-OP: retorna {} com HTTP 201, sem copiar nada)
169
+ 5. GameManager.copy(data) :357
170
+ para cada Component item em data.items (que tenha id E type não-vazios): GameManager.java:462-463
171
+ item.id == "*" → aggregate "{ $merge: { into: { db: target, coll: type }}}" :466
172
+ item.id != "*" → obj = source.findOne({_id: id}); target.save(obj) :471-472
173
+ 6. result.put("status","OK") → HTTP 201 GamificationRest.java:358
174
+ ```
175
+
176
+ > **Sem try/catch no loop de `copy()`** (`GameManager.java:462-476`). Se um `item.id` específico não existir na origem, `findOne(...).as(Object.class)` retorna `null` e `target.save(null)` (`:472`) lança exceção — a requisição inteira falha **no meio da iteração**, deixando os itens anteriores já copiados. Diferente de `diff()`, não há proteção por item.
177
+
178
+ ### 2.6 Pipeline — Diff (`/data/diff`)
179
+
180
+ `GamificationRest.dataDiff()` (linha 376) → `GameManager.diff()` (linha 481):
181
+
182
+ ```
183
+ 1-4. Mesmos guards de auth e mesma conta de data/copy GamificationRest.java:379-388
184
+ 5. GameManager.diff(data) :389
185
+ para cada Component item (id E type não-vazios): GameManager.java:492-493
186
+ item.id == "*" → source.find().limit(100) ← LIMITE FIXO 100 :496
187
+ para cada doc: target.findOne({_id}); diffObjects(...) :497-507
188
+ item.id != "*" → source.findOne; target.findOne; diffObjects(...) :512-516
189
+ cada chamada a diffObjects() é envolta em try/catch :502-506, 515-519
190
+ → exceção apenas faz e.printStackTrace() e o item é DESCARTADO
191
+ 6. Retorna { source, target, diff: [...] } :524
192
+ ```
193
+
194
+ **`diffObjects(type, id, source, target)` (linha 533):**
195
+
196
+ ```
197
+ resolve título (depende do type): GameManager.java:540-562
198
+ action → source.get("action")
199
+ point_category → source.get("category")
200
+ challenge → source.get("challenge")
201
+ level → source.get("level")
202
+ outros → "title" ?: "name" ?: "label" ?: "_id"
203
+ se source != null E target == null → operations = "[op: create]" :565-566
204
+ senão → JsonDiff.asJsonPatch(target, source) :571-577
205
+ (RFC 6902; só inclui se != "[]")
206
+ ```
207
+
208
+ > **Deletions nunca são reportadas.** O bloco `else if(source == null && target != null) { "[op: delete]" }` está **comentado** (`GameManager.java:568-570`). Além disso, mesmo que fosse reativado, é **inalcançável**: quando `source == null` a resolução de título no topo (`source.get("action")`, `source.containsKey(...)`) lança NullPointerException antes de chegar lá; a exceção é capturada em `diff()` e o item é silenciosamente descartado. Documentos presentes só no `target` jamais aparecem no resultado.
209
+
210
+ ### 2.7 Diagrama — Modos de operação do clone
211
+
212
+ ```mermaid
213
+ flowchart LR
214
+ A["POST /v3/gamification/clone/{id}"] --> AUTH{isAppSecretAllowed?}
215
+ AUTH -- não --> X401[401 UNAUTHORIZED]
216
+ AUTH -- sim --> B{X-Proxy-Endpoint?}
217
+ B -- "vazio (LOCAL)" --> C{accountId do body}
218
+ C -- "null ou conta atual" --> L1{allowCreateMore?}
219
+ L1 -- sim --> D["GameManager.clone()<br/>copyDatabase local"]
220
+ L1 -- não --> X401
221
+ C -- "conta diferente" --> E{Proxy auth válido?}
222
+ E -- sim --> L2{allowCreateMore proxy?}
223
+ L2 -- sim --> D
224
+ L2 -- não --> X401
225
+ E -- não --> X401
226
+ B -- "URL remota" --> G["GameManager.cloneRemoteSend()<br/>lotes HTTP de 501 docs"]
227
+ G --> H["POST remoto<br/>/v3/gamification/clone/remote"]
228
+ ```
229
+
230
+ ---
231
+
232
+ ## 3. Estrutura dos Objetos
233
+
234
+ ### 3.1 `Game` — documento raiz (coleção `game`)
235
+
236
+ Mapeada por `Game.java`. `@JsonIgnoreProperties(ignoreUnknown=true)` — campos desconhecidos no JSON são silenciosamente ignorados na desserialização.
237
+
238
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
239
+ | --- | --- | --- | --- | --- |
240
+ | `_id` | String | gerado | — | `apiKey` (anotado `@JsonProperty("_id")`). Também é o **nome do banco MongoDB** da gamification |
241
+ | `created_at` | Date | servidor | — | Definido no primeiro save; preservado em updates via `add()` (`GameManager.java:95`) |
242
+ | `updated_at` | Date | servidor | — | Sempre sobrescrito em cada save (`:99`) |
243
+ | `templateId` | String | — | — | Referência a template de criação |
244
+ | `inlineEditable` | boolean | `false` | — | Flag de edição inline no Studio |
245
+ | `description` | String | — | — | Descrição livre |
246
+ | `name` | String | — | — | Nome da gamification (não há validação de obrigatoriedade no backend) |
247
+ | `accountId` | String | — | ✓* | Conta proprietária. *Definido pelo servidor em `insert`/`clone`; `add()` ignora a Game se `accountId == null` (`GameManager.java:77`) |
248
+ | `image` | `Image` | — | — | Objeto com `small`, `medium`, `original` (cada um `ImageItem`) |
249
+ | `extra` | `Map<String,Object>` | `{}` | — | Metadados extras livres |
250
+ | `i18n` | `Map<String,Map<String,String>>` | `{}` | — | Strings de internacionalização |
251
+ | `paying` | boolean | `false` | — | Flag de conta pagante |
252
+ | `usage` | `List<GameUsage>` | `[]` | — | Configurações de métricas de uso |
253
+
254
+ **Campos computados (não persistem como campo próprio):**
255
+ - `getImageUrl()` (`Game.java:80-82`) — retorna `image.small.url`, ou `null`. É um getter derivado, não um campo armazenado.
256
+
257
+ **Campos legados comentados no código-fonte (`Game.java:33-36, 84-100`):**
258
+ - `imageUrl` (String) — substituído por `image`.
259
+ - `timeZone` (String) — removido; getter/setter comentados.
260
+
261
+ > Por estarem comentados, esses campos **não são desserializados nem persistidos**. Enviá-los no JSON é inócuo (ignorados por `@JsonIgnoreProperties`).
262
+
263
+ ### 3.2 `GameDataCopy` — payload de `data/copy` e `data/diff`
264
+
265
+ `GameDataCopy.java`. `@JsonIgnoreProperties(ignoreUnknown=true)`.
266
+
267
+ | Campo | Tipo | Obrigatório | Descrição |
268
+ | --- | --- | --- | --- |
269
+ | `source` | String | ✓ | apiKey da gamification de origem |
270
+ | `target` | String | ✓ | apiKey da gamification de destino |
271
+ | `items` | `List<Component>` | ✓ | Itens a copiar/comparar (default `[]`) |
272
+
273
+ ### 3.3 `Component` — item de copy/diff
274
+
275
+ `Component.java`. `@JsonIgnoreProperties(ignoreUnknown=true)`.
276
+
277
+ | Campo | Tipo | Obrigatório (runtime) | Descrição |
278
+ | --- | --- | --- | --- |
279
+ | `_id` | String | ✓ | ID do documento, ou `*` para a coleção inteira (`@JsonProperty("_id")`) |
280
+ | `type` | String | ✓ | **Nome da coleção MongoDB** (ex.: `action`, `challenge`, `level`) |
281
+ | `title` | String | — | Apenas rótulo — **ignorado** por `copy()` e `diff()` |
282
+ | `content` | String | — | Apenas rótulo — **ignorado** por `copy()` e `diff()` |
283
+
284
+ > Os javadocs em `Component.java` marcam todos os 4 campos como "required", mas o runtime só usa `id` e `type`: o loop em `copy()`/`diff()` exige `id` e `type` não-vazios (`GameManager.java:463`, `:493`) e ignora `title`/`content`. São aceitos pelo desserializador e servem apenas de label na UI.
285
+
286
+ ### 3.4 `GameUsage` — subentidade de métricas (`Game.usage[]`)
287
+
288
+ `GameUsage.java`.
289
+
290
+ | Campo | Tipo | Descrição |
291
+ | --- | --- | --- |
292
+ | `_id` | String | Identificador (`@JsonProperty("_id")`) |
293
+ | `title` | String | Título da métrica |
294
+ | `type` | String | `custom` ou `players_with_action_logs` (constantes `TYPE_CUSTOM`, `TYPE_PLAYERS_WITH_ACTION_LOGS`) |
295
+ | `collection` | String | Coleção MongoDB onde o aggregate é executado (ex.: `player`) |
296
+ | `aggregate` | String | Pipeline de aggregation; deve retornar um campo `total` |
297
+
298
+ ### 3.5 Coleções de runtime removidas no `clone` com `removeLogs=true`
299
+
300
+ Nomes reais resolvidos via `Entity.java`. **11 coleções são dropadas** + remoção parcial em `principal`. A coleção `team` **NÃO** é removida (linha comentada).
301
+
302
+ | Constante `Entity` | Coleção | Ação no clone LOCAL (`GameManager.java:199-213`) |
303
+ | --- | --- | --- |
304
+ | `PLAYER` | `player` | `drop()` |
305
+ | `PLAYER_STATUS` | `player_status` | `drop()` |
306
+ | `ACHIEVEMENT` | `achievement` | `drop()` |
307
+ | `ACTION_LOG` | `action_log` | `drop()` |
308
+ | `NOTIFICATION` | `notification` | `drop()` |
309
+ | `LEADER` | `leader` | `drop()` |
310
+ | `TEAM` | `team` | **não removida** (comentada, `:206`) |
311
+ | `TEAM_PLAYER` | `team_player` | `drop()` |
312
+ | `PRINCIPAL` | `principal` | `remove("{type:#}", 0)` — remove só `type == 0` (`TYPE_USER`); mantém os demais principals |
313
+ | `LOTTERY_TICKET` | `lottery_ticket` | `drop()` |
314
+ | `PROGRESS_LOG` | `progress_log` | `drop()` |
315
+ | `CHALLENGE_PROGRESS` | `challenge_progress` | `drop()` |
316
+ | `GAMIFICATION_STATUS` | `game_status` | `drop()` (enum `GAMIFICATION_STATUS` → coleção `game_status`, `Entity.java:140`) |
317
+
318
+ > **`Principal.TYPE_USER = 0` é um inteiro** (`Principal.java:25`). A query real é `{type: 0}`, **não** `{type: "user"}`. Principals com `type != 0` (ex.: tipos de máquina/serviço) são preservados no clone local.
319
+
320
+ > **Divergência local × remoto:** no clone **remoto** (`cloneRemoteSend`, `:262`) a coleção `principal` é colocada **inteira** na blacklist (não é enviada de jeito nenhum quando `removeLogs=true`), enquanto no clone **local** apenas `type == 0` é removido. Resultado: clone remoto com `removeLogs` perde **todos** os principals; clone local mantém os não-usuário.
321
+
322
+ ---
323
+
324
+ ## 4. Endpoints
325
+
326
+ Todos sob `@Path("v3/gamification")` (`GamificationRest.java:35`), `Produces: application/json; charset=UTF-8`. As respostas passam por `JsonUtil.toJsonRemoveNullFields(...)` — **campos nulos são omitidos** do JSON de saída.
327
+
328
+ ### `GET /v3/gamification` — listar gamifications
329
+
330
+ | Aspecto | Detalhe |
331
+ | --- | --- |
332
+ | Finalidade | Lista gamifications da conta autenticada (`findAllGamifications`, `:59`) |
333
+ | Autenticação | `isAppSecretAllowed` (credenciais de conta); senão 401 |
334
+ | Status sucesso | 200 OK |
335
+
336
+ **Query params** (`GameManager.findAllByAccountId`, linha 647):
337
+
338
+ | Param | Tipo | Comportamento real |
339
+ | --- | --- | --- |
340
+ | `name` | String | `{name: {$regex: <valor>}}` — match por regex |
341
+ | `published_min` | String | RFC 3339 **ou** keyword (`-1d`, `-30m`, unidades `y M w d h m s`) → `created_at >= valor` (inclusivo) |
342
+ | `published_max` | String | RFC 3339 ou keyword → `created_at <= valor` |
343
+ | `orderby` | String | Campo de ordenação concatenado **cru** na query (`name`, `description`, `created_at`) |
344
+ | `reverse` | boolean | `true` → `-1` (DESC); default ASC. Parse tolerante a erro (`:71`) |
345
+ | `max_results` | int | `<= 0` ⇒ default **100** (`GameManager.java:660`) |
346
+
347
+ ### `GET /v3/gamification/{id}` — buscar por apiKey
348
+
349
+ | Aspecto | Detalhe |
350
+ | --- | --- |
351
+ | Finalidade | `find()` (`:102`) |
352
+ | Autenticação | Dois caminhos: (1) `isAppSecretAllowed` → **200 OK**; (2) Bearer de marketplace com claim `account` igual ao `gamification.accountId` → **201 CREATED** |
353
+
354
+ > **Quirk confirmado:** o caminho de marketplace retorna `Response.Status.CREATED` (201) para um GET (`GamificationRest.java:112`). Não é o convencional 200. Se nenhum caminho casar → 401.
355
+
356
+ ### `POST /v3/gamification` — criar
357
+
358
+ | Aspecto | Detalhe |
359
+ | --- | --- |
360
+ | Finalidade | `insert()` (`:158`) |
361
+ | Autenticação | `isAppSecretAllowed` (conta) **ou** Bearer marketplace com claim `account` válido |
362
+ | Full replace ou patch | Criação. `accountId` é **sempre sobrescrito** pelo servidor (`:161` ou `:173`) |
363
+ | Status sucesso | 201 CREATED |
364
+
365
+ **Comportamento real (via `GameManager.add()`, linha 75):**
366
+ - `apiKey` ausente ⇒ gera `Guid.newShortGuid()` e define `created_at` (`:83-86`).
367
+ - `apiKey` presente e já existe ⇒ **preserva o `created_at` antigo** (`:95`).
368
+ - `updated_at` sempre sobrescrito (`:99`).
369
+ - Adiciona o apiKey em `Account.games[]` se ausente (`:106-108`).
370
+ - Chama `calculateStatisticsByAccount(accountId)` (`:113`).
371
+ - Antes, verifica `allowCreateMoreGamifications` (`:160`) → 401 "account is not allowed to create more gamifications" se o limite for atingido.
372
+
373
+ ### `DELETE /v3/gamification/{id}` — excluir (cascade)
374
+
375
+ | Aspecto | Detalhe |
376
+ | --- | --- |
377
+ | Finalidade | `delete()` (`:196`) |
378
+ | Autenticação | `isAppSecretAllowed` E (a gamification é da conta **ou** o scope contém `CROSSDOMAIN`) |
379
+ | Status | 200 OK; 401 com mensagem se não autorizado |
380
+
381
+ **Cascade real (ordem em `GameManager.delete()`, linha 135):**
382
+ 1. `ManagerFactory.getInstance(apiKey).getSchedulerManager().deleteAll()` (`:141`)
383
+ 2. `StaticManager.deleteByApiKey(apiKey)` (`:144`)
384
+ 3. `ManagerFactory.stopInstance(apiKey)` (`:147`)
385
+ 4. `MONGO.dropDatabase(apiKey)` — **destrói o banco MongoDB inteiro da gamification** (`:151`)
386
+ 5. `game` collection: `remove({_id: apiKey})` (`:157`)
387
+ 6. Remove o apiKey de `Account.games[]` (`:162`)
388
+ 7. `S3Service.deleteAllFilesByApiKey(apiKey)` (`:167`)
389
+
390
+ > **Irreversível e não-transacional.** Não há soft-delete nem rollback. Os 7 passos rodam em sequência sem transação: se um passo intermediário falhar (ex.: `dropDatabase`, atualização de `Account`, S3), a gamification fica em **estado inconsistente** (banco já dropado mas registro ainda em `game`, ou vice-versa).
391
+
392
+ ### `POST /v3/gamification/clone/{id}` — clonar
393
+
394
+ | Aspecto | Detalhe |
395
+ | --- | --- |
396
+ | Finalidade | `clone()` (`:272`) — local mesma conta, local outra conta, ou remoto |
397
+ | Autenticação | `isAppSecretAllowed` (conta de origem); proxy auth se conta destino diferir |
398
+ | Status sucesso | 201 CREATED |
399
+
400
+ **Path param:** `id` — apiKey da gamification **origem**.
401
+
402
+ **Query param:**
403
+
404
+ | Param | Tipo | Comportamento real |
405
+ | --- | --- | --- |
406
+ | `removeLogs` | String | `remove = !"false".equals(removeLogs)` (`:278`). Ou seja: **somente** a string literal `"false"` mantém os logs; ausente ou qualquer outro valor ⇒ `removeLogs=true` (remove) |
407
+
408
+ **Headers especiais:**
409
+
410
+ | Header | Uso |
411
+ | --- | --- |
412
+ | `X-Proxy-Authorization` | Credenciais da conta de destino (clone entre contas) ou do servidor remoto |
413
+ | `X-Proxy-Endpoint` | URL do servidor remoto; **define o modo remoto** (`:281`) |
414
+
415
+ **Body (`Game`):** `name`, `description`, `accountId` (opcional), `image`, `_id`/`apiKey` (opcional).
416
+
417
+ **Comportamento real:**
418
+ - Clone local sem `apiKey` no body ⇒ gera novo GUID (`GameManager.java:189`).
419
+ - Clone **remoto** sem `apiKey` no body ⇒ usa o **mesmo apiKey da origem** (`:244`).
420
+ - Clone remoto retorna `{ all, out, err }`; erros acumulados sem falhar.
421
+
422
+ ### `POST /v3/gamification/clone/remote` — recepção interna
423
+
424
+ | Aspecto | Detalhe |
425
+ | --- | --- |
426
+ | Finalidade | `cloneRemote()` (`:330`) — recebe dados do `cloneRemoteSend` do servidor origem |
427
+ | Autenticação | `isAppSecretAllowed` (conta) |
428
+ | Body | `{ operation: "create_gamification" \| "save_collection", ... }` |
429
+
430
+ > Endpoint interno. Não destinado a uso direto. **Loga o payload inteiro em `System.out`** (`GameManager.java:402`) e **não valida** o nome da coleção (`:427`) — ver seção 8.
431
+
432
+ ### `POST /v3/gamification/data/copy` — cópia seletiva
433
+
434
+ | Aspecto | Detalhe |
435
+ | --- | --- |
436
+ | Finalidade | `dataCopy()` (`:344`) — comentário `//staging : send selected data` (`:340`) |
437
+ | Autenticação | `isAppSecretAllowed` (conta) |
438
+ | Restrição | **Apenas local** (`proxyEndpoint` deve ser vazio) e **mesma conta** |
439
+ | Status | 201 CREATED |
440
+
441
+ **Body:** `GameDataCopy` (ver 3.2).
442
+
443
+ **Comportamento real:**
444
+ - `_id: "*"` ⇒ `$merge` da coleção inteira da origem no destino (`GameManager.java:466`). Sem `whenMatched`/`whenNotMatched` explícitos → padrão MongoDB: `whenMatched: "merge"`, `whenNotMatched: "insert"`. Documentos com `_id` coincidente têm campos **mesclados** (não substituídos); documentos só do destino são **mantidos**.
445
+ - `_id` específico ⇒ `findOne` na origem + `save` no destino (`:471-472`).
446
+ - Sem retorno detalhado de itens copiados — só `{ "status": "OK" }`.
447
+
448
+ ### `POST /v3/gamification/data/diff` — comparação
449
+
450
+ | Aspecto | Detalhe |
451
+ | --- | --- |
452
+ | Finalidade | `dataDiff()` (`:376`) |
453
+ | Autenticação | `isAppSecretAllowed` (conta) |
454
+ | Restrição | Apenas local e mesma conta |
455
+ | Status | 201 CREATED |
456
+
457
+ **Body:** mesmo `GameDataCopy`.
458
+
459
+ **Comportamento real:**
460
+ - `_id: "*"` ⇒ **limita a 100 documentos** da origem (`.limit(100)`, hardcoded, `GameManager.java:496`).
461
+ - Retorna operações em **RFC 6902 JSON Patch** (`[{op, path, value}]`) como string no campo `operations`.
462
+ - Item presente só na origem ⇒ `operations: "[op: create]"`.
463
+ - Item presente só no destino (deletion) ⇒ **nunca reportado** (ver 2.6).
464
+ - Source e target em contas diferentes (ou alguma gamification inexistente) ⇒ retorna `{}` sem erro.
465
+
466
+ ---
467
+
468
+ ## 5. Regras de Negócio
469
+
470
+ **Clone — isolamento de conta (`GamificationRest.java:285-312`):** o clone local só prossegue se `gamification.accountId` for nulo, igual à conta atual, ou — se diferente — acompanhado de `X-Proxy-Authorization` válido para a conta destino. Caso contrário, 401.
471
+
472
+ **Clone — preservação de apiKey no remoto (`GameManager.java:244`):** sem `apiKey` no body, a gamification remota recebe o **mesmo apiKey da origem**. Intencional para migração de ambiente, mas colide se o servidor destino já tiver gamification com esse apiKey de **outra** conta (→ 401 em `cloneRemoteReceive`).
473
+
474
+ **Copy/Diff — mesma conta obrigatória (`GamificationRest.java:356`, `:388`):** verificação `target.accountId.equals(source.accountId)`. Se falhar (ou alguma gamification não existir, ou `proxyEndpoint` estar preenchido), o handler simplesmente cai para o `return` final com `result` vazio → **HTTP 201 com `{}`**, sem mensagem de erro.
475
+
476
+ **Limite de gamifications por conta (`Account.games_allowed` / `games[]`):** `insert` e `clone` chamam `allowCreateMoreGamifications`. Atingido o limite → 401 (na conta de origem ou, no clone entre contas, na conta de destino proxy).
477
+
478
+ **Versionamento do MongoDB (`copyDatabase`, `:599`):** versão detectada em runtime via `buildinfo`. `< 4.2` usa `copydb` (deprecated); `>= 4.2` usa `$merge` por coleção. Falha na detecção ⇒ NPE.
479
+
480
+ **Diff — resolução de título (`diffObjects`, `:540-562`):** por tipo (`action`→`action`, `point_category`→`category`, `challenge`→`challenge`, `level`→`level`); demais tipos: `title` > `name` > `label` > `_id`.
481
+
482
+ **`add()` ignora Game sem conta (`GameManager.java:77`):** se `accountId == null`, `add()` não faz nada (sem erro). Nos fluxos REST isso não ocorre porque o servidor preenche `accountId` antes.
483
+
484
+ ---
485
+
486
+ ## 6. Comportamentos Automáticos
487
+
488
+ | Comportamento | Trigger | Impacto | Persistência |
489
+ | --- | --- | --- | --- |
490
+ | Geração de apiKey | `add()`/`clone()` local sem apiKey | `Guid.newShortGuid()` | Persistido em `game._id` |
491
+ | Preservação de `created_at` | `add()` em apiKey existente | Mantém data original | Persistido em `game` |
492
+ | Sobrescrita de `updated_at` | Todo `add()`/save | Atualiza para `now()` | Persistido em `game` |
493
+ | Atualização de `Account.games[]` | `add()` (create/clone) | Adiciona apiKey à lista da conta | Persistido em `account` |
494
+ | Recálculo de estatísticas | `add()` | `calculateStatisticsByAccount(accountId)` | Persistido (stats da conta) |
495
+ | Remoção de coleções de runtime | `clone()` com `removeLogs=true` (default) | 11 coleções dropadas no destino | Irreversível |
496
+ | Remoção de principals usuário | `clone()` local com `removeLogs=true` | `remove({type:0})` em `principal` | Irreversível (clone local) |
497
+ | Log de payload | `cloneRemoteReceive()` | `System.out.println` do payload | Não persistido (stdout) |
498
+
499
+ #### Diagrama — `clone()` local com `removeLogs=true`
500
+
501
+ ```mermaid
502
+ flowchart LR
503
+ A["clone(id, gamification, true)"] --> B["source = findByApiKey(id)"]
504
+ B --> C["add(gamification)<br/>(grava em game + Account)"]
505
+ C --> D["copyDatabase(from, to)"]
506
+ D --> E{MongoDB >= 4.2?}
507
+ E -- sim --> F["$merge por coleção"]
508
+ E -- não --> G["comando admin copydb"]
509
+ F --> H["drop de 11 coleções de runtime"]
510
+ G --> H
511
+ H --> I["principal.remove({type:0})"]
512
+ I --> J["retorna nova Game (201)"]
513
+ ```
514
+
515
+ ---
516
+
517
+ ## 7. Suportado vs NÃO Suportado
518
+
519
+ ### ✅ Suportado
520
+
521
+ - Clone local na mesma conta (`POST /clone/{id}` sem proxy).
522
+ - Clone local entre contas (com `X-Proxy-Authorization`).
523
+ - Clone para servidor remoto (com `X-Proxy-Authorization` + `X-Proxy-Endpoint`).
524
+ - Cópia de documento individual entre gamifications (`data/copy` com `_id` específico).
525
+ - Cópia de coleção inteira via `$merge` (`data/copy` com `_id: "*"`).
526
+ - Diff de documento individual (`data/diff` com `_id` específico).
527
+ - Diff de coleção (máx. 100 docs) (`data/diff` com `_id: "*"`).
528
+ - Clone com/sem dados de runtime (`removeLogs`).
529
+ - Detecção automática de versão do MongoDB.
530
+ - Acumulação de erros no clone remoto (não interrompe no primeiro erro).
531
+ - Exclusão em cascade (`DELETE`) incluindo drop do banco e arquivos S3.
532
+
533
+ ### ❌ NÃO Suportado / armadilhas confirmadas no código
534
+
535
+ - **Rota `/v3/staging`** — não existe. Não há Service, Repository, Scheduler, enum de status nem coleção `staging`.
536
+ - **`data/copy` / `data/diff` cross-server** — o guard `proxyEndpoint == null` faz a operação virar **no-op silencioso** (retorna `{}`) quando há `X-Proxy-Endpoint`.
537
+ - **`data/copy` / `data/diff` cross-account** — retornam `{}` (201) sem copiar/comparar e sem erro.
538
+ - **Detecção de deletions no diff** — bloco `[op: delete]` comentado (`GameManager.java:568-570`) **e** inalcançável por NPE quando `source == null`. Documentos só no target são ignorados.
539
+ - **`data/diff` além de 100 docs por coleção** — `.limit(100)` hardcoded (`:496`). Coleções maiores são truncadas sem aviso.
540
+ - **`data/copy` resiliente a id inexistente** — não há try/catch no loop; `target.save(null)` aborta a requisição no meio (`:472`).
541
+ - **Validação do nome da coleção em `cloneRemoteReceive`** — qualquer string é aceita como `collection` (`:427`).
542
+ - **Rollback de clone ou de delete** — inexistente; ambos são irreversíveis e o `delete` é não-transacional.
543
+ - **Campos `title`/`content` de `Component`** — aceitos no JSON mas ignorados pelo runtime.
544
+ - **Campos legados `imageUrl`/`timeZone` de `Game`** — comentados; não desserializados nem persistidos.
545
+ - **Transacionalidade no clone remoto** — falhas parciais por lote vão para `err[]`; não há atomicidade nem retry.
546
+
547
+ ---
548
+
549
+ ## 8. Segurança e Permissões
550
+
551
+ **Autenticação:** todas as operações de staging exigem `isAppSecretAllowed(apiKey, appSecret)` — credenciais de **nível de conta** (header `Authorization: Account ...` ou `Basic`). Para auth `Account`, `AuthBean.getApiKey()` retorna na verdade o **accountId** (`AuthBean.java:64-65`) — daí o uso de `authBean.getApiKey()` como conta nos handlers. Bearer de player **não** habilita clone/copy/diff.
552
+
553
+ **Isolamento por conta:** `data/copy` e `data/diff` comparam `accountId` de origem e destino; `clone` entre contas exige credenciais de proxy da conta destino; `cloneRemoteReceive` rejeita (401) gravação em gamification de outra conta.
554
+
555
+ **Scope `CROSSDOMAIN` no delete (`GamificationRest.java:201-202`):** com `Application.SCOPE_CROSSDOMAIN` no scope, um token pode **excluir gamifications de outra conta**. Documentado explicitamente: `authBean.getScope().indexOf(Application.SCOPE_CROSSDOMAIN) != -1`.
556
+
557
+ **`cloneRemoteReceive` aceita coleção arbitrária (`GameManager.java:427`):** o campo `collection` é lido como String crua e usado direto em `jongo.getCollection(collection)` sem validar contra `Entity`. Um cliente com credenciais válidas da conta poderia gravar documentos em coleções internas (`principal`, `security`, etc.). A barreira é apenas a posse de credenciais de conta + a checagem de `accountId`.
558
+
559
+ **Sequestro de metadados via `clone` na mesma conta (`GameManager.java:189-190`):** se o body informar um `apiKey` de gamification **já existente da mesma conta**, `clone()` mantém esse apiKey e chama `add()`, que **preserva `created_at` mas sobrescreve nome/descrição/imagem/etc.**; em seguida `copyDatabase` faz `$merge` no banco existente. Quem tiver o app secret da conta pode sobrescrever metadados e mesclar dados em uma gamification existente.
560
+
561
+ **Vazamento de dados em log (`GameManager.java:402`):** `cloneRemoteReceive` imprime o payload completo recebido em `System.out`. Em ambientes que coletam stdout, dados de todas as coleções clonadas ficam expostos nos logs.
562
+
563
+ **Colisão de apiKey no clone remoto (`:244`, `:413`):** sem `apiKey` no body, a gamification remota herda o apiKey da origem. Se o destino já tiver esse apiKey em outra conta → 401; se for da mesma conta → **sobrescreve** via `add()`.
564
+
565
+ **Injeção em `orderby` (`GameManager.java:683`):** o valor de `orderby` é concatenado **cru** na string de aggregation (`"{$sort : {" + orderby + " : #}}"`). Não há allowlist no backend; valores inesperados podem alterar/quebrar o estágio `$sort`.
566
+
567
+ ---
568
+
569
+ ## 9. Observabilidade e Troubleshooting
570
+
571
+ **Verificar se a gamification existe / inspecionar:**
572
+ ```
573
+ GET /v3/gamification/{apiKey}
574
+ Authorization: Account <token>
575
+ ```
576
+
577
+ **Listar gamifications da conta:**
578
+ ```
579
+ GET /v3/gamification
580
+ Authorization: Account <token>
581
+ ```
582
+
583
+ **Verificar resultado do clone local:** após o 201, buscar a nova gamification pelo `_id` retornado.
584
+
585
+ **Diagnosticar clone remoto:** o retorno `{ all, out, err }` (`GameManager.java:282-284`) lista todas as coleções (`all`), respostas OK (`out`) e falhas (`err`). Verifique `err[]` para coleções/lotes que falharam — o processo **não** para no primeiro erro.
586
+
587
+ **Erros comuns:**
588
+
589
+ | Sintoma | Causa provável |
590
+ | --- | --- |
591
+ | `401 UNAUTHORIZED_CLIENT` | Credenciais inválidas, ou conta destino sem proxy auth válido no clone entre contas |
592
+ | `401 account is not allowed to create more gamifications` | Limite `games_allowed` atingido |
593
+ | `401 gamification dont belong to this account` | Clone remoto: apiKey já pertence a outra conta (`cloneRemoteReceive`, `:414`) |
594
+ | `401 you dont have permission to save data in this gamification` | Clone remoto: tentou salvar coleção em gamification de outra conta (`:433`) |
595
+ | Resposta `{}` (201) em `data/copy` ou `data/diff` | Contas diferentes, gamification inexistente, ou `X-Proxy-Endpoint` preenchido |
596
+ | `data/diff` retorna menos itens que o esperado | `.limit(100)` por coleção; ou item lançou exceção e foi descartado (ver stdout) |
597
+ | `data/copy` falha no meio | `_id` específico inexistente na origem → `save(null)` aborta o loop |
598
+ | `NullPointerException` em `copyDatabase` | `buildinfo` falhou ao detectar a versão do MongoDB |
599
+ | Deletions não aparecem no diff | Comportamento esperado — diff não reporta documentos ausentes na origem |
600
+
601
+ **O que verificar quando "o staging não funciona":**
602
+ 1. As credenciais são de **conta** (não Bearer de player)?
603
+ 2. Source e target são da **mesma conta**? (copy/diff)
604
+ 3. Há `X-Proxy-Endpoint` indevidamente setado tornando copy/diff um no-op?
605
+ 4. Para clone remoto: o `apiKey` foi informado para evitar colisão no destino?
606
+ 5. Coleções > 100 docs no diff: o resultado está truncado.
607
+
608
+ ---
609
+
610
+ ## 10. Exemplos Práticos
611
+
612
+ ### Exemplo mínimo — clonar (mesmo servidor, mesma conta, sem logs)
613
+
614
+ ```
615
+ POST /v3/gamification/clone/minha_gamification_api_key
616
+ Authorization: Account NTQwZjE0YjAwZmZlZWI4YzJmZT
617
+ Content-Type: application/json
618
+
619
+ { "name": "Staging - Cópia de Produção" }
620
+ ```
621
+ Resposta `201`:
622
+ ```json
623
+ {
624
+ "_id": "a1b2c3d4e5f6",
625
+ "name": "Staging - Cópia de Produção",
626
+ "accountId": "540f14b00ffeeb8c2xx00000",
627
+ "created_at": 1700000000000,
628
+ "updated_at": 1700000000000
629
+ }
630
+ ```
631
+ Internamente: banco MongoDB copiado integralmente; `player`, `action_log`, `achievement`, etc. (11 coleções) dropadas; `principal` com `type:0` removidos.
632
+
633
+ ### Exemplo — clonar preservando dados de runtime
634
+
635
+ ```
636
+ POST /v3/gamification/clone/minha_gamification_api_key?removeLogs=false
637
+ Authorization: Account NTQwZjE0YjAwZmZlZWI4YzJmZT
638
+
639
+ { "name": "Backup Completo" }
640
+ ```
641
+ > Lembre: **somente** `removeLogs=false` preserva os logs. `removeLogs=0`, `removeLogs=no` ou ausência ⇒ remove.
642
+
643
+ ### Exemplo — copiar actions e challenges específicos entre ambientes (mesma conta)
644
+
645
+ ```
646
+ POST /v3/gamification/data/copy
647
+ Authorization: Account NTQwZjE0YjAwZmZlZWI4YzJmZT
648
+ Content-Type: application/json
649
+
650
+ {
651
+ "source": "staging_api_key",
652
+ "target": "producao_api_key",
653
+ "items": [
654
+ { "_id": "action_id_novo", "type": "action" },
655
+ { "_id": "*", "type": "challenge" }
656
+ ]
657
+ }
658
+ ```
659
+ Resposta `201`: `{ "status": "OK" }`
660
+
661
+ ### Exemplo avançado — diff de todos os actions
662
+
663
+ ```
664
+ POST /v3/gamification/data/diff
665
+ Authorization: Account NTQwZjE0YjAwZmZlZWI4YzJmZT
666
+ Content-Type: application/json
667
+
668
+ {
669
+ "source": "staging_api_key",
670
+ "target": "producao_api_key",
671
+ "items": [ { "_id": "*", "type": "action" } ]
672
+ }
673
+ ```
674
+ Resposta:
675
+ ```json
676
+ {
677
+ "source": "staging_api_key",
678
+ "target": "producao_api_key",
679
+ "diff": [
680
+ {
681
+ "type": "action",
682
+ "_id": "login_action",
683
+ "title": "login",
684
+ "operations": "[{\"op\":\"replace\",\"path\":\"/points\",\"value\":10}]"
685
+ },
686
+ {
687
+ "type": "action",
688
+ "_id": "nova_action_so_no_staging",
689
+ "title": "boas_vindas",
690
+ "operations": "[op: create]"
691
+ }
692
+ ]
693
+ }
694
+ ```
695
+ > Limitado a 100 docs da origem. Actions presentes só em produção (deletados no staging) **não** aparecem.
696
+
697
+ ### Exemplo — clone para servidor remoto
698
+
699
+ ```
700
+ POST /v3/gamification/clone/minha_gamification_api_key
701
+ Authorization: Account TokenDaContaOrigem
702
+ X-Proxy-Authorization: Account TokenDaContaNoServidorRemoto
703
+ X-Proxy-Endpoint: https://eu1.service.funifier.com
704
+ Content-Type: application/json
705
+
706
+ { "name": "Gamification EU", "apiKey": "minha_gamification_eu" }
707
+ ```
708
+ Resposta:
709
+ ```json
710
+ { "all": ["action","challenge","level","point_category"], "out": ["{...}"], "err": [] }
711
+ ```
712
+ > Sempre informe `apiKey` no body: sem ele, o destino herda o apiKey da origem e pode colidir (`GameManager.java:244`).
713
+
714
+ ### Anti-pattern 1 — `data/copy` entre contas diferentes
715
+
716
+ ```
717
+ POST /v3/gamification/data/copy
718
+ Authorization: Account token_conta_A
719
+
720
+ { "source": "game_da_conta_A", "target": "game_da_conta_B", "items": [ ... ] }
721
+ ```
722
+ **Resultado:** `201` com `{}` — **nada é copiado, sem erro**. Para mover entre contas use `POST /clone/{id}` com `X-Proxy-Authorization`.
723
+
724
+ ### Anti-pattern 2 — `data/copy` de `_id` inexistente na origem
725
+
726
+ ```json
727
+ { "source": "stg", "target": "prod", "items": [ { "_id": "id_que_nao_existe", "type": "action" } ] }
728
+ ```
729
+ **Resultado:** `findOne` retorna null → `target.save(null)` lança exceção → a requisição inteira falha; itens anteriores da lista já foram copiados (sem rollback). Sempre confirme a existência do `_id` na origem antes.
730
+
731
+ ### Anti-pattern 3 — confiar no diff para detectar exclusões
732
+
733
+ Usar `data/diff` esperando ver itens "a remover" no destino **não funciona**: deletions nunca são reportadas. Para sincronizar exclusões, compare manualmente as listas de `_id` de cada lado.
734
+
735
+ ---
736
+
737
+ ## Checklist de Configuração
738
+
739
+ - [ ] Credenciais de **conta** (`Authorization: Account ...`) — Bearer de player não habilita staging.
740
+ - [ ] Limite `games_allowed` da conta não atingido antes de `clone`/`insert`.
741
+ - [ ] Clone entre contas: `X-Proxy-Authorization` com credenciais válidas da conta destino.
742
+ - [ ] Clone remoto: `X-Proxy-Endpoint` com a URL do servidor destino.
743
+ - [ ] Clone remoto: **definir `apiKey` no body** para evitar colisão (senão herda o da origem).
744
+ - [ ] `data/copy`/`data/diff`: confirmar que `source` e `target` são da **mesma conta** (senão `{}` silencioso).
745
+ - [ ] `data/copy`/`data/diff`: **não** enviar `X-Proxy-Endpoint` (vira no-op).
746
+ - [ ] `data/copy` com `_id` específico: confirmar que o documento **existe na origem** (senão a requisição aborta).
747
+ - [ ] `data/diff` com `"_id": "*"`: ciente do limite fixo de **100 docs** por coleção.
748
+ - [ ] Armadilha: `removeLogs` default é `true`; só `removeLogs=false` preserva runtime.
749
+ - [ ] Armadilha: `data/copy` com `"_id": "*"` faz `$merge` — não apaga documentos extras no destino.
750
+ - [ ] Armadilha: clone remoto com `removeLogs` **descarta toda** a coleção `principal` (≠ clone local).
751
+ - [ ] Armadilha: diff não reporta deletions (itens só no destino).