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,40 +1,578 @@
1
- # Notification (Notificação)
1
+ # `notification`
2
2
 
3
+ **Acesso Studio:** Não há página de CRUD dedicada no backend. As notificações automáticas são configuradas como arrays `NotificationDefinition[]` **embutidos em outros módulos** (Challenge/Achievement, Competition, Lottery, Lost, Crowning). O envio manual/avulso é feito exclusivamente via API.
3
4
  **API Endpoint:** `/v3/notification`
5
+ **Coleção MongoDB:** `notification`
4
6
 
5
- ## O que é
7
+ ---
6
8
 
7
- Envio de mensagens automáticas e feedback instantâneo aos jogadores. Permite configurar e disparar notificações personalizadas quando eventos importantes acontecem, como a conclusão de um desafio, o ganho de uma competição ou uma compra na loja virtual. Notificações podem ser configuradas diretamente em diferentes módulos (Challenge, Competition, etc.).
9
+ ## 1. Visão Geral
8
10
 
9
- ## Quando usar
11
+ O módulo `notify` (pacote `com.funifier.engine.notify`) é a **camada de entrega, persistência e leitura** de mensagens dirigidas a jogadores e ao mural público (newsfeed). Ele **não** decide *quando* uma notificação deve ser gerada — essa decisão pertence aos módulos de negócio (Achievement, Competition, Lottery, etc.), que carregam suas próprias listas de `NotificationDefinition` e chamam `NotificationManager.send(...)` quando um evento ocorre.
10
12
 
11
- - Para parabenizar jogadores por conquistas
12
- - Para informar sobre prêmios ganhos
13
- - Para enviar lembretes e feedbacks
14
- - Para comunicar mudanças de nível
13
+ Papéis arquiteturais:
15
14
 
