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,75 +1,593 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `team`
|
|
2
2
|
|
|
3
3
|
**Acesso Studio:** `/studio/team`
|
|
4
4
|
**API Endpoint:** `/v3/team`
|
|
5
|
+
**Coleção MongoDB:** `team` (coleções auxiliares: `principal`, `team_player`; coleção legada: `team_challenge`)
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
> Documentação de engenharia reversa baseada **exclusivamente** no código de `funifier-service` (pacote `com.funifier.engine.team` + `com.funifier.rest.v3.rest.TeamRest`). Reflete o comportamento real do runtime, não a documentação anterior.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
---
|
|
9
10
|
|
|
10
|
-
##
|
|
11
|
+
## 1. Visão Geral
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
- Para organizar jogadores por departamento, região ou função
|
|
14
|
-
- Para criar rankings por equipe
|
|
15
|
-
- Para atribuir desafios coletivos
|
|
13
|
+
O módulo `team` agrupa jogadores em **equipes**. Uma equipe é uma entidade de gamificação de primeira classe: ao ser criada, recebe um registro na coleção `principal` (`type = 1 / TYPE_TEAM`), o que permite que a equipe **participe de desafios, acumule pontos, itens e níveis exatamente como um jogador** — todos os totais da equipe são lançamentos da coleção `achievement` cujo campo `player` é o `_id` da equipe.
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
Papel arquitetural:
|
|
18
16
|
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
17
|
+
- **Agrupamento de principais.** A equipe é um `Principal` do tipo time. O `_id` da equipe entra no mesmo espaço de identificadores dos jogadores (coleção `principal`), e por isso pode ser alvo de `achievement`, `action_log`, `challenge_progress`, etc.
|
|
18
|
+
- **Membership (vínculo jogador↔equipe).** Existem dois mecanismos mutuamente exclusivos por equipe:
|
|
19
|
+
- **Manual** — vínculos persistidos na coleção `team_player` (uma linha `TeamLink` por par equipe/jogador), espelhados no array `player.teams[]`.
|
|
20
|
+
- **Dinâmico (v4)** — a equipe não tem vínculos persistidos; seus membros são **calculados em tempo de execução** rodando um pipeline de aggregation MongoDB (`Team.dynamic`) sobre uma coleção arbitrária.
|
|
21
|
+
- **Desafios de equipe.** Quando um jogador dispara uma ação, `AchievementManager.fireAction` reavalia a mesma ação para **cada equipe manual** do jogador, gerando achievements no nome da equipe (vide §2.5). É assim que uma equipe “ganha” pontos/desafios.
|
|
22
|
+
- **Status agregado.** `GET /v3/team/{id}/status` calcula o extrato de gamificação **da própria equipe** (não a soma dos membros) via `AchievementManager.calculateTeamStatus`.
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
Problemas que resolve:
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
- Competição/colaboração entre grupos (rankings por equipe via `crowning`/leaderboard).
|
|
27
|
+
- Organização de jogadores por departamento, região, função (manual) ou por critério calculado, ex.: “aniversariantes do dia”, “jogadores com score > X” (dinâmico).
|
|
28
|
+
- Atribuição de desafios coletivos (`Challenge.teamChallenge = true`).
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
**Método:** POST
|
|
33
|
-
**Endpoint:** `/v3/team`
|
|
30
|
+
Relação direta com outros módulos:
|
|
34
31
|
|
|
35
|
-
|
|
32
|
+
- `player` — `player.teams[]` é a fonte de verdade do membership manual; salvar um player re-sincroniza `team_player` (vide §2.4).
|
|
33
|
+
- `achievement` — `AchievementManager.fireAction` faz a recursão de desafios de equipe; `calculateTeamStatus` lê achievements da equipe.
|
|
34
|
+
- `principal` — toda equipe cria/atualiza um `Principal` espelho.
|
|
35
|
+
- `challenge` — `teamChallenge = true` direciona desafios para principais do tipo time.
|
|
36
|
+
- `crowning` — `findLeadersByPlayerId(teamId)` devolve posições de leaderboard da equipe.
|
|
37
|
+
- `competition` — reaproveita a classe `AutoJoin` (`com.funifier.engine.competition.AutoJoin`) como configuração de equipe dinâmica.
|
|
38
|
+
|
|
39
|
+
> **Nota de manutenção (no código):** o comentário em `AchievementManager.java:849` indica que a resolução de equipes do jogador está sendo migrada para Node.js (`"...quando o esau implementar no nodejs"`). O módulo Java aqui descrito é o comportamento vigente no `funifier-service`.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 2. Arquitetura e Fluxos
|
|
44
|
+
|
|
45
|
+
### 2.1 Classes envolvidas
|
|
46
|
+
|
|
47
|
+
| Classe | Arquivo | Papel |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `Team` | `engine/team/Team.java` | Entidade/POJO raiz — documento da coleção `team` |
|
|
50
|
+
| `TeamManager` | `engine/team/TeamManager.java` | Manager: CRUD, membership, resolução dinâmica |
|
|
51
|
+
| `TeamDaoMongo` | `engine/team/TeamDaoMongo.java` | Acesso MongoDB via Jongo; cascatas de delete/reset |
|
|
52
|
+
| `TeamLink` | `engine/team/TeamLink.java` | Vínculo jogador↔equipe persistido em `team_player` |
|
|
53
|
+
| `TeamStatus` | `engine/team/TeamStatus.java` | **Código morto** — definido mas nunca instanciado (vide §7) |
|
|
54
|
+
| `AutoJoin` | `engine/competition/AutoJoin.java` | Configuração de equipe dinâmica (`entity` + `aggregate`) |
|
|
55
|
+
| `Principal` | `engine/player/Principal.java` | Espelho da equipe na coleção `principal` (`type = 1`) |
|
|
56
|
+
| `TeamRest` | `rest/v3/rest/TeamRest.java` | Controller REST `/v3/team` |
|
|
57
|
+
|
|
58
|
+
Não há `TeamService` separado — a lógica de negócio mora inteiramente em `TeamManager`. O acesso ao Mongo é direto via Jongo dentro de `TeamDaoMongo`.
|
|
59
|
+
|
|
60
|
+
### 2.2 Pipeline de criação — `TeamManager.add(Team)`
|
|
61
|
+
|
|
62
|
+
Sequência real (`TeamManager.add`, linhas 34-49 → `TeamDaoMongo.add`, linhas 19-27):
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
[1] Se team.id é null ou vazio → team.id = Guid.shortTimeMillis()
|
|
66
|
+
[2] Se team.created é null → team.created = new Date()
|
|
67
|
+
[3] team.updated = new Date() (sempre sobrescreve)
|
|
68
|
+
[4] Monta Principal(id=team.id, name=team.name, type=TYPE_TEAM, teamId=team.id, userId=null)
|
|
69
|
+
[5] mongo.add:
|
|
70
|
+
- team collection → c.save(team) (Jongo save = UPSERT por _id)
|
|
71
|
+
- principal collection → cP.save(principal) (UPSERT por _id = team.id)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```mermaid
|
|
75
|
+
flowchart LR
|
|
76
|
+
A["POST /v3/team"] --> B{"id vazio?"}
|
|
77
|
+
B -- sim --> C["id = Guid.shortTimeMillis()"]
|
|
78
|
+
B -- nao --> D["mantem id"]
|
|
79
|
+
C --> E{"created null?"}
|
|
80
|
+
D --> E
|
|
81
|
+
E -- sim --> F["created = now"]
|
|
82
|
+
E -- nao --> G["mantem created"]
|
|
83
|
+
F --> H["updated = now"]
|
|
84
|
+
G --> H
|
|
85
|
+
H --> I["save(team) UPSERT"]
|
|
86
|
+
I --> J["save(Principal type=TEAM)"]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Observações de comportamento:
|
|
90
|
+
|
|
91
|
+
- **Não existe operação de update/PATCH.** `POST /v3/team` sempre chama `add`. Como `c.save` é **upsert por `_id`**, reenviar um POST com um `_id` existente faz **substituição total (full replace)** do documento. Se o corpo não trouxer `created`, o `created` é **reescrito com a data atual** (perde-se a data original).
|
|
92
|
+
- A geração de id no manager (`Guid.shortTimeMillis`) torna o fallback de `TeamDaoMongo.add` (`Guid.newShortGuid`, linha 20) um **ramo morto** — quando `mongo.add` roda, o id já foi definido.
|
|
93
|
+
- `add` é **síncrono** e **não dispara triggers nem webhooks** próprios do módulo team. A equipe só entra no fluxo de eventos quando passa a receber achievements (via desafios de equipe).
|
|
94
|
+
|
|
95
|
+
### 2.3 Fluxo de adição de membro (manual)
|
|
96
|
+
|
|
97
|
+
`TeamRest.addMember` → `TeamManager.linkUser` (linhas 188-202):
|
|
98
|
+
|
|
99
|
+
```mermaid
|
|
100
|
+
sequenceDiagram
|
|
101
|
+
participant C as Cliente
|
|
102
|
+
participant R as TeamRest.addMember
|
|
103
|
+
participant TM as TeamManager
|
|
104
|
+
participant DAO as TeamDaoMongo
|
|
105
|
+
participant PM as PlayerManager
|
|
106
|
+
C->>R: GET /v3/team/{id}/member/add/{playerId}
|
|
107
|
+
R->>TM: isUserOfTeam(playerId, id)?
|
|
108
|
+
alt ja e membro
|
|
109
|
+
R-->>C: 200 (no-op)
|
|
110
|
+
else nao e membro
|
|
111
|
+
R->>TM: linkUser(team, playerId)
|
|
112
|
+
TM->>DAO: linkUser -> save TeamLink em team_player
|
|
113
|
+
TM->>PM: player.teams += teamId; insert(player)
|
|
114
|
+
Note over PM: insert re-sincroniza team_player a partir de player.teams<br/>e chama updateUserStatus(playerId)
|
|
115
|
+
R-->>C: 200 {team, player}
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- A resposta é sempre `{ "team": id, "player": playerId }` — **não** retorna a equipe nem a lista de membros.
|
|
120
|
+
- `linkUser` deduplica (`teamIds.remove` seguido de `add`) antes de salvar o player.
|
|
121
|
+
- `POST /v3/team/{id}/member/bulk` (corpo = `["p1","p2",...]`) processa em laço e devolve um status por membro: `DONT_EXIST` (player inexistente), `MEMBER_ADDED`, `ALREADY_MEMBER`.
|
|
122
|
+
|
|
123
|
+
### 2.4 Re-sincronização `player.teams[]` → `team_player`
|
|
124
|
+
|
|
125
|
+
Ponto não óbvio: a coleção `team_player` é **derivada de `player.teams[]`** a cada save de player (`PlayerManager`, linhas 309-320):
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
[1] teamDao.deleteLinksByPlayer(player.id) → apaga TODOS os TeamLink do jogador
|
|
129
|
+
[2] para cada teamId em player.teams:
|
|
130
|
+
team = teamDao.findById(teamId)
|
|
131
|
+
se team != null E team.dynamic == null:
|
|
132
|
+
teamDao.linkUser(teamId, player.id) → recria o vínculo
|
|
133
|
+
[3] updateUserStatus(player.id)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Consequências:
|
|
137
|
+
|
|
138
|
+
- Editar `player.teams[]` (via `PUT/POST /v3/player`) é uma **forma alternativa e equivalente** de gerenciar membership manual.
|
|
139
|
+
- Equipes **dinâmicas são ignoradas** nesse passo (`team.dynamic == null`); um id de equipe dinâmica em `player.teams[]` é **silenciosamente descartado** (não vira `TeamLink`).
|
|
140
|
+
- Um id de equipe **inexistente** em `player.teams[]` também é descartado em silêncio.
|
|
141
|
+
- Como `TeamManager.linkUser`/`removeUsers` chamam `playerManager.insert(player)`, toda mutação de membership manual passa por essa re-sincronização completa + recálculo de status.
|
|
142
|
+
|
|
143
|
+
### 2.5 Desafios de equipe — recursão em `fireAction`
|
|
144
|
+
|
|
145
|
+
`AchievementManager.fireAction` (linhas 815-823), após avaliar os desafios do **jogador**:
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
se principal é PLAYER:
|
|
149
|
+
teamIds = findTeamIdsByUserId(userId) // AchievementManager:850 — SOMENTE team_player
|
|
150
|
+
para cada teamId:
|
|
151
|
+
fireAction(new ActionLog(actionId, teamId, time, attributes))
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Na chamada recursiva, o principal resolvido é a **equipe** (`type = TEAM`), então apenas desafios com `teamChallenge = true` são avaliados, e os achievements resultantes são gravados com `player = teamId`.
|
|
155
|
+
|
|
156
|
+
```mermaid
|
|
157
|
+
flowchart TB
|
|
158
|
+
A["Jogador dispara acao"] --> B["fireAction(player)"]
|
|
159
|
+
B --> C["Avalia desafios do jogador"]
|
|
160
|
+
C --> D{"principal e PLAYER?"}
|
|
161
|
+
D -- sim --> E["findTeamIdsByUserId(userId)<br/>SOMENTE team_player (manual)"]
|
|
162
|
+
E --> F["para cada teamId"]
|
|
163
|
+
F --> G["fireAction(teamId) recursivo"]
|
|
164
|
+
G --> H["Avalia desafios teamChallenge=true"]
|
|
165
|
+
H --> I["addAchievement(player=teamId)"]
|
|
166
|
+
D -- nao --> Z["fim"]
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
> **Assimetria crítica (vide §7):** a recursão usa `AchievementManager.findTeamIdsByUserId` (linha 850), que consulta **apenas** `team_player`. Equipes **dinâmicas não entram nessa lista**, logo **membros de equipes dinâmicas nunca disparam desafios de equipe**. Isto difere de `TeamManager.findTeamIdsByUserId` (linha 204), que inclui dinâmicas — e que é o método usado por `GET /v3/player/{id}/team`.
|
|
170
|
+
|
|
171
|
+
Quando o principal é uma equipe, o passo de “update status” é um **TODO vazio** (`AchievementManager.java:807-809`): **não existe `player_status` materializado para equipes**. O status da equipe é sempre calculado sob demanda.
|
|
172
|
+
|
|
173
|
+
### 2.6 Resolução de membership — `findUserIdsByTeamId`
|
|
174
|
+
|
|
175
|
+
```mermaid
|
|
176
|
+
flowchart LR
|
|
177
|
+
A["findUserIdsByTeamId(teamId)"] --> B["findById(teamId)"]
|
|
178
|
+
B --> C{"team.dynamic.entity E<br/>team.dynamic.aggregate definidos?"}
|
|
179
|
+
C -- sim --> D["roda pipeline em dynamic.entity<br/>coleta _id de cada doc"]
|
|
180
|
+
C -- nao --> E["team_player.distinct(linkId)<br/>where teamId"]
|
|
181
|
+
D --> F["lista de ids dinamica"]
|
|
182
|
+
E --> G["lista de ids manual"]
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`findDynamicMemberIds` (linhas 318-337): desserializa `dynamic.aggregate` (string JSON) numa lista de estágios, executa `collection(dynamic.entity).aggregate(estágio[0])` encadeando `.and(estágio[i])`, e coleta o campo `_id` de cada resultado. `isDynamicMember` (linhas 339-355) faz o mesmo e anexa um estágio final `{$match:{_id: <user>}}` para testar pertinência de um jogador específico.
|
|
186
|
+
|
|
187
|
+
### 2.7 Status da equipe — `calculateTeamStatus`
|
|
188
|
+
|
|
189
|
+
`AchievementManager.calculateTeamStatus(teamId)` (linhas 1086-1129) retorna um objeto **`PlayerStatus`** (não `TeamStatus`), tratando o `teamId` como se fosse um `player`:
|
|
190
|
+
|
|
191
|
+
| Campo do status | Origem |
|
|
192
|
+
|---|---|
|
|
193
|
+
| `player`, `name`, `image`, `extra` | Copiados do documento `team` |
|
|
194
|
+
| `total_challenges`, `challenges` | `achievement` `type=CHALLENGE` onde `player = teamId` |
|
|
195
|
+
| `total_points`, `point_categories` | `sumTotalPoints(teamId)` / `getTotalPointsGroupedByType(teamId)` |
|
|
196
|
+
| `total_catalog_items`, `catalog_items` | `achievement` `type=CATALOG_ITEM` onde `player = teamId` |
|
|
197
|
+
| `challenge_progress` | `challenge_progress` onde `player = teamId` |
|
|
198
|
+
| `positions` | `crowningManager.findLeadersByPlayerId(teamId)` (envolto em try/catch silencioso) |
|
|
199
|
+
| `time` | `new Date()` |
|
|
200
|
+
|
|
201
|
+
> O status reflete **as conquistas da própria equipe** (geradas por desafios de equipe), **não** a soma das conquistas individuais dos membros.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## 3. Estrutura dos Objetos
|
|
206
|
+
|
|
207
|
+
### 3.1 `Team` — documento raiz (coleção `team`)
|
|
208
|
+
|
|
209
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
210
|
+
|---|---|---|---|---|
|
|
211
|
+
| `_id` | String | auto (`Guid.shortTimeMillis()`) | — | Id da equipe. Se ausente/vazio no POST, é gerado. |
|
|
212
|
+
| `name` | String | — | não (recomendado) | Nome de exibição. Copiado para o `Principal`. |
|
|
213
|
+
| `description` | String | — | não | Descrição livre. |
|
|
214
|
+
| `image` | `Image` | — | não | Estrutura `{small, medium, original}`, cada um `{url}`. |
|
|
215
|
+
| `extra` | `Map<String,Object>` | `{}` | não | Atributos arbitrários. Persistido e devolvido. Lido em `calculateTeamStatus`. |
|
|
216
|
+
| `owner` | String | — | não | **Persistido mas nunca lido** em nenhum ponto do código (campo decorativo — vide §7). |
|
|
217
|
+
| `created` | Date | `now` no 1º save | — | Definido por `add` apenas se null; reescrito em re-POST sem `created`. |
|
|
218
|
+
| `updated` | Date | `now` | — | Reescrito em todo `add`/`update`. |
|
|
219
|
+
| `dynamic` | `AutoJoin` | — | não | Configuração de equipe dinâmica (v4). Se presente e válido, a equipe não usa `team_player`. |
|
|
220
|
+
|
|
221
|
+
**Getters/setters:** a entidade só expõe getters/setters para `id`, `name`, `image`, `description`. Os campos `extra`, `owner`, `created`, `updated`, `dynamic` são **públicos** e mapeados diretamente pelo Jackson.
|
|
222
|
+
|
|
223
|
+
`@JsonIgnoreProperties(ignoreUnknown = true)` — campos desconhecidos no corpo do POST são **ignorados silenciosamente**.
|
|
224
|
+
|
|
225
|
+
### 3.2 `AutoJoin` — configuração dinâmica (`Team.dynamic`)
|
|
226
|
+
|
|
227
|
+
| Campo | Tipo | Descrição |
|
|
228
|
+
|---|---|---|
|
|
229
|
+
| `entity` | String | Coleção MongoDB onde o pipeline é executado (ex.: `"player"`). |
|
|
230
|
+
| `aggregate` | String | **String JSON** com um array de estágios de aggregation. Deve retornar documentos cujo `_id` é o id do membro (ex.: `[{"$project":{"_id":1}}]`). |
|
|
231
|
+
|
|
232
|
+
Ambos `entity` e `aggregate` precisam estar **não-nulos** para a equipe ser tratada como dinâmica (`TeamManager:240, 319, 340`). Caso contrário, cai no caminho manual.
|
|
233
|
+
|
|
234
|
+
### 3.3 `TeamLink` — vínculo manual (coleção `team_player`)
|
|
235
|
+
|
|
236
|
+
| Campo | Tipo | Descrição |
|
|
237
|
+
|---|---|---|
|
|
238
|
+
| `_id` | String | `Guid.newShortGuid()` por vínculo. |
|
|
239
|
+
| `teamId` | String | Id da equipe. |
|
|
240
|
+
| `linkId` | String | Id do jogador (membro). |
|
|
241
|
+
|
|
242
|
+
### 3.4 `Principal` — espelho da equipe (coleção `principal`)
|
|
243
|
+
|
|
244
|
+
Criado/atualizado a cada `add`. Para equipes: `_id = teamId`, `name = team.name`, `type = 1 (TYPE_TEAM)`, `teamId = teamId`, `userId = null`.
|
|
245
|
+
|
|
246
|
+
**Enum `Principal.type`:**
|
|
247
|
+
|
|
248
|
+
| Valor | Constante | Significado |
|
|
249
|
+
|---|---|---|
|
|
250
|
+
| `0` | `TYPE_USER` | Principal é um jogador |
|
|
251
|
+
| `1` | `TYPE_TEAM` | Principal é uma equipe |
|
|
252
|
+
|
|
253
|
+
### 3.5 `TeamStatus` — subentidade **inativa**
|
|
254
|
+
|
|
255
|
+
`TeamStatus extends Team` adiciona um booleano `joined`. **Nunca é instanciada** em lugar nenhum do repositório (vide §7). O endpoint de status retorna `PlayerStatus`, não esta classe.
|
|
256
|
+
|
|
257
|
+
### 3.6 Coleções escritas pelo módulo
|
|
258
|
+
|
|
259
|
+
| Coleção | Conteúdo | Quem escreve |
|
|
260
|
+
|---|---|---|
|
|
261
|
+
| `team` | Documentos `Team` | `add`/`update` |
|
|
262
|
+
| `principal` | Espelho `Principal` (type=1) | `add`/`update`/`delete` |
|
|
263
|
+
| `team_player` | Vínculos `TeamLink` manuais | `linkUser`/`removeUsers`/sync do player |
|
|
264
|
+
| `team_challenge` | **Legado** — vínculos via `linkChallenge` (`@Deprecated`) | Não usado em fluxo ativo (§7) |
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## 4. Endpoints
|
|
269
|
+
|
|
270
|
+
Todos os endpoints exigem `Authorization: Bearer <token>` (`@BeanParam AuthBean`). O tenant é resolvido por `FrontController.getInstance(authBean.getApiKey())` — isolamento por organização (§8).
|
|
271
|
+
|
|
272
|
+
### `GET /v3/team` — listar/consultar equipes
|
|
273
|
+
|
|
274
|
+
| Aspecto | Detalhe |
|
|
275
|
+
|---|---|
|
|
276
|
+
| Finalidade | Lista paginada de equipes (`findAllPaginated`) |
|
|
277
|
+
| Autenticação | Bearer token |
|
|
278
|
+
| Paginação | Header `Range: items=0-100` (default 0-100); `max_results` default 100 |
|
|
279
|
+
|
|
280
|
+
**Query params:**
|
|
281
|
+
|
|
282
|
+
| Param | Tipo | Descrição |
|
|
283
|
+
|---|---|---|
|
|
284
|
+
| `team` | String | Ids separados por vírgula → `{_id:{$in:[...]}}`. |
|
|
285
|
+
| `name` | String | Filtro `{name:{$regex:<name>, $options:'i'}}` (substring, case-insensitive). |
|
|
286
|
+
| `q` | String | **Critério MongoDB cru** injetado diretamente no `$match` (`", " + q`). Ex.: `extra.country:"USA"`. Superfície de injeção (§8). |
|
|
287
|
+
| `fields` | String | Projeção `$project` (campos separados por vírgula). |
|
|
288
|
+
| `orderby` | String | Campo de ordenação (`$sort`). |
|
|
289
|
+
| `reverse` | boolean | `true` → ordem decrescente. |
|
|
290
|
+
| `max_results` | int | Limite (`$limit`); `<=0` vira 100. |
|
|
291
|
+
|
|
292
|
+
**Comportamento real:** monta um pipeline de aggregation `[$match, ($project), ($sort), ($limit)]`. A resposta é o envelope paginado padrão (`Callback.paginatedCallback`).
|
|
293
|
+
|
|
294
|
+
### `GET /v3/team/{id}` — obter uma equipe
|
|
295
|
+
|
|
296
|
+
Retorna o documento `Team` (campos null removidos via `JsonUtil.toJsonRemoveNullFields`). Não embute membros.
|
|
297
|
+
|
|
298
|
+
### `GET /v3/team/{id}/status` — extrato de gamificação da equipe
|
|
299
|
+
|
|
300
|
+
Retorna um `PlayerStatus` calculado por `calculateTeamStatus` (§2.7). Reflete as conquistas **da equipe**, não dos membros.
|
|
301
|
+
|
|
302
|
+
### `POST /v3/team` — criar/substituir equipe
|
|
303
|
+
|
|
304
|
+
| Aspecto | Detalhe |
|
|
305
|
+
|---|---|
|
|
306
|
+
| Finalidade | `TeamManager.add` (upsert por `_id`) |
|
|
307
|
+
| Full replace ou patch | **Full replace** (não há PATCH). Re-POST com `_id` existente substitui o documento inteiro. |
|
|
308
|
+
| Status | `201 CREATED`; corpo = a equipe (com `_id`/`created`/`updated` preenchidos) |
|
|
309
|
+
|
|
310
|
+
**Exemplo de request:**
|
|
36
311
|
```json
|
|
37
312
|
{
|
|
38
313
|
"_id": "sales",
|
|
39
314
|
"name": "Sales",
|
|
40
315
|
"description": "Funifier sales team",
|
|
41
316
|
"image": {
|
|
42
|
-
"small": {"url": "https://my.funifier.com/
|
|
43
|
-
"medium": {"url": "https://my.funifier.com/
|
|
44
|
-
"original": {"url": "https://my.funifier.com/
|
|
317
|
+
"small": {"url": "https://my.funifier.com/funny.png"},
|
|
318
|
+
"medium": {"url": "https://my.funifier.com/funny.png"},
|
|
319
|
+
"original": {"url": "https://my.funifier.com/funny.png"}
|
|
45
320
|
},
|
|
46
|
-
"extra": {
|
|
321
|
+
"extra": {"country": "USA"},
|
|
47
322
|
"owner": "john"
|
|
48
323
|
}
|
|
49
324
|
```
|
|
50
325
|
|
|
51
|
-
###
|
|
52
|
-
|
|
53
|
-
|
|
326
|
+
### `DELETE /v3/team/{id}` — excluir equipe (cascata)
|
|
327
|
+
|
|
328
|
+
`TeamManager.delete` (linhas 66-72) → `removeUsers(membros)` → `mongo.delete`:
|
|
329
|
+
- remove `principal {teamId:id}`
|
|
330
|
+
- remove `team {_id:id}`
|
|
331
|
+
- remove `team_player {teamId:id}`
|
|
332
|
+
- executa `reset(id)` (vide abaixo)
|
|
333
|
+
|
|
334
|
+
Resposta `204 NO_CONTENT`.
|
|
335
|
+
|
|
336
|
+
### `DELETE /v3/team/{id}/reset` — **reset destrutivo**
|
|
337
|
+
|
|
338
|
+
`TeamManager.reset` → `TeamDaoMongo.reset` (linhas 37-46) apaga, tratando `id` como id de jogador, **8 coleções**:
|
|
339
|
+
|
|
340
|
+
| Coleção | Filtro |
|
|
341
|
+
|---|---|
|
|
342
|
+
| `action_log` | `{userId:id}` |
|
|
343
|
+
| `mystery_box_log` | `{player:id}` |
|
|
344
|
+
| `player_status` | `{_id:id}` |
|
|
345
|
+
| `achievement` | `{player:id}` |
|
|
346
|
+
| `challenge_progress` | `{player:id}` |
|
|
347
|
+
| `lottery_ticket` | `{player:id}` |
|
|
348
|
+
| `competition_join` | `{player:id}` |
|
|
349
|
+
| `folder_log` | `{player:id}` |
|
|
350
|
+
|
|
351
|
+
Resposta `200 OK`.
|
|
352
|
+
|
|
353
|
+
> **Atenção:** não há validação de que `id` é uma equipe. Se um `id` de **jogador** for passado, os dados de gamificação **desse jogador** são apagados. Vide anti-pattern em §10.
|
|
354
|
+
|
|
355
|
+
### `GET /v3/team/{id}/member` — listar membros (objetos `Player`)
|
|
356
|
+
|
|
357
|
+
Resolve ids via `findUserIdsByTeamId` (manual **ou** dinâmico) e carrega cada `Player`. Custo O(n) de `findById` por membro.
|
|
358
|
+
|
|
359
|
+
### `GET /v3/team/{id}/memberids` — listar ids dos membros
|
|
360
|
+
|
|
361
|
+
Mesma resolução, devolve apenas a lista de ids.
|
|
362
|
+
|
|
363
|
+
### `GET /v3/team/{id}/member/add/{playerId}` — adicionar membro
|
|
364
|
+
|
|
365
|
+
`isUserOfTeam` → se não membro, `linkUser` (§2.3). Resposta `{team, player}`. **Não tem efeito útil em equipes dinâmicas** (membership é calculado; o `TeamLink` criado seria ignorado na resolução dinâmica).
|
|
366
|
+
|
|
367
|
+
### `POST /v3/team/{id}/member/bulk` — adicionar membros em lote
|
|
368
|
+
|
|
369
|
+
Corpo: array de ids `["p1","p2"]`. Retorna `{team, members:[{member, status}]}` com `status ∈ {DONT_EXIST, MEMBER_ADDED, ALREADY_MEMBER}`. Status `201 CREATED`.
|
|
370
|
+
|
|
371
|
+
### `GET /v3/team/{id}/member/remove/{playerId}` — remover membro
|
|
372
|
+
|
|
373
|
+
`isUserOfTeam` → se membro, `removeUsers([playerId], id)` (remove `TeamLink` + atualiza `player.teams`). Resposta `200 OK`.
|
|
374
|
+
|
|
375
|
+
### Endpoints de membership no controller de player
|
|
376
|
+
|
|
377
|
+
São equivalentes funcionais aos de `team` (mesmos métodos de `TeamManager`):
|
|
378
|
+
|
|
379
|
+
| Método | Endpoint | Ação |
|
|
380
|
+
|---|---|---|
|
|
381
|
+
| GET | `/v3/player/{id}/team` | Lista equipes do jogador (`TeamManager.findTeamIdsByUserId` — **inclui dinâmicas**) |
|
|
382
|
+
| GET | `/v3/player/{id}/team/join/{teamId}` | `linkUser` (entrar) |
|
|
383
|
+
| GET | `/v3/player/{id}/team/unjoin/{teamId}` | `removeUsers` (sair) |
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## 5. Regras de Negócio
|
|
388
|
+
|
|
389
|
+
Regras presentes no código e **não** evidentes pelo schema:
|
|
390
|
+
|
|
391
|
+
- **Membership manual vs dinâmico é exclusivo por equipe.** Se `dynamic.entity` e `dynamic.aggregate` estão definidos, a equipe **ignora** completamente `team_player`; vínculos manuais criados nessa equipe não têm efeito na resolução de membros.
|
|
392
|
+
- **`player.teams[]` é a fonte de verdade do membership manual.** `team_player` é reconstruído a partir dele em todo save de player (§2.4).
|
|
393
|
+
- **Equipe é um principal.** Pontos/desafios/itens da equipe vivem em `achievement` com `player = teamId`. Não há campo agregado de “pontos da equipe” no documento `team`.
|
|
394
|
+
- **Desafios de equipe só alcançam membros manuais** (assimetria de §2.5/§7).
|
|
395
|
+
- **Idempotência de criação:** `POST` é upsert; reenviar com mesmo `_id` substitui o documento (e zera `created` se omitido).
|
|
396
|
+
- **Sem update parcial:** a única forma de “editar” é re-POSTar o objeto completo.
|
|
397
|
+
- **Multi-tenant:** todo acesso é escopado pela `apiKey` do bearer (instância isolada de `ManagerFactory`/conexão Mongo). Não há vazamento entre organizações.
|
|
398
|
+
- **`name` propaga para `principal`:** em `update` (não exposto) e em saves de player, o nome do principal é mantido em sincronia; em `add`, o principal é (re)criado com o nome atual.
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## 6. Comportamentos Automáticos
|
|
403
|
+
|
|
404
|
+
| Comportamento | Trigger | Impacto | Persistência |
|
|
405
|
+
|---|---|---|---|
|
|
406
|
+
| Geração de `_id` | `add` com id vazio | `Guid.shortTimeMillis()` | `team._id` |
|
|
407
|
+
| Criação do `Principal` espelho | `add` | Equipe vira principal `type=1` | coleção `principal` |
|
|
408
|
+
| `created`/`updated` automáticos | `add` | Datas preenchidas/reescritas | `team` |
|
|
409
|
+
| Sync `player.teams[]` → `team_player` | save de qualquer player | Reescreve vínculos manuais (ignora dinâmicas) | `team_player` |
|
|
410
|
+
| Recálculo de status do jogador | `linkUser`/`removeUsers` (via `insert`) | `updateUserStatus(playerId)` | `player_status` |
|
|
411
|
+
| Recursão de desafio de equipe | `fireAction` de um jogador | Achievements no nome da equipe (só membros manuais) | `achievement` |
|
|
412
|
+
| Cascata de delete | `DELETE /v3/team/{id}` | Remove principal+team+team_player e roda reset | múltiplas coleções |
|
|
413
|
+
| Reset destrutivo | `DELETE /v3/team/{id}/reset` ou delete | Apaga 8 coleções pelo id | múltiplas coleções |
|
|
414
|
+
|
|
415
|
+
```mermaid
|
|
416
|
+
flowchart TB
|
|
417
|
+
A["DELETE /v3/team/{id}"] --> B["findUserIdsByTeamId(id)"]
|
|
418
|
+
B --> C["removeUsers(membros)<br/>limpa team_player + player.teams"]
|
|
419
|
+
C --> D["remove principal {teamId:id}"]
|
|
420
|
+
D --> E["remove team {_id:id}"]
|
|
421
|
+
E --> F["remove team_player {teamId:id}"]
|
|
422
|
+
F --> G["reset(id)"]
|
|
423
|
+
G --> H["apaga 8 colecoes:<br/>action_log, mystery_box_log, player_status,<br/>achievement, challenge_progress,<br/>lottery_ticket, competition_join, folder_log"]
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
## 7. Suportado vs NÃO Suportado
|
|
429
|
+
|
|
430
|
+
### ✅ Suportado
|
|
431
|
+
|
|
432
|
+
- CRUD de equipes (criar/substituir via POST, ler, listar paginado, excluir).
|
|
433
|
+
- Membership manual: add/remove individual, add em lote (`bulk`), via endpoints de team **ou** de player.
|
|
434
|
+
- Equipes dinâmicas (membros calculados por pipeline de aggregation).
|
|
435
|
+
- Equipe como principal de gamificação (pontos/desafios/itens/níveis via `achievement`).
|
|
436
|
+
- Desafios de equipe (`Challenge.teamChallenge = true`) para **membros manuais**.
|
|
437
|
+
- Status agregado da equipe (`/status`) e posições de leaderboard.
|
|
438
|
+
- Reset destrutivo e cascata de delete.
|
|
439
|
+
|
|
440
|
+
### ❌ NÃO Suportado / armadilhas confirmadas no código
|
|
441
|
+
|
|
442
|
+
- **Update parcial / PUT.** `TeamManager.update` existe (remove+save + atualiza nome do principal) mas **não está ligado a nenhum endpoint** (nenhum chamador no repositório). “Editar” = re-POST full replace.
|
|
443
|
+
- **`TeamStatus` é código morto.** A classe e seu campo `joined` nunca são instanciados (`grep "new TeamStatus"` = 0 ocorrências). O endpoint `/status` devolve `PlayerStatus`.
|
|
444
|
+
- **Campo `owner` é decorativo.** Persistido e devolvido, mas **nenhum código o lê** — não confere permissão, propriedade nem qualquer comportamento.
|
|
445
|
+
- **Desafios de equipe não alcançam membros dinâmicos.** `AchievementManager.findTeamIdsByUserId` (linha 850) consulta só `team_player`; equipes dinâmicas ficam de fora da recursão de `fireAction`.
|
|
446
|
+
- **Sem `player_status` materializado para equipes.** `fireAction` tem `// TODO UPDATE TEAM STATUS` vazio; status só existe sob demanda via `calculateTeamStatus`.
|
|
447
|
+
- **`TeamDaoMongo.findByLikeName` está quebrado:** consulta `{team: {$regex:...}}`, mas o campo correto é `name` — sempre retorna vazio. Método **não usado** por nenhum endpoint.
|
|
448
|
+
- **`Team.equals` usa `==` em String** (`this.id == t.id`) — comparação por referência, não por valor. Não está em caminho crítico, mas é incorreto.
|
|
449
|
+
- **Coleção/fluxo `team_challenge` é legado.** `linkChallenge` é `@Deprecated`; `removeTeamsOfChallenge` filtra `{challengeId:#}` enquanto `TeamLink` só tem `linkId`/`teamId` — o filtro nunca casa. Bloco de métodos comentados em `TeamDaoMongo` (findTeamIdsByUserId, findByUserId, findByChallengeId, isUserInChallengeTeams, isUserOfTeam) foi movido para `TeamManager`.
|
|
450
|
+
- **`findAll`, `findLimited`, `findByLikeName` (variantes não paginadas)** não são chamados por nenhum endpoint — `GET /v3/team` usa exclusivamente `findAllPaginated`.
|
|
451
|
+
- **Sem hooks/triggers/webhooks próprios** do módulo team (criação/edição/exclusão de equipe não dispara eventos). Eventos só surgem indiretamente via achievements de desafio de equipe.
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## 8. Segurança e Permissões
|
|
456
|
+
|
|
457
|
+
- **Autenticação:** todos os endpoints exigem `Authorization: Bearer <token>` (`AuthBean`). A `apiKey` extraída do token seleciona a instância de `FrontController`/`ManagerFactory`, isolando dados por **organização/tenant**.
|
|
458
|
+
- **Isolamento multi-tenant:** todas as queries rodam na conexão Mongo do tenant; não há cruzamento entre organizações.
|
|
459
|
+
- **Superfícies de injeção (requerem token válido, normalmente de administração):**
|
|
460
|
+
- `GET /v3/team?q=...` — o parâmetro `q` é **concatenado cru** no `$match` da aggregation (`TeamManager.java:105`). Um cliente autorizado pode injetar operadores arbitrários do Mongo.
|
|
461
|
+
- `Team.dynamic.aggregate` — é uma **string JSON de pipeline executada como aggregation** sobre `dynamic.entity` (coleção arbitrária). Quem cria/edita equipes dinâmicas controla um pipeline Mongo arbitrário (`findDynamicMemberIds`/`isDynamicMember`). Não há sanitização nem allowlist de coleções/operadores.
|
|
462
|
+
- **Operações destrutivas sem confirmação:**
|
|
463
|
+
- `DELETE /v3/team/{id}/reset` apaga 8 coleções **pelo id**, sem checar se o id é de equipe — pode apagar dados de um **jogador** homônimo.
|
|
464
|
+
- `DELETE /v3/team/{id}` remove a equipe e roda o mesmo reset.
|
|
465
|
+
- **Sem autorização granular por equipe:** o campo `owner` não é verificado; qualquer token com acesso ao endpoint pode editar/excluir qualquer equipe do tenant.
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
## 9. Observabilidade e Troubleshooting
|
|
470
|
+
|
|
471
|
+
### Diagnóstico rápido
|
|
472
|
+
|
|
473
|
+
```http
|
|
474
|
+
GET /v3/team/{id} # a equipe existe? confira _id, dynamic
|
|
475
|
+
GET /v3/team/{id}/memberids # quem o runtime considera membro (manual ou dinâmico)
|
|
476
|
+
GET /v3/team/{id}/status # a equipe acumulou pontos/desafios? (achievements com player=teamId)
|
|
477
|
+
GET /v3/player/{id}/team # de quais equipes o jogador participa (inclui dinâmicas)
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Queries MongoDB úteis
|
|
481
|
+
|
|
482
|
+
```js
|
|
483
|
+
// Vínculos manuais de uma equipe
|
|
484
|
+
db.team_player.find({ teamId: "sales" })
|
|
485
|
+
|
|
486
|
+
// Equipes de um jogador (manual)
|
|
487
|
+
db.team_player.distinct("teamId", { linkId: "john" })
|
|
488
|
+
|
|
489
|
+
// O principal espelho existe? (necessário para desafios de equipe)
|
|
490
|
+
db.principal.findOne({ _id: "sales", type: 1 })
|
|
491
|
+
|
|
492
|
+
// Conquistas geradas pela equipe (desafios de equipe)
|
|
493
|
+
db.achievement.find({ player: "sales" })
|
|
494
|
+
|
|
495
|
+
// Todas as equipes dinâmicas
|
|
496
|
+
db.team.find({ "dynamic.entity": { $exists: true }, "dynamic.aggregate": { $exists: true } })
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### Erros comuns e causas
|
|
500
|
+
|
|
501
|
+
| Sintoma | Causa provável |
|
|
502
|
+
|---|---|
|
|
503
|
+
| Equipe não ganha pontos em desafio de equipe | Equipe é **dinâmica** (recursão de `fireAction` só vê `team_player`); ou `Challenge.teamChallenge` não é `true`; ou falta o `principal` (recriar via re-POST). |
|
|
504
|
+
| `/memberids` vazio numa equipe dinâmica | `dynamic.aggregate` não retorna documentos com `_id`, ou `dynamic.entity` errada, ou JSON do pipeline inválido. |
|
|
505
|
+
| Membro adicionado “some” | Save de player sobrescreveu `player.teams[]` e re-sincronizou `team_player` (§2.4); ou a equipe é dinâmica e o `TeamLink` é ignorado. |
|
|
506
|
+
| `created` mudou após “editar” | Re-POST sem o campo `created` (full replace reescreve a data). |
|
|
507
|
+
| Dados de gamificação de um jogador sumiram | Alguém chamou `DELETE /v3/team/{id}/reset` com um id que coincide com o do jogador. |
|
|
508
|
+
| Filtro por nome não funciona via `findByLikeName` | Método legado quebrado (campo `team` em vez de `name`); use `GET /v3/team?name=...`. |
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## 10. Exemplos Práticos
|
|
513
|
+
|
|
514
|
+
### Exemplo mínimo (equipe manual)
|
|
515
|
+
|
|
516
|
+
```http
|
|
517
|
+
POST /v3/team
|
|
518
|
+
Authorization: Bearer <token>
|
|
519
|
+
Content-Type: application/json
|
|
520
|
+
|
|
521
|
+
{ "_id": "sales", "name": "Sales" }
|
|
522
|
+
```
|
|
523
|
+
```http
|
|
524
|
+
GET /v3/team/sales/member/add/john # adiciona "john"
|
|
525
|
+
GET /v3/team/sales/memberids # => ["john"]
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Exemplo avançado (equipe dinâmica)
|
|
529
|
+
|
|
530
|
+
Equipe cujos membros são, em tempo real, todos os jogadores com `extra.department == "sales"`:
|
|
531
|
+
|
|
532
|
+
```http
|
|
533
|
+
POST /v3/team
|
|
534
|
+
Authorization: Bearer <token>
|
|
535
|
+
Content-Type: application/json
|
|
536
|
+
|
|
537
|
+
{
|
|
538
|
+
"_id": "sales_dynamic",
|
|
539
|
+
"name": "Sales (dinâmico)",
|
|
540
|
+
"description": "Todos os jogadores do departamento de vendas",
|
|
541
|
+
"image": { "small": {"url": "https://cdn/sales.png"} },
|
|
542
|
+
"extra": { "managedBy": "rule" },
|
|
543
|
+
"dynamic": {
|
|
544
|
+
"entity": "player",
|
|
545
|
+
"aggregate": "[{\"$match\":{\"extra.department\":\"sales\"}},{\"$project\":{\"_id\":1}}]"
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
- `GET /v3/team/sales_dynamic/memberids` executa o pipeline e devolve os `_id` casados.
|
|
551
|
+
- **Não** adicione membros manualmente nessa equipe — o vínculo seria ignorado.
|
|
552
|
+
- Lembre-se: membros dinâmicos **não** disparam desafios de equipe (§2.5).
|
|
54
553
|
|
|
55
|
-
###
|
|
56
|
-
|
|
57
|
-
|
|
554
|
+
### Exemplo de membership via player
|
|
555
|
+
|
|
556
|
+
```http
|
|
557
|
+
GET /v3/player/john/team/join/sales # entra na equipe
|
|
558
|
+
GET /v3/player/john/team # lista equipes de john (inclui dinâmicas)
|
|
559
|
+
GET /v3/player/john/team/unjoin/sales # sai
|
|
560
|
+
```
|
|
58
561
|
|
|
59
|
-
###
|
|
60
|
-
**Método:** GET
|
|
61
|
-
**Endpoint:** `/v3/team/:team_id/member/remove/:player_id`
|
|
562
|
+
### Anti-pattern — `reset` confundido com “limpar membros”
|
|
62
563
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
564
|
+
```http
|
|
565
|
+
# ERRADO: a intenção era "remover todos os membros da equipe"
|
|
566
|
+
DELETE /v3/team/john/reset
|
|
567
|
+
```
|
|
568
|
+
Se `john` for (ou coincidir com) um **id de jogador**, este comando **apaga as conquistas, action_logs, status, tickets e progresso do jogador** em 8 coleções — irreversível. Para esvaziar uma equipe, remova membros individualmente (`/member/remove/{playerId}`) ou exclua a equipe (`DELETE /v3/team/{id}`), ciente de que o delete **também** roda o reset.
|
|
569
|
+
|
|
570
|
+
### Anti-pattern — “editar” com PATCH mental
|
|
571
|
+
|
|
572
|
+
```http
|
|
573
|
+
# Não existe PATCH. Isto NÃO atualiza só o nome — substitui o documento inteiro:
|
|
574
|
+
POST /v3/team
|
|
575
|
+
{ "_id": "sales", "name": "Novo Nome" }
|
|
576
|
+
# Resultado: description, image, extra, owner e created (se omitido) são PERDIDOS/reescritos.
|
|
577
|
+
```
|
|
578
|
+
Sempre reenvie o objeto **completo** ao re-POSTar.
|
|
66
579
|
|
|
67
|
-
|
|
68
|
-
**Método:** GET
|
|
69
|
-
**Endpoint:** `/v3/team/:id/status`
|
|
580
|
+
---
|
|
70
581
|
|
|
71
|
-
##
|
|
582
|
+
## Checklist de Configuração
|
|
72
583
|
|
|
73
|
-
- [ ]
|
|
74
|
-
- [ ]
|
|
75
|
-
- [ ]
|
|
584
|
+
- [ ] `_id` definido (ou aceitar o gerado automaticamente).
|
|
585
|
+
- [ ] `name` preenchido (propaga para o `principal`).
|
|
586
|
+
- [ ] Para desafios de equipe: equipe é **manual** (`team_player`) — equipes **dinâmicas não recebem desafios de equipe**.
|
|
587
|
+
- [ ] Para desafios de equipe: existe `principal` com `type=1` (recriado via POST) e o `Challenge` tem `teamChallenge=true`.
|
|
588
|
+
- [ ] Equipe dinâmica: `dynamic.entity` aponta para coleção existente e `dynamic.aggregate` é JSON válido retornando documentos com `_id`.
|
|
589
|
+
- [ ] **Não** misture membership manual com `dynamic` na mesma equipe (manual é ignorado).
|
|
590
|
+
- [ ] Ao “editar”, reenviar o objeto **completo** (full replace) e incluir `created` se quiser preservá-lo.
|
|
591
|
+
- [ ] **Nunca** use `/{id}/reset` para esvaziar membros — ele apaga dados de gamificação do id (tratado como jogador).
|
|
592
|
+
- [ ] Não dependa de `owner` para autorização — o campo não tem efeito.
|
|
593
|
+
- [ ] `q` em `GET /v3/team` é critério Mongo cru — valide a entrada se exposto a clientes não confiáveis.
|