funifier-mcp 0.2.26 → 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.
- package/.cursor/rules/funifier.mdc +38 -41
- package/.github/copilot-instructions.md +38 -41
- package/AGENTS.md +56 -49
- package/README.md +40 -22
- package/datasource-funifier-docs/.coverage.json +326 -0
- package/datasource-funifier-docs/.validation.json +593 -0
- package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
- package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
- package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
- package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
- package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
- package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
- package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
- package/datasource-funifier-docs/knowledge/index.md +4 -1
- package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
- package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
- package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
- package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
- package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
- package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
- package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
- package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
- package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
- package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
- package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
- package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
- package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
- package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
- package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
- package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
- package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
- package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
- package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
- package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
- package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
- package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
- package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
- package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
- package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
- package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
- package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
- package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
- package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
- package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
- package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
- package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
- package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
- package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
- package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
- package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
- package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
- package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
- package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
- package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
- package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +42 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/init.test.js +74 -3
- package/dist/cli/init.test.js.map +1 -1
- package/dist/cli/persona.d.ts +3 -0
- package/dist/cli/persona.d.ts.map +1 -0
- package/dist/cli/persona.js +25 -0
- package/dist/cli/persona.js.map +1 -0
- package/dist/mcp/bundle.js +119 -93
- package/dist/mcp/index.js +2 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/resources/documentation.d.ts +1 -1
- package/dist/mcp/resources/documentation.d.ts.map +1 -1
- package/dist/mcp/resources/documentation.js +39 -3
- package/dist/mcp/resources/documentation.js.map +1 -1
- package/dist/mcp/tools/connect.d.ts.map +1 -1
- package/dist/mcp/tools/connect.js +18 -8
- package/dist/mcp/tools/connect.js.map +1 -1
- package/dist/mcp/tools/database.d.ts.map +1 -1
- package/dist/mcp/tools/database.js +59 -47
- package/dist/mcp/tools/database.js.map +1 -1
- package/dist/mcp/tools/database.test.js +2 -2
- package/dist/mcp/tools/database.test.js.map +1 -1
- package/dist/mcp/tools/delete.d.ts.map +1 -1
- package/dist/mcp/tools/delete.js +13 -3
- package/dist/mcp/tools/delete.js.map +1 -1
- package/dist/mcp/tools/execute.d.ts.map +1 -1
- package/dist/mcp/tools/execute.js +20 -9
- package/dist/mcp/tools/execute.js.map +1 -1
- package/dist/mcp/tools/folder.d.ts.map +1 -1
- package/dist/mcp/tools/folder.js +22 -12
- package/dist/mcp/tools/folder.js.map +1 -1
- package/dist/mcp/tools/get.d.ts.map +1 -1
- package/dist/mcp/tools/get.js +16 -6
- package/dist/mcp/tools/get.js.map +1 -1
- package/dist/mcp/tools/index.d.ts +1 -1
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +3 -1
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/list.d.ts.map +1 -1
- package/dist/mcp/tools/list.js +38 -14
- package/dist/mcp/tools/list.js.map +1 -1
- package/dist/mcp/tools/logs.d.ts.map +1 -1
- package/dist/mcp/tools/logs.js +15 -5
- package/dist/mcp/tools/logs.js.map +1 -1
- package/dist/mcp/tools/save.d.ts.map +1 -1
- package/dist/mcp/tools/save.js +14 -4
- package/dist/mcp/tools/save.js.map +1 -1
- package/dist/mcp/tools/save.test.js +3 -3
- package/dist/mcp/tools/save.test.js.map +1 -1
- package/dist/mcp/tools/search-docs.d.ts +3 -0
- package/dist/mcp/tools/search-docs.d.ts.map +1 -0
- package/dist/mcp/tools/search-docs.js +102 -0
- package/dist/mcp/tools/search-docs.js.map +1 -0
- package/package.json +6 -2
- package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
- package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
- package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
- package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
- package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
- package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
- package/skills/funifier/SKILL.md +88 -0
- package/skills/funifier/references/configure-security.md +96 -0
- package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
- package/skills/funifier/references/create-aggregate.md +144 -0
- package/skills/funifier/references/create-challenge.md +116 -0
- package/skills/funifier/references/create-competition.md +98 -0
- package/skills/funifier/references/create-crossword.md +574 -0
- package/skills/funifier/references/create-custom-object.md +91 -0
- package/skills/funifier/references/create-custom-page.md +135 -0
- package/skills/funifier/references/create-folder.md +104 -0
- package/skills/funifier/references/create-lastmile.md +643 -0
- package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
- package/skills/funifier/references/create-level.md +94 -0
- package/skills/funifier/references/create-lottery.md +913 -0
- package/skills/funifier/references/create-mystery.md +769 -0
- package/skills/funifier/references/create-notification.md +75 -0
- package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
- package/skills/funifier/references/create-quiz.md +98 -0
- package/skills/funifier/references/create-scheduler.md +141 -0
- package/skills/funifier/references/create-story.md +636 -0
- package/skills/funifier/references/create-swap.md +95 -0
- package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
- package/skills/funifier/references/create-virtual-good.md +96 -0
- package/skills/funifier/references/create-webhook.md +72 -0
- package/skills/funifier/references/create-websocket.md +71 -0
- package/skills/funifier/references/create-widget.md +76 -0
- package/skills/funifier/references/debug.md +87 -0
- package/skills/funifier/references/help.md +81 -0
- package/skills/funifier/references/implement-frontend.md +106 -0
- package/skills/funifier/references/import-csv.md +75 -0
- package/skills/funifier/references/manage-player.md +82 -0
- package/skills/funifier/references/manage-team.md +76 -0
- package/skills/funifier/references/upload-file.md +91 -0
- package/skills/funifier-create-aggregate/SKILL.md +0 -127
- package/skills/funifier-create-challenge/SKILL.md +0 -88
- package/skills/funifier-create-custom-page/SKILL.md +0 -127
- package/skills/funifier-create-level/SKILL.md +0 -87
- package/skills/funifier-create-quiz/SKILL.md +0 -87
- package/skills/funifier-create-scheduler/SKILL.md +0 -127
- package/skills/funifier-create-virtual-good/SKILL.md +0 -87
- package/skills/funifier-debug/SKILL.md +0 -92
- package/skills/funifier-help/SKILL.md +0 -86
- package/skills/funifier-implement-frontend/SKILL.md +0 -90
- package/skills/funifier-index/SKILL.md +0 -58
|
@@ -1,40 +1,578 @@
|
|
|
1
|
-
#
|
|
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
|
-
|
|
7
|
+
---
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
## 1. Visão Geral
|
|
8
10
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
**Método:** POST
|
|
30
|
-
**Endpoint:** `/v3/notification`
|
|
316
|
+
---
|
|
31
317
|
|
|
32
|
-
|
|
33
|
-
**Método:** DELETE
|
|
34
|
-
**Endpoint:** `/v3/notification/:id`
|
|
318
|
+
**`POST /v3/notification/send`** — `send` (broadcast)
|
|
35
319
|
|
|
36
|
-
|
|
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
|
-
- [ ]
|
|
39
|
-
- [ ]
|
|
40
|
-
- [ ]
|
|
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.
|