16
- ## Checklist de Configuração
15
+ - **Persistir** cada notificação na coleção `notification` (uma única coleção para mensagens privadas e de newsfeed, diferenciadas pelo campo `definition.scope`).
16
+ - **Renderizar** o texto da mensagem com Mustache no momento do envio (`{{player.name}}`, `{{item.name}}`, etc.).
17
+ - **Disparar push mobile** para notificações privadas, via AWS SNS → APNS/GCM.
18
+ - **Servir leituras** de dois modos: leitura não-destrutiva (agregação com janela temporal) e leitura destrutiva legada (consome-e-apaga).
19
+
20
+ Problemas que resolve: feedback assíncrono ao jogador (conquistas, prêmios, mudanças de nível), mural de novidades da gamificação (newsfeed) e broadcast manual de mensagens para um segmento de jogadores.
21
+
22
+ Relação com outros módulos: é um **destino** chamado por managers de negócio. Não chama nenhum módulo de regra de volta — apenas `MobileManagerV3` (push) e `MustacheUtils` (render).
23
+
24
+ ---
25
+
26
+ ## 2. Arquitetura e Fluxos
27
+
28
+ ### 2.1 Classes envolvidas
29
+
30
+ | Classe | Papel |
31
+ | --- | --- |
32
+ | `com.funifier.engine.notify.Notification` | Entidade/POJO principal — documento raiz da coleção `notification` |
33
+ | `com.funifier.engine.notify.NotificationDefinition` | Sub-entidade: `event`, `type`, `scope`, `content`, `tag`. Implementa `Cloneable`. É também a forma usada para *configurar* notificações dentro de outros módulos |
34
+ | `com.funifier.engine.notify.Item` | Sub-entidade que descreve o `player` e o `item` da notificação |
35
+ | `com.funifier.engine.notify.NotificationManager` | Manager: `send`, push, leitura (`findAll`, `receive`), `deleteByPlayer`, `totalUnreadPrivateMessages` |
36
+ | `com.funifier.engine.notify.NotificationDaoMongo` | DAO Jongo/Mongo. Constante `COLLECTION = "notification"` |
37
+ | `com.funifier.engine.notify.NotificationBuilder` | Builder fluente (uso interno/legado — não exposto na API REST) |
38
+ | `com.funifier.rest.v3.rest.NotificationRest` | Controller REST v3 — `@Path("v3/notification")` |
39
+ | `com.funifier.engine.notify.news.NewsManager` | Wrapper de newsfeed. Em grande parte **código morto** (ver §7) |
40
+ | `com.funifier.engine.notify.news.NewsContainer` | Buffer em memória (`LinkedList` com limite). **Totalmente comentado/morto** |
41
+ | `com.funifier.engine.mobile.MobileManagerV3` | `sendPushNotification` — fan-out de push por device do jogador |
42
+ | `com.funifier.engine.integration.mobile.push.Message` | Payload de push. Título fixo `"Funifier Notification"` |
43
+ | `com.funifier.engine.util.MustacheUtils` | Render do `content` via `com.github.mustachejava` |
44
+
45
+ ### 2.2 Pipeline principal — `NotificationManager.send(Notification n)`
46
+
47
+ Fluxo síncrono, sem transação:
48
+
49
+ 1. **[Guarda]** `send` só prossegue se `n != null && (n.getPlayer() != null || n.getDefinition().getScope() == SCOPE_NEWSFEED)`. Caso contrário a notificação é **descartada silenciosamente** (sem erro, sem log). Ou seja: uma notificação `private` sem `player` é jogada fora.
50
+ 2. **[Render]** `n.getDefinition().setContent(MustacheUtils.parse(content, n))` — o template é renderizado tendo o **próprio objeto `Notification` como contexto raiz**. Se o template lançar exceção, `MustacheUtils.parse` imprime stacktrace e **retorna `null`** → o `content` vira literalmente `null` (ver §7).
51
+ 3. **[Timestamp]** `n.timestamp` recebe `ZonedDateTime.now()` no formato `ISO_OFFSET_DATE_TIME` (ex.: `2026-05-20T10:29:35.733-03:00`). O campo `time` (Date) **não** é alterado aqui.
52
+ 4. **[Persistência]** `mongo.add(n, jongo)` → se `n.id == null` gera `Guid.newShortGuid()`, depois `collection.save(n)`. `save` é **upsert por `_id`** — ver §4 (POST com `_id` existente sobrescreve).
53
+ 5. **[Push]** Se `n.getPlayer() != null`, chama `sendPushNotification(n)` → `MobileManagerV3.sendPushNotification`:
54
+ - monta `Message(notification)` + `apiKey`;
55
+ - `findAllDevicesByPlayer(player.id)` → query `{player:#}` na coleção `mobile_device`;
56
+ - para **cada** device: `awsManager.sendNotification(device, message)` (AWS SNS → APNS/GCM).
57
+ - Notificações `newsfeed` (sem `player`) **nunca** geram push.
58
+
59
+ #### Fluxo de envio — `send()`
60
+
61
+ ```mermaid
62
+ flowchart TD
63
+ A["send(n)"] --> B{"n != null E<br/>(player != null OU scope == NEWSFEED)?"}
64
+ B -- Não --> Z["Descarte silencioso<br/>(sem erro/log)"]
65
+ B -- Sim --> C["content = MustacheUtils.parse(content, n)"]
66
+ C --> D["timestamp = now (ISO_OFFSET_DATE_TIME)"]
67
+ D --> E["mongo.add(n): gera _id se null + collection.save (UPSERT)"]
68
+ E --> F{"player != null?"}
69
+ F -- Não --> G["Fim (apenas persistido)"]
70
+ F -- Sim --> H["sendPushNotification(n)"]
71
+ H --> I["findAllDevicesByPlayer(player.id)"]
72
+ I --> J["para cada device: awsManager.sendNotification(device, message)"]
73
+ ```
74
+
75
+ ### 2.3 Interações entre módulos (vista de chamadas)
76
+
77
+ Os módulos de negócio guardam um array `notifications: NotificationDefinition[]` na própria entidade. Quando ocorre o evento (tipicamente `EVENT_WIN`), eles filtram as definições aplicáveis com `NotificationDefinition.getNotificationsByEvent(...)`, constroem um `Notification(player, item, definition, time)` e chamam `send`.
78
+
79
+ ```mermaid
80
+ sequenceDiagram
81
+ participant M as Módulo de negócio<br/>(Achievement/Competition/Lottery/...)
82
+ participant ND as NotificationDefinition
83
+ participant NM as NotificationManager
84
+ participant MU as MustacheUtils
85
+ participant DB as Mongo (notification)
86
+ participant MB as MobileManagerV3
87
+ participant AWS as AwsManager (SNS)
88
+
89
+ M->>ND: getNotificationsByEvent(EVENT_WIN, def[])
90
+ ND-->>M: List<NotificationDefinition>
91
+ M->>NM: send(new Notification(player, item, def, time))
92
+ NM->>MU: parse(content, notification)
93
+ MU-->>NM: content renderizado (ou null em erro)
94
+ NM->>DB: save(notification) [upsert por _id]
95
+ alt player != null
96
+ NM->>MB: sendPushNotification(notification)
97
+ MB->>AWS: sendNotification(device, message) por device
98
+ end
99
+ ```
100
+
101
+ | Origem (caller de `getNotificationManager().send`) | Quando dispara |
102
+ | --- | --- |
103
+ | `AchievementManager` | Conquista/desafio completado, level-up (constrói `new Notification(player, item, def, new Date())`) |
104
+ | `CompetitionManager` | `EVENT_WIN` — vencedor de competição (`def.clone()`) |
105
+ | `LotteryManager` | `EVENT_WIN` — ganhador de loteria (`def.clone()`) |
106
+ | `LostManager` | `EVENT_WIN` configurado em `lost.notifications` |
107
+ | `CrowningManager` | Mudança de líder em leaderboard (líder atual e anterior) |
108
+ | `CharacterStarManager` | Subiu nível de Character Star |
109
+ | `CatalogManager` | Eventos de catálogo |
110
+ | `LastMileManager` / `BonusManager` | Last Mile / bônus (`send`) |
111
+ | `MSOfficeRest` | Integração legada: **envia** (`send(new Notification(userId, msg))` → privada, `EVENT_WIN`/`TYPE_TEXT` via construtor de 2 args) **e lê** via `receive` (consome‑e‑apaga) |
112
+
113
+ > Todas as origens acima foram confirmadas chamando `getNotificationManager().send(...)` (exceto a leitura adicional de `MSOfficeRest.receive`).
114
+
115
+ > Observação: `getNotificationsByEvent` **ignora explicitamente** definições com `scope == SCOPE_CUSTOM (99)` (comentário no código: *"ignora escopo customizado, pois este método antigo é usado em toda a plataforma"*). Logo, notificações com scope custom configuradas em módulos **não são entregues** por esse caminho automático.
116
+
117
+ ### 2.4 Caminhos de leitura
118
+
119
+ Há **dois** caminhos distintos, com semânticas opostas:
120
+
121
+ - **Não-destrutivo (REST v3):** `GET /v3/notification` → `NotificationManager.findAll(...)` (agregação Mongo). Não apaga nada (salvo `delete=true`, ver §4).
122
+ - **Destrutivo / consome-e-apaga (legado):** `NotificationManager.receive(userId)` / `receiveNotification(userId)` → `NotificationDaoMongo.findAllByPlayer` retorna as privadas do jogador **e em seguida apaga todas** (`deleteByPlayer`). Usado apenas por `FrontController.receive` (sessão legada) e `MSOfficeRest`. **Não** está em `/v3/notification`.
123
+
124
+ ---
125
+
126
+ ## 3. Estrutura dos Objetos
127
+
128
+ ### 3.1 `Notification` — documento raiz (coleção `notification`)
129
+
130
+ `@JsonIgnoreProperties(ignoreUnknown=true)` — campos extras enviados no POST são **silenciosamente ignorados** (ver §4).
131
+
132
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
133
+ | --- | --- | --- | --- | --- |
134
+ | `_id` | String | `Guid.newShortGuid()` se ausente | Não (auto) | SHORT GUID interno do Funifier (mapeado por `@JsonProperty("_id")`). Não é ObjectId |
135
+ | `player` | `Item` | — | Condicional | Destinatário. **Obrigatório para `private`** (sem ele a notificação é descartada). Ausente em `newsfeed` |
136
+ | `item` | `Item` | — | Não | Objeto de contexto (challenge, action, level, etc.) referenciado pela mensagem |
137
+ | `definition` | `NotificationDefinition` | — | **Sim** | Define `event`, `type`, `scope`, `content`, `tag` |
138
+ | `time` | Date | `new Date()` no POST se ausente | Não (auto) | Timestamp lógico. Usado nos filtros temporais de `findAll` |
139
+ | `timestamp` | String | preenchido em `send()` | Não (auto) | Data ISO‑8601 com offset, gravada no envio. **Variável "automaticamente populada pelo Funifier"** (comentário no código) |
140
+
141
+ #### Campos computados / não persistidos
142
+
143
+ - Nenhum campo é puramente computado. `timestamp` é derivado do relógio no `send()` e **é** persistido.
144
+
145
+ #### Campos aceitos e silenciosamente ignorados
146
+
147
+ - Qualquer propriedade fora de `_id, player, item, definition, time, timestamp` no corpo do POST é descartada por `@JsonIgnoreProperties(ignoreUnknown=true)`. Não existe campo `read`, `seen`, `userId` ou `read_at` no modelo — enviá-los não faz nada.
148
+
149
+ ### 3.2 `NotificationDefinition` — sub-entidade
150
+
151
+ `@JsonIgnoreProperties(ignoreUnknown=true)`, `implements Cloneable`.
152
+
153
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
154
+ | --- | --- | --- | --- | --- |
155
+ | `event` | int | 0 | Não | Evento que originou a notificação (ver enum) |
156
+ | `type` | int | 0 | Não | Tipo de mídia (ver enum). **Apenas armazenado** — o backend não trata `type` de forma diferenciada |
157
+ | `scope` | int | 0 | Sim (significativo) | `0` private, `1` newsfeed, `99` custom. Define roteamento e persistência |
158
+ | `content` | String | — | **Sim** | Texto da mensagem. Suporta template Mustache (renderizado no `send`) |
159
+ | `tag` | String | — | Não | Rótulo livre para filtro. **Sem null-check** em `getNotifications`/`getRandomNotification` (ver §7) |
160
+
161
+ #### Enum `event` (`NotificationDefinition.EVENT_*`)
162
+
163
+ | Valor | Constante | Significado operacional |
164
+ | --- | --- | --- |
165
+ | `0` | `EVENT_WIN` | Ganho/conquista. **Único evento usado pelos disparadores automáticos** via `getNotificationsByEvent(EVENT_WIN, ...)` |
166
+ | `1` | `EVENT_CREATE` | Criação |
167
+ | `2` | `EVENT_CHANGE` | Alteração |
168
+ | `3` | `EVENT_LOSE` | Perda |
169
+ | `4` | `EVENT_REMOVE` | Remoção |
170
+ | `99` | `EVENT_CUSTOM` | **Comentado no código** — não implementado |
171
+
172
+ #### Enum `type` (`NotificationDefinition.TYPE_*`)
173
+
174
+ | Valor | Constante | Significado |
175
+ | --- | --- | --- |
176
+ | `0` | `TYPE_TEXT` | Texto |
177
+ | `1` | `TYPE_VIDEO` | Vídeo. Armazenado, mas **sem comportamento de backend** (renderização é responsabilidade do cliente) |
178
+
179
+ #### Enum `scope` (`NotificationDefinition.SCOPE_*`)
180
+
181
+ | Valor | Constante | Comportamento |
182
+ | --- | --- | --- |
183
+ | `0` | `SCOPE_PRIVATE` | Mensagem dirigida a um `player`. Gera push. Contabilizada em `totalUnread`. Apagável por jogador |
184
+ | `1` | `SCOPE_NEWSFEED` | Mensagem pública no mural. Sem push. Nunca apagada por `deleteByPlayer` |
185
+ | `99` | `SCOPE_CUSTOM` | Aceito e persistido, mas **excluído** dos disparadores automáticos (`getNotificationsByEvent`) e o mapeamento de `scope` no `GET` nunca produz `99`. Só entregável via `POST /v3/notification` direto |
186
+
187
+ ### 3.3 `Item` — sub-entidade (`player` e `item`)
188
+
189
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
190
+ | --- | --- | --- | --- | --- |
191
+ | `id` | String | — | Não | Identificador do jogador ou do item de contexto |
192
+ | `name` | String | — | Não | Nome de exibição |
193
+ | `image` | `Image` | — | Não | Imagem associada |
194
+ | `type` | String | — | Não | Tipo (ver constantes abaixo) |
195
+ | `title` | String | — | Não | Título alternativo. Populado apenas pelo construtor `Item(image, title, content, type)`, **não** usado nos disparadores automáticos |
196
+ | `content` | String | — | Não | Conteúdo alternativo. Mesma observação de `title` |
197
+
198
+ #### Constantes `Item.TYPE_*`
199
+
200
+ `player`, `team`, `challenge`, `action`, `level`, `stuff`, `news_feed`, `lottery`, `lost`, `competition`.
201
+
202
+ ### 3.4 Variáveis Mustache disponíveis em `content`
203
+
204
+ Como o `send()` chama `MustacheUtils.parse(content, notification)`, o **contexto raiz do template é o objeto `Notification`**. Estão acessíveis (entre outros):
205
+
206
+ - `{{player.id}}`, `{{player.name}}`, `{{player.type}}`
207
+ - `{{item.id}}`, `{{item.name}}`, `{{item.type}}`, `{{item.title}}`, `{{item.content}}`
208
+ - `{{definition.content}}`, `{{definition.tag}}`, `{{definition.event}}`, `{{definition.scope}}`
209
+ - `{{timestamp}}`
210
+
211
+ > No `POST /v3/notification/send` (modo `private`) o `content` passa por um render adicional **antes** do `send()`, com contexto `{ "player": Player }` — ver §4. Isso significa **dois passes de Mustache** nesse fluxo.
212
+
213
+ ### 3.5 Ciclo de vida de uma notificação privada
214
+
215
+ ```mermaid
216
+ stateDiagram-v2
217
+ [*] --> Persistida: send() / POST
218
+ Persistida --> Push: player != null → SNS
219
+ Push --> Disponivel: aguardando leitura
220
+ Persistida --> Disponivel
221
+ Disponivel --> Lida_NaoDestrutiva: GET /v3/notification (continua existindo)
222
+ Lida_NaoDestrutiva --> Disponivel
223
+ Disponivel --> Consumida: GET ?delete=true&scope=private OU receive() legado
224
+ Consumida --> [*]: removida da coleção
225
+ ```
226
+
227
+ ---
228
+
229
+ ## 4. Endpoints
230
+
231
+ Todos sob `@Path("v3/notification")`, `Produces application/json; charset=UTF-8`. Autenticação por Bearer token (`AuthBean`); a `apiKey` extraída do token define o tenant (ver §8).
232
+
233
+ ---
234
+
235
+ **`GET /v3/notification`** — `findAllLog`
236
+
237
+ | Aspecto | Detalhe |
238
+ | --- | --- |
239
+ | Finalidade | Consultar notificações (privadas ou de newsfeed) via agregação |
240
+ | Autenticação | Bearer token |
241
+ | Tipo de leitura | **Não-destrutiva** (exceto `delete=true`) |
242
+
243
+ **Query params:**
244
+
245
+ | Param | Tipo | Descrição |
246
+ | --- | --- | --- |
247
+ | `player` | String | Id do jogador. `"me"` é resolvido para o jogador do token |
248
+ | `scope` | String | `"private"` → scope `0`. **Qualquer outro valor não-vazio → newsfeed (`1`)**, inclusive typos |
249
+ | `tag` | String | Filtra por `definition.tag` |
250
+ | `delete` | String("true"/"false") | Se `true` **e** `player` informado **e** `scope == "private"`, apaga as privadas do jogador **após** retornar |
251
+ | `fields` | String | Lista separada por vírgula → `$project` na agregação |
252
+ | `published_min` | String | Limite inferior de `time` (RFC 3339 ou keyword tipo `-1d`, `-30m`). **Se omitido, assume `agora − 1 hora`** |
253
+ | `published_max` | String | Limite superior de `time` |
254
+ | `max_results` | String(int) | Máximo de resultados. `<= 0` ou inválido → **`10`** |
255
+
256
+ **Comportamento real:**
257
+
258
+ - A agregação sempre inicia com `{ $match : { _id: {$exists:true} ...} }` e ordena por `{$sort : {time : -1}}`.
259
+ - **Armadilha do `published_min`:** sem ele, só retorna a **última 1 hora**. Notificações mais antigas "somem" da resposta sem aviso.
260
+ - O parâmetro `delete=true` é a única forma de "marcar como lida" via REST — ele **apaga** os registros privados do jogador.
261
+
262
+ ```json
263
+ [
264
+ {
265
+ "player": { "id": "ricardo@funifier.com", "name": "Ricardo", "type": "player" },
266
+ "item": { "id": "578052b75a73038c5f3d0e77", "name": "Visit", "type": "action" },
267
+ "definition": { "event": 0, "type": 0, "scope": 1, "content": "Ricardo visited us" },
268
+ "timestamp": "2026-05-20T10:29:35.733-03:00"
269
+ }
270
+ ]
271
+ ```
272
+
273
+ ---
274
+
275
+ **`GET /v3/notification/totalUnread`** — `totalUnread`
276
+
277
+ | Aspecto | Detalhe |
278
+ | --- | --- |
279
+ | Finalidade | Contar notificações **privadas** (interpretadas como "não lidas") |
280
+ | Autenticação | Bearer token |
281
+
282
+ **Query params:** `player` (`"me"` resolvido pelo token), `published_min`, `published_max`.
283
+
284
+ **Comportamento real:** executa `count` em `{ definition.scope: 0 [, player.id: ...] [, time: {$gte/$lte ...}] }`. "Não lida" = simplesmente uma privada que ainda existe na coleção (não há flag de leitura). Diferente do `GET`, **não** aplica a janela default de 1 hora.
285
+
286
+ ```json
287
+ { "total": 7 }
288
+ ```
289
+
290
+ ---
291
+
292
+ **`POST /v3/notification`** — `insert`
293
+
294
+ | Aspecto | Detalhe |
295
+ | --- | --- |
296
+ | Finalidade | Criar/enviar **uma** notificação avulsa |
297
+ | Autenticação | Bearer token |
298
+ | Full replace ou patch | **Upsert por `_id`** — POST com `_id` existente **sobrescreve** o documento inteiro (não retorna 409) |
17
299
 
18
- - [ ] Definir mensagem da notificação
19
- - [ ] Vincular ao módulo que dispara a notificação (challenge, competition, etc.)
20
- - [ ] Definir tipo de notificação (push, in-app, email)
300
+ **Comportamento real:**
21
301
 
22
- ## API Endpoints
302
+ - Se `time == null` → `new Date()`. Se `id == null` → `Guid.newShortGuid()`.
303
+ - Chama `send()` (toda a §2.2 se aplica: guarda, Mustache, timestamp, push).
304
+ - Campos desconhecidos no corpo são ignorados (`@JsonIgnoreProperties`).
305
+ - Retorna `201 CREATED` com o objeto (sem campos nulos).
23
306
 
24
- ### Listar Notificações
25
- **Método:** GET
26
- **Endpoint:** `/v3/notification`
307
+ ```json
308
+ POST /v3/notification
309
+ {
310
+ "player": { "id": "ricardo@funifier.com", "name": "Ricardo", "type": "player" },
311
+ "item": { "id": "578052b75a73038c5f3d0e77", "name": "Visit", "type": "action" },
312
+ "definition": { "event": 0, "type": 0, "scope": 0, "content": "Olá {{player.name}}!" }
313
+ }
314
+ ```
27
315
 
28
- ### Criar Notificação
29
- **Método:** POST
30
- **Endpoint:** `/v3/notification`
316
+ ---
31
317
 
32
- ### Deletar Notificação
33
- **Método:** DELETE
34
- **Endpoint:** `/v3/notification/:id`
318
+ **`POST /v3/notification/send`** — `send` (broadcast)
35
319
 
36
- ## Validações e Testes
320
+ | Aspecto | Detalhe |
321
+ | --- | --- |
322
+ | Finalidade | Disparar a mesma mensagem para um segmento de jogadores **ou** para o newsfeed |
323
+ | Autenticação | Bearer token |
324
+ | Limite | **Máximo 1000 jogadores** por chamada (`findAllPlayers(..., 1000)`) |
325
+
326
+ **Corpo:** um `NotificationDefinition` (não um `Notification`).
327
+
328
+ **Query params:**
329
+
330
+ | Param | Tipo | Descrição |
331
+ | --- | --- | --- |
332
+ | `to` | String | Critério de query Mongo aplicado à coleção `player` (ex.: `extra.gender:"male"`). **Obrigatório no modo privado**. Query raw — ver §8 |
333
+ | `when` | String | RFC 3339 ou keyword (`+1d`, `+30m`). Default = agora |
334
+
335
+ **Comportamento real:**
336
+
337
+ - Validação: `content` vazio → `400` `"You must define a message"`. `to` vazio → `400` `"You must define a to query criteria"`.
338
+ - **Modo newsfeed** (`scope == 1`): cria **uma** notificação sem `player` e envia. Retorna `ids: ["newsfeed"]`.
339
+ - **Modo privado** (qualquer outro scope): itera os players que casam com `to` (até 1000); para cada um faz `content = MustacheUtils.parse(content, {player})` (1º passe) e `send()` re-renderiza (2º passe). Define `player = Item(player.id, player.name, player.image, null)`.
340
+ - **`when` NÃO adia a entrega.** O valor é convertido em Date e gravado em `notification.time`, mas o `send()` persiste e dispara push **imediatamente**. Não há scheduler que leia a coleção `notification` e entregue depois (nenhum encontrado no código). Notificações com `time` futuro aparecem nas leituras de newsfeed de imediato (o `GET` não impõe limite superior de tempo por default).
341
+
342
+ ```json
343
+ { "to": "extra.gender:\"male\"", "ids": ["a@x.com","b@x.com"], "when": "+1d", "total": 2 }
344
+ ```
345
+
346
+ > **Ausência de DELETE:** **não existe** `DELETE /v3/notification/:id` (nem qualquer `@DELETE`) neste controller. Qualquer documentação que o mencione está incorreta. A remoção só ocorre via `GET ?delete=true&scope=private` (em massa, por jogador) ou pelo caminho legado `receive()`.
347
+
348
+ ---
349
+
350
+ ## 5. Regras de Negócio
351
+
352
+ Regras presentes no código mas não evidentes no schema:
353
+
354
+ - **Privada exige `player`.** `send()` descarta silenciosamente uma notificação `private` sem `player`. Newsfeed dispensa `player`.
355
+ - **Render é obrigatório e pode zerar o conteúdo.** Todo `content` passa por Mustache no envio; template inválido → `content == null` (sem erro propagado).
356
+ - **Push só para privadas com `player`.** Newsfeed nunca gera push; privadas geram um push **por device** registrado do jogador.
357
+ - **Janela temporal implícita.** `findAll` sem `published_min` retorna apenas a última 1 hora; `max_results` default 10.
358
+ - **Mapeamento de scope no GET é binário.** Só `"private"` é tratado como privado; tudo mais cai em newsfeed.
359
+ - **`delete=true` é condicional.** Só apaga se `player != null` **e** `scope == "private"`.
360
+ - **Custom scope é ignorado pelos disparadores.** `getNotificationsByEvent` exclui `SCOPE_CUSTOM`.
361
+ - **Multi-tenant por `apiKey`.** Cada gamificação resolve `FrontController.getInstance(apiKey).getManagerFactory()` → conexão Jongo isolada. Notificações nunca cruzam tenants.
362
+ - **Consistência:** sem transação. A persistência ocorre antes do push; falha de push não desfaz a gravação.
363
+
364
+ ---
365
+
366
+ ## 6. Comportamentos Automáticos
367
+
368
+ | Comportamento | Trigger | Impacto | Persistência |
369
+ | --- | --- | --- | --- |
370
+ | Geração de notificação | `EVENT_WIN` em Achievement/Competition/Lottery/Lost/Crowning/... | Cria `Notification(player, item, def, time)` e chama `send()` | Sim (coleção `notification`) |
371
+ | Render Mustache | Todo `send()` | `content` renderizado contra o `Notification` | Sim (conteúdo final gravado) |
372
+ | Stamp de `timestamp` | Todo `send()` | Grava data ISO no envio | Sim |
373
+ | Push mobile | `send()` com `player != null` | 1 push por device via AWS SNS | Não (transporte) |
374
+ | Consumo destrutivo | `receive()` / `GET ?delete=true&scope=private` | Lê e **apaga** as privadas do jogador | Sim (remoção) |
375
+ | Limpeza no clone de gamificação | `GameManager` com `removeLogs=true` | **Dropa a coleção `notification` inteira** (e a exclui do clone remoto) | Sim (destrutivo) |
376
+ | Cascade na exclusão de player | `PlayerDaoMongo.delete` | Tenta `remove("{userId:#}", userId)` — **mas o campo é `player.id`, não `userId`** → nenhum documento casa | Inefetiva (ver §7) |
377
+
378
+ #### Fluxo dos disparadores automáticos
379
+
380
+ ```mermaid
381
+ flowchart TD
382
+ A["Evento de negócio (ex.: desafio completo)"] --> B["getNotificationsByEvent(EVENT_WIN, def[])"]
383
+ B --> C{"scope == CUSTOM (99)?"}
384
+ C -- Sim --> X["Ignorado pelo disparador"]
385
+ C -- Não --> D["new Notification(player, item, def, time)"]
386
+ D --> E["NotificationManager.send(n)"]
387
+ E --> F["persiste + push (ver §2.2)"]
388
+ ```
389
+
390
+ ---
391
+
392
+ ## 7. Suportado vs NÃO Suportado
393
+
394
+ ### ✅ Suportado
395
+
396
+ - Persistência de notificações privadas e de newsfeed na coleção `notification`.
397
+ - Render de `content` com Mustache (variáveis em §3.4).
398
+ - Push mobile para privadas (AWS SNS → APNS/GCM), um por device registrado.
399
+ - Leitura paginada/filtrada via `GET /v3/notification` (player, scope, tag, janela temporal, `fields`, `max_results`).
400
+ - Contagem de privadas via `GET /v3/notification/totalUnread`.
401
+ - Criação avulsa (`POST /v3/notification`) e broadcast por segmento ou newsfeed (`POST /v3/notification/send`).
402
+ - Remoção em massa por jogador via `GET ?delete=true&scope=private` e caminho legado `receive()`.
403
+ - Isolamento multi-tenant por `apiKey`.
404
+
405
+ ### ❌ NÃO Suportado
406
+
407
+ - **`DELETE /v3/notification/:id`** — não existe. Nenhum `@DELETE` no controller.
408
+ - **Agendamento real (`when`)** — o parâmetro não adia entrega; só carimba `time`. Push e persistência são imediatos.
409
+ - **Notificações com `scope == CUSTOM (99)` via disparadores automáticos** — excluídas por `getNotificationsByEvent`. Só entregam por POST direto.
410
+ - **`EVENT_CUSTOM (99)`** — constante comentada, não implementada.
411
+ - **Tratamento diferenciado por `TYPE_VIDEO`** — `type` é apenas armazenado.
412
+ - **Flag de leitura/`read`** — não há campo de "lido". "Não lida" = privada ainda existente.
413
+ - **Cascade de exclusão de notificações ao apagar um player** — `PlayerDaoMongo.delete` usa `remove("{userId:#}", userId)`, mas o documento referencia o jogador por `player.id`. O campo `userId` é **legado** (consta no comentário de schema do método) e **não existe mais** no objeto. Resultado: **notificações órfãs** permanecem após exclusão do jogador.
414
+ - **`NewsContainer`** — buffer em memória totalmente comentado/morto.
415
+ - **`NewsManager.findAllAfterDate`** — não está ligado a nenhuma rota REST; leitura de newsfeed ocorre por `findAll`.
416
+
417
+ ### Delimitação — subpacote `notify/theme/` (fora de escopo deste módulo)
418
+
419
+ O pacote `com.funifier.engine.notify` contém um subpacote `theme/` (`ThemeNotify`, `ThemeNotifyConfig`, `ThemeGalleryNotify`, `ThemeNotifyConfigManager`, `ThemeNotifyDaoMongo`) que **não tem relação com o sistema de mensagens/notificações** documentado aqui. Trata-se de **theming visual de widgets** (CSS, script, gallery), persistido em coleções próprias — `theme_notify`, `theme_config_notify`, `theme_gallery_notify` — e **nunca toca** a coleção `notification` nem o `NotificationManager`. Está documentado em outro contexto (widgets/temas); citado apenas para que a leitura do pacote não pareça omissa.
420
+
421
+ ### Comportamentos confusos / legado relevante
422
+
423
+ - **NPE por `tag` nula:** `getNotifications(...)` e `getRandomNotification(...)` fazem `n.tag.indexOf(tag)` sem null-check. Se um filtro `tag` for passado e **qualquer** definição tiver `tag == null`, lança `NullPointerException`.
424
+ - **Mustache silencioso:** falha de template → stacktrace no stdout + `content = null`.
425
+ - **Duplo render:** no `POST /send` privado, `content` é renderizado duas vezes (uma com `{player}`, outra no `send()`).
426
+ - **`send()` em `NotificationManager` tem grande bloco comentado** (V1 de push via SNS/`Device`, observers `setChanged/notifyObservers`, `sendPrivate/sendPublic`) — arquitetura antiga desativada.
427
+
428
+ ---
429
+
430
+ ## 8. Segurança e Permissões
431
+
432
+ ### Autenticação e autorização
433
+
434
+ - Todas as rotas exigem **Bearer token** (`@BeanParam AuthBean`).
435
+ - `player == "me"` é resolvido para o jogador do token (`authBean.getPlayerFromTokenIfExist()`).
436
+ - A `apiKey` do token determina o tenant: `FrontController.getInstance(authBean.getApiKey())`.
437
+
438
+ ### Isolamento multi-tenant
439
+
440
+ - Cada gamificação tem sua própria conexão Jongo (`manager.getJongoConnection()`), portanto a coleção `notification` é fisicamente separada por tenant. Não há filtro de `apiKey` dentro das queries — o isolamento é garantido pela conexão.
441
+
442
+ ### Superfícies de injeção
443
+
444
+ - **`to` em `POST /send` é query Mongo raw** aplicada à coleção `player` (`findAllPlayers(..., to, ...)`). Um valor malicioso pode injetar **operadores** Mongo (ex.: `$where`, seleção ampla) para atingir jogadores não pretendidos. Não há sanitização visível neste caminho.
445
+ - **Template injection:** `content` é renderizado no servidor com Mustache tendo o `Notification` (e, no `/send`, o `Player`) como contexto. Mustache é lógica-less, mas conteúdo controlado externamente pode **vazar campos** do objeto de contexto (`{{player.*}}`). Trate `content` de origem não confiável com cuidado.
446
+ - **Sem rate limit explícito** no envio além do teto de 1000 jogadores por `/send`.
447
+
448
+ ---
449
+
450
+ ## 9. Observabilidade e Troubleshooting
451
+
452
+ ### Diagnóstico básico
453
+
454
+ - A notificação foi persistida? Consulte a coleção `notification` por `_id` ou `player.id`.
455
+ - O push não chegou? Verifique se há devices em `mobile_device` para `player.id` (`{player: "<id>"}`). Sem device → sem push, sem erro.
456
+ - A mensagem chegou com `null`? Template Mustache inválido (ver stacktrace no log do serviço).
457
+
458
+ ### Queries úteis (mongosh)
459
+
460
+ ```javascript
461
+ // Privadas de um jogador
462
+ db.notification.find({ "definition.scope": 0, "player.id": "ricardo@funifier.com" }).sort({ time: -1 })
463
+
464
+ // Mural (newsfeed) recente
465
+ db.notification.find({ "definition.scope": 1 }).sort({ time: -1 }).limit(10)
466
+
467
+ // Contagem de "não lidas" (privadas) de um jogador
468
+ db.notification.count({ "definition.scope": 0, "player.id": "ricardo@funifier.com" })
469
+
470
+ // Notificações com conteúdo nulo (suspeita de Mustache quebrado)
471
+ db.notification.find({ "definition.content": null })
472
+
473
+ // Notificações órfãs (jogador já excluído) — checar manualmente, pois a cascade por {userId} não as remove
474
+ db.notification.find({ "definition.scope": 0 }).forEach(n => { /* validar player.id contra a coleção player */ })
475
+
476
+ // Devices de um jogador (diagnóstico de push)
477
+ db.mobile_device.find({ player: "ricardo@funifier.com" })
478
+ ```
479
+
480
+ ### Comandos HTTP úteis
481
+
482
+ ```
483
+ GET /v3/notification?player=me&scope=private&published_min=-7d&max_results=50
484
+ GET /v3/notification?scope=newsfeed&published_min=-1d
485
+ GET /v3/notification/totalUnread?player=me
486
+ POST /v3/notification (corpo: Notification)
487
+ POST /v3/notification/send?to=extra.dept:"sales"&when=+0m (corpo: NotificationDefinition)
488
+ ```
489
+
490
+ ### Erros comuns e causas
491
+
492
+ | Sintoma | Causa provável |
493
+ | --- | --- |
494
+ | `GET` retorna vazio mas há dados | `published_min` omitido → janela de **1 hora**. Informe `published_min` |
495
+ | Notificação `private` "não foi criada" | `player` ausente → descartada silenciosamente em `send()` |
496
+ | `content` chega como `null` | Template Mustache inválido (`MustacheUtils.parse` retornou null) |
497
+ | `scope=public` virou newsfeed | Mapeamento aceita só `"private"`; qualquer outro valor → newsfeed |
498
+ | Push não enviado | Nenhum device em `mobile_device` para o `player.id`, ou notificação é newsfeed |
499
+ | `500` ao filtrar por `tag` | Alguma `NotificationDefinition` com `tag == null` (NPE em `indexOf`) |
500
+ | `?delete=true` não apagou | Faltou `player` ou `scope` diferente de `"private"` |
501
+
502
+ ---
503
+
504
+ ## 10. Exemplos Práticos
505
+
506
+ ### 10.1 Exemplo mínimo — mensagem privada para um jogador
507
+
508
+ ```json
509
+ POST /v3/notification
510
+ {
511
+ "player": { "id": "ricardo@funifier.com", "type": "player" },
512
+ "definition": { "scope": 0, "content": "Bem-vindo de volta!" }
513
+ }
514
+ ```
515
+
516
+ `scope: 0` (privada) + `player` presente → persiste e dispara push para os devices do jogador.
517
+
518
+ ### 10.2 Exemplo avançado — mensagem privada personalizada com contexto
519
+
520
+ ```json
521
+ POST /v3/notification
522
+ {
523
+ "player": { "id": "ricardo@funifier.com", "name": "Ricardo", "type": "player" },
524
+ "item": { "id": "578052b75a73038c5f3d0e77", "name": "Visit", "type": "action" },
525
+ "definition": {
526
+ "event": 0,
527
+ "type": 0,
528
+ "scope": 0,
529
+ "tag": "welcome",
530
+ "content": "Parabéns {{player.name}}! Você concluiu {{item.name}}."
531
+ }
532
+ }
533
+ ```
534
+
535
+ O `content` é renderizado para `"Parabéns Ricardo! Você concluiu Visit."` no envio.
536
+
537
+ ### 10.3 Exemplo avançado — broadcast segmentado
538
+
539
+ ```
540
+ POST /v3/notification/send?to=extra.dept:"sales"&when=+0m
541
+ ```
542
+ ```json
543
+ { "scope": 0, "content": "Olá {{player.name}}, sua meta foi atualizada." }
544
+ ```
545
+
546
+ Envia para até 1000 jogadores do segmento `extra.dept = "sales"`, renderizando o nome por jogador. Para o mural público, use `"scope": 1` (o `to` é ignorado e gera uma única entrada de newsfeed).
547
+
548
+ ### 10.4 Anti-pattern — usar `when` esperando agendamento
549
+
550
+ ```
551
+ POST /v3/notification/send?to=...&when=+1d ❌
552
+ ```
553
+
554
+ **Por quê é errado:** `when` **não adia** nada. A notificação é persistida e o push é disparado imediatamente; `when` só define o campo `time` (que pode até ficar no futuro, mas a entrega já aconteceu). Não confie nisso para campanhas agendadas.
555
+
556
+ ### 10.5 Anti-pattern — criar notificação privada sem `player`
557
+
558
+ ```json
559
+ POST /v3/notification
560
+ { "definition": { "scope": 0, "content": "mensagem" } } ❌
561
+ ```
562
+
563
+ **Por quê é errado:** sem `player`, a guarda de `send()` **descarta a notificação silenciosamente**. A resposta pode parecer `201`, mas nada é persistido nem entregue. Para mensagem pública use `scope: 1`.
564
+
565
+ ---
566
+
567
+ ## Checklist de Configuração
37
568
 
38
- - [ ] Notificação é disparada ao completar evento
39
- - [ ] Mensagem chega corretamente ao jogador
40
- - [ ] Variáveis dinâmicas são substituídas (nome, pontos, etc.)
569
+ - [ ] `definition.content` preenchido (vazio `400` no `/send`; ignorado no `POST` direto).
570
+ - [ ] Para `scope: 0` (privada): `player.id` presente — senão é descartada silenciosamente.
571
+ - [ ] Para personalizar com Mustache, usar apenas variáveis do contexto `Notification` (§3.4).
572
+ - [ ] Garantir que toda `NotificationDefinition` filtrável tenha `tag` definida (evita NPE em filtros por `tag`).
573
+ - [ ] Em leituras, **sempre** informar `published_min` — caso contrário só vê a última 1 hora.
574
+ - [ ] Não esperar agendamento de `when`: a entrega é imediata.
575
+ - [ ] Não esperar `DELETE /v3/notification/:id`: use `GET ?delete=true&scope=private`.
576
+ - [ ] Para push: confirmar que o jogador tem device registrado em `mobile_device`.
577
+ - [ ] Lembrar que `POST` com `_id` existente **sobrescreve** o documento (upsert).
578
+ - [ ] Notificações de jogadores excluídos ficam órfãs (cascade por `userId` é inefetiva) — limpar manualmente se necessário.