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.
Files changed (170) hide show
  1. package/.cursor/rules/funifier.mdc +38 -41
  2. package/.github/copilot-instructions.md +38 -41
  3. package/AGENTS.md +56 -49
  4. package/README.md +40 -22
  5. package/datasource-funifier-docs/.coverage.json +326 -0
  6. package/datasource-funifier-docs/.validation.json +593 -0
  7. package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
  8. package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
  9. package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
  10. package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
  11. package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
  12. package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
  13. package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
  14. package/datasource-funifier-docs/knowledge/index.md +4 -1
  15. package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
  16. package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
  17. package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
  18. package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
  19. package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
  20. package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
  21. package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
  22. package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
  23. package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
  24. package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
  25. package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
  26. package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
  27. package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
  28. package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
  29. package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
  30. package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
  31. package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
  32. package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
  33. package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
  34. package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
  35. package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
  36. package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
  37. package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
  38. package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
  39. package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
  40. package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
  41. package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
  42. package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
  43. package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
  44. package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
  45. package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
  46. package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
  47. package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
  48. package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
  49. package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
  50. package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
  51. package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
  52. package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
  53. package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
  54. package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
  55. package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
  56. package/dist/cli/init.d.ts.map +1 -1
  57. package/dist/cli/init.js +42 -1
  58. package/dist/cli/init.js.map +1 -1
  59. package/dist/cli/init.test.js +74 -3
  60. package/dist/cli/init.test.js.map +1 -1
  61. package/dist/cli/persona.d.ts +3 -0
  62. package/dist/cli/persona.d.ts.map +1 -0
  63. package/dist/cli/persona.js +25 -0
  64. package/dist/cli/persona.js.map +1 -0
  65. package/dist/mcp/bundle.js +119 -93
  66. package/dist/mcp/index.js +2 -2
  67. package/dist/mcp/index.js.map +1 -1
  68. package/dist/mcp/resources/documentation.d.ts +1 -1
  69. package/dist/mcp/resources/documentation.d.ts.map +1 -1
  70. package/dist/mcp/resources/documentation.js +39 -3
  71. package/dist/mcp/resources/documentation.js.map +1 -1
  72. package/dist/mcp/tools/connect.d.ts.map +1 -1
  73. package/dist/mcp/tools/connect.js +18 -8
  74. package/dist/mcp/tools/connect.js.map +1 -1
  75. package/dist/mcp/tools/database.d.ts.map +1 -1
  76. package/dist/mcp/tools/database.js +59 -47
  77. package/dist/mcp/tools/database.js.map +1 -1
  78. package/dist/mcp/tools/database.test.js +2 -2
  79. package/dist/mcp/tools/database.test.js.map +1 -1
  80. package/dist/mcp/tools/delete.d.ts.map +1 -1
  81. package/dist/mcp/tools/delete.js +13 -3
  82. package/dist/mcp/tools/delete.js.map +1 -1
  83. package/dist/mcp/tools/execute.d.ts.map +1 -1
  84. package/dist/mcp/tools/execute.js +20 -9
  85. package/dist/mcp/tools/execute.js.map +1 -1
  86. package/dist/mcp/tools/folder.d.ts.map +1 -1
  87. package/dist/mcp/tools/folder.js +22 -12
  88. package/dist/mcp/tools/folder.js.map +1 -1
  89. package/dist/mcp/tools/get.d.ts.map +1 -1
  90. package/dist/mcp/tools/get.js +16 -6
  91. package/dist/mcp/tools/get.js.map +1 -1
  92. package/dist/mcp/tools/index.d.ts +1 -1
  93. package/dist/mcp/tools/index.d.ts.map +1 -1
  94. package/dist/mcp/tools/index.js +3 -1
  95. package/dist/mcp/tools/index.js.map +1 -1
  96. package/dist/mcp/tools/list.d.ts.map +1 -1
  97. package/dist/mcp/tools/list.js +38 -14
  98. package/dist/mcp/tools/list.js.map +1 -1
  99. package/dist/mcp/tools/logs.d.ts.map +1 -1
  100. package/dist/mcp/tools/logs.js +15 -5
  101. package/dist/mcp/tools/logs.js.map +1 -1
  102. package/dist/mcp/tools/save.d.ts.map +1 -1
  103. package/dist/mcp/tools/save.js +14 -4
  104. package/dist/mcp/tools/save.js.map +1 -1
  105. package/dist/mcp/tools/save.test.js +3 -3
  106. package/dist/mcp/tools/save.test.js.map +1 -1
  107. package/dist/mcp/tools/search-docs.d.ts +3 -0
  108. package/dist/mcp/tools/search-docs.d.ts.map +1 -0
  109. package/dist/mcp/tools/search-docs.js +102 -0
  110. package/dist/mcp/tools/search-docs.js.map +1 -0
  111. package/package.json +6 -2
  112. package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
  113. package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
  114. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
  115. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
  116. package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
  117. package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
  118. package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
  119. package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
  120. package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
  121. package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
  122. package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
  123. package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
  124. package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
  125. package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
  126. package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
  127. package/skills/funifier/SKILL.md +88 -0
  128. package/skills/funifier/references/configure-security.md +96 -0
  129. package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
  130. package/skills/funifier/references/create-aggregate.md +144 -0
  131. package/skills/funifier/references/create-challenge.md +116 -0
  132. package/skills/funifier/references/create-competition.md +98 -0
  133. package/skills/funifier/references/create-crossword.md +574 -0
  134. package/skills/funifier/references/create-custom-object.md +91 -0
  135. package/skills/funifier/references/create-custom-page.md +135 -0
  136. package/skills/funifier/references/create-folder.md +104 -0
  137. package/skills/funifier/references/create-lastmile.md +643 -0
  138. package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
  139. package/skills/funifier/references/create-level.md +94 -0
  140. package/skills/funifier/references/create-lottery.md +913 -0
  141. package/skills/funifier/references/create-mystery.md +769 -0
  142. package/skills/funifier/references/create-notification.md +75 -0
  143. package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
  144. package/skills/funifier/references/create-quiz.md +98 -0
  145. package/skills/funifier/references/create-scheduler.md +141 -0
  146. package/skills/funifier/references/create-story.md +636 -0
  147. package/skills/funifier/references/create-swap.md +95 -0
  148. package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
  149. package/skills/funifier/references/create-virtual-good.md +96 -0
  150. package/skills/funifier/references/create-webhook.md +72 -0
  151. package/skills/funifier/references/create-websocket.md +71 -0
  152. package/skills/funifier/references/create-widget.md +76 -0
  153. package/skills/funifier/references/debug.md +87 -0
  154. package/skills/funifier/references/help.md +81 -0
  155. package/skills/funifier/references/implement-frontend.md +106 -0
  156. package/skills/funifier/references/import-csv.md +75 -0
  157. package/skills/funifier/references/manage-player.md +82 -0
  158. package/skills/funifier/references/manage-team.md +76 -0
  159. package/skills/funifier/references/upload-file.md +91 -0
  160. package/skills/funifier-create-aggregate/SKILL.md +0 -127
  161. package/skills/funifier-create-challenge/SKILL.md +0 -88
  162. package/skills/funifier-create-custom-page/SKILL.md +0 -127
  163. package/skills/funifier-create-level/SKILL.md +0 -87
  164. package/skills/funifier-create-quiz/SKILL.md +0 -87
  165. package/skills/funifier-create-scheduler/SKILL.md +0 -127
  166. package/skills/funifier-create-virtual-good/SKILL.md +0 -87
  167. package/skills/funifier-debug/SKILL.md +0 -92
  168. package/skills/funifier-help/SKILL.md +0 -86
  169. package/skills/funifier-implement-frontend/SKILL.md +0 -90
  170. package/skills/funifier-index/SKILL.md +0 -58
@@ -1,200 +1,669 @@
1
- # Leaderboard (Ranking)
1
+ # `leaderboard`
2
2
 
3
3
  **Acesso Studio:** `/studio/leaderboard`
4
4
  **API Endpoint:** `/v3/leaderboard`
5
+ **Coleção MongoDB:** `leaderboard`
6
+ **Coleções auxiliares:** `leader` (resultado calculado), `leader_header` (metadados do cálculo), `expire` (controle de TTL do cache) — **compartilhadas com o módulo de crowning**.
5
7
 
6
- ## O que é
8
+ > Documentação de engenharia reversa. Baseada no código de `funifier-service` (commit `830037e`).
9
+ > Classes principais: `LeaderBoard_V2`, `Operation`, `Period`, `Filter` (pacote `engine.crowning`); `Leader` (`engine.leader`); `LeaderBoardManager` (`engine.leader`); `CrowningManager` (`engine.crowning`); `LeaderboardRest` (`rest.v3.rest`).
7
10
 
8
- Rankings que comparam o desempenho dos jogadores por pontos, contagem de ações, atributos ou métricas customizadas. Podem ser individuais ou por equipe, em tempo real ou por período fixo.
11
+ ---
9
12
 
10
- ## Técnica
13
+ ## 1. Visão Geral
11
14
 
12
- Leaderboard usa **sempre** `"techniques": ["GT03"]` é o único código válido e é obrigatório.
15
+ Leaderboard é um ranking que ordena **jogadores** ou **equipes** por um valor calculado a partir de dados de gamificação: contagem de ações, soma/média de atributos de ações, soma de conquistas (pontos, challenges, etc.) ou um KPI pré-preparado. O ranking é sempre relativo a um **período** (janela relativa ou intervalo fixo) e compara a posição atual com a do período anterior (`previous_position`, `move`).
13
16
 
14
- ## Tabela de Referência dos Enums
17
+ A entidade de configuração é `LeaderBoard_V2`, persistida na coleção `leaderboard`. O **resultado** do ranking não é calculado em cada requisição; ele é materializado na coleção `leader`, com metadados em `leader_header` e validade controlada por um registro na coleção `expire`. Quando o cache expira, o ranking é recalculado.
15
18
 
16
- ### `principalType` quem é rankeado
19
+ ### Particularidade arquitetural central: dois motores de cálculo distintos
17
20
 
18
- | Valor | Label |
19
- |---|---|
20
- | `0` | Player (jogadores individuais) |
21
- | `1` | Team (equipes) |
21
+ O módulo possui **duas implementações independentes** de cálculo de líderes, acionadas por endpoints diferentes, com comportamentos divergentes. Entender essa divisão é o ponto mais importante deste módulo:
22
22
 
23
- ### `operation.type` como calcular o score
23
+ | Motor | Endpoint | Classe / método | Triggers | Tipos de operação suportados | Cache (TTL default) |
24
+ | --- | --- | --- | --- | --- | --- |
25
+ | **A — V3 "leaders"** | `GET /v3/leaderboard/{id}/leaders` | `CrowningManager.calculateLeaderBoardV3` | dispara `before_render` e `after_render` | player: 1,2,3,10 (**type 4 quebra**); team: 1,2,3,4,10 | `+1h` |
26
+ | **B — "aggregate"** | `POST /v3/leaderboard/{id}/leader/aggregate` | `LeaderBoardManager.findLiveLeadersAggregate` / `findCacheLeadersAggregate` | nenhum | player: 1,2,3,10 (**type 4 quebra**); team: 1,2,3,10 **mas filtro de equipe está hardcoded/quebrado** | `+3h` |
24
27
 
25
- | Valor | Label | Uso |
26
- |---|---|---|
27
- | `1` | Count Actions | Conta quantas vezes a action foi executada |
28
- | `2` | Sum of attributes | Soma um atributo numérico de uma action |
29
- | `3` | Sum achievements | Soma pontos/conquistas acumulados (**mais comum**) |
30
- | `4` | Average of attributes | Média de um atributo numérico de uma action |
31
- | `10` | Prepared KPI | Usa um KPI pré-configurado |
28
+ Os dois motores **não compartilham cache** (usam chaves de referência diferentes na coleção `expire`) e produzem resultados com formatos de campo ligeiramente diferentes. Detalhes nas seções 2, 5 e 7.
32
29
 
33
- ### `operation.achievement_type` tipo de conquista (somente quando `operation.type = 3`)
30
+ ### Relação com outros módulos
34
31
 
35
- | Valor | Label |
36
- |---|---|
32
+ - **`action`/`action_log`** operações `Count Actions`, `Sum/Avg attribute` agregam sobre `action_log`.
33
+ - **`achievement`** — operação `Sum achievements` agrega sobre a coleção `achievement` (pontos, challenges, levels, etc.).
34
+ - **`team`/`team_player`** — rankings de equipe usam `team_player` (membros) e `team` (nome/imagem).
35
+ - **`player`** — nomes e imagens dos jogadores; jogadores não registrados são removidos do resultado (motor A).
36
+ - **`challenge_rule_prepared`** — operação `Prepared KPI` (type 10) executa um pipeline de agregação armazenado.
37
+ - **`trigger`** — eventos `before_render`/`after_render` (somente motor A).
38
+ - **`package`** — `PackageManager` exporta/importa leaderboards (`findAll`, `findById`, `addLeaderboard`, `delete`).
39
+ - **`crowning`** — feature separada (coroamento de 1 posição) que compartilha o pacote, os helpers de cálculo e as coleções `leader`/`leader_header`/`expire`. **Não confundir** com leaderboard.
40
+
41
+ ---
42
+
43
+ ## 2. Arquitetura e Fluxos
44
+
45
+ ### 2.1 Pipeline de persistência (criar / atualizar / excluir)
46
+
47
+ **Criar/atualizar** — `POST /v3/leaderboard` → `LeaderBoardManager.addLeaderboard` (`LeaderBoardManager.java:94`):
48
+
49
+ 1. Se `_id` ausente/vazio → gera `Guid.shortTimeMillis()`.
50
+ 2. `c.save(board)` na coleção `leaderboard` — **upsert/full-replace**, não patch.
51
+ 3. **Purga o cache do board** (side effect): remove de `leader` (`{boardId:#}`), de `expire` (`{extra.item:#}`) e de `leader_header` (`{board:#}`).
52
+ 4. Resposta: o próprio objeto com campos nulos removidos (`JsonUtil.toJsonRemoveNullFields`), status `201`.
53
+
54
+ **Excluir** — `DELETE /v3/leaderboard/{id}` → `LeaderBoardManager.delete` (`LeaderBoardManager.java:108`): remove o board e executa a mesma purga das 3 coleções auxiliares. Status `204`.
55
+
56
+ > Não há validação de `operation`, `period` ou `principalType` no insert. Configurações inválidas são aceitas silenciosamente e só falham no momento do cálculo (ver seções 5 e 7).
57
+
58
+ ### 2.2 Motor A — `GET /v3/leaderboard/{id}/leaders`
59
+
60
+ `LeaderboardRest.findLeaders` (`LeaderboardRest.java:241`) → `CrowningManager.calculateLeaderBoardV3` (`CrowningManager.java:109`) + `findHeaderV3`.
61
+
62
+ Sequência:
63
+
64
+ 1. **Monta período de consulta.** Se `time_amount`+`time_scale` informados → `Period` variável; senão se `start_date`+`end_date` → `Period` fixo; senão usa o `period` default do board. Esse período sobrescreve `board.period` e entra na **chave de cache** (`getSearchId`).
65
+ 2. `reference = getSearchId(board, sub)` = `"leaderboard_" + board.id + "_" + sub + "_" + board.period.toString()`.
66
+ 3. Busca `expire` por `{extra.reference:#}`.
67
+ 4. **Se cache expirou ou não existe:**
68
+ a. dispara trigger `before_render` na coleção `leaderboard`;
69
+ b. `calculateLeaderBoardLeadersIfNecessaryV3` (recalcula — ver 2.3);
70
+ c. dispara trigger `after_render`.
71
+ 5. Consulta a coleção `leader` por `{extra.cache: expire.id}`, aplica filtro de view, ordena por `position` e limita.
72
+ 6. Monta resposta com `leaderboard`, dados do header (`creation_datetime`, `expiration_datetime`, `start_datetime`, `end_datetime`, `operation_unit`) e `leaders`.
73
+
74
+ #### Fluxo de cálculo — Motor A (`/leaders`)
75
+
76
+ ```mermaid
77
+ flowchart TD
78
+ A["GET /{id}/leaders"] --> B["monta Period da query<br/>(time_scale/start_date) ou usa board.period"]
79
+ B --> C["reference = getSearchId(board, sub)"]
80
+ C --> D{"expire existe<br/>e não expirou?"}
81
+ D -- sim --> J["consulta coleção leader<br/>{extra.cache: expire.id}"]
82
+ D -- não --> E["trigger before_render"]
83
+ E --> F["calculateLeaderBoardLeadersIfNecessaryV3"]
84
+ F --> G["trigger after_render"]
85
+ G --> J
86
+ J --> K["aplica view (FRIENDS/MIDDLE)"]
87
+ K --> L["sort position + limit"]
88
+ L --> M["resposta: leaderboard + header + leaders"]
89
+ ```
90
+
91
+ ### 2.3 Recálculo do Motor A — `calculateLeaderBoardLeadersIfNecessaryV3`
92
+
93
+ Método `synchronized` (`CrowningManager.java:196`):
94
+
95
+ 1. Cria registro `expire` se não existir (`extra.reference`, `extra.type="leaderboard"`, `extra.item=board.id`).
96
+ 2. Se expirado: remove `leader` antigos (`{extra.cache: expire.id}`).
97
+ 3. **Calcula faixas de tempo.** Trata **apenas** `TYPE_VARIABLE` e `TYPE_FIXED`. ⚠️ **`period.expression` é ignorado aqui** (diferente de `Period.getCurrentRange`, usado pelo Motor B).
98
+ - `currentRange` = janela atual; `previewRange` = janela anterior (mesmo tamanho, imediatamente antes).
99
+ 4. **Player** (`principalType=0`):
100
+ - `findPlayerIds(board.principals)` resolve principals (cada um pode ser jogador ou equipe) em ids de usuário; se vazio → todos.
101
+ - `excludePrincipals` = todos os `teamId` (remove equipes do ranking de jogadores).
102
+ - `calculatePlayerLeadersHelper(operation, currentRange, ...)` e idem para `previewRange`.
103
+ - Para cada líder: resolve `Player`, define `position` (1-based), `name`, `image` (`image.medium.url`), `boardId`, `extra.cache`. **Jogadores não encontrados na coleção `player` são descartados.**
104
+ - Calcula `previous_position`/`previous_total` cruzando com a janela anterior e define `move` (ver pseudocódigo).
105
+ 5. **Team** (`principalType=1`):
106
+ - `linkedTeams` é passado **vazio** (`new ArrayList<>()`, `CrowningManager.java:292` — `findAllTeamsByLeaderboard` comentado), então `calculateTeamLeadersHelper` usa **todas** as equipes.
107
+ - `board.principals` é **ignorado** para equipes.
108
+ 6. Salva cada `Leader` na coleção `leader`.
109
+ 7. Define `expire.expireAt` = `board.timeout` (keyword) ou **`+1h`** default.
110
+ 8. Monta `LeaderHeader` (datas + `unit`):
111
+ - `Sum achievements` → `unit = findOptionByTypeAndItem(achievement_type, item)`.
112
+ - `Count actions`/`Sum attribute` → `unit = {type:"action", item, title, image}` (+ `attribute` no caso de sum attribute).
113
+ - **Sem `unit` para `Avg attribute` (4) e `Prepared KPI` (10).**
114
+
115
+ Pseudocódigo do cálculo de `move` (Motor A):
116
+
117
+ ```
118
+ se position == previous_position: move = "stop"
119
+ senão se position < previous_position
120
+ OU previous_position == 0: move = "up" // novo entrante sobe
121
+ senão se position > previous_position: move = "down"
122
+ ```
123
+
124
+ ### 2.4 Motor B — `POST /v3/leaderboard/{id}/leader/aggregate`
125
+
126
+ `LeaderboardRest.findAllLeadersAggregate` (`LeaderboardRest.java:352`). Aceita um corpo JSON com **estágios de agregação extras** (array) que são anexados ao pipeline do ranking.
127
+
128
+ ```mermaid
129
+ flowchart TD
130
+ A["POST /{id}/leader/aggregate?live="] --> B{"live=true?"}
131
+ B -- "true" --> C["findLiveLeadersAggregate<br/>(pipeline Mongo em tempo real)"]
132
+ B -- "false (default)" --> D["findCacheLeadersAggregate"]
133
+ D --> E{"cache válido?"}
134
+ E -- não --> F["recalcula via findLiveLeadersAggregate<br/>insere em leader em lotes de 5000"]
135
+ E -- sim --> G["$match {extra.cache: expire.id}"]
136
+ F --> G
137
+ C --> H["applyViewLeadersAggregate (FRIENDS/MIDDLE)"]
138
+ G --> H
139
+ H --> I["anexa estágios de agregação do corpo"]
140
+ I --> J["PaginationUtil (Range header) → resposta paginada"]
141
+ ```
142
+
143
+ - **`live=true`** → `findLiveLeadersAggregate` (`LeaderBoardManager.java:302`): monta o pipeline Mongo completo em tempo real (não persiste).
144
+ - **`live=false` / ausente** → `findCacheLeadersAggregate` (`LeaderBoardManager.java:146`):
145
+ - `reference = "leaderboard_" + id` (+ `"_" + period` se `period` informado). ⚠️ **Chave diferente da do Motor A.**
146
+ - Se expirado: cria/atualiza `expire`, recalcula via `findLiveLeadersAggregate`, remove `leader` antigos (`{extra.cache: expire.id}`), insere os novos **em lotes de 5.000** na coleção `leader`. TTL = `board.timeout` ou **`+3h`** default.
147
+ - Retorna `$match {extra.cache: expire.id}` sobre `leader`.
148
+
149
+ Estrutura interna do pipeline `findLiveLeadersAggregate` (por operação): um `$facet` com `p1` (janela atual) e `p2` (janela anterior), `$group` por jogador/equipe, ordenação com desempate por `time`, `$unwind` com `includeArrayIndex` para gerar `position`, `$cmp` para calcular `move`, `$lookup` em `player`/`team` para nome/imagem (`image.small.url`), e marcação `extra.cache` + `_id` composto `"<id>_<cache>"`.
150
+
151
+ Cálculo de `move` no Motor B: `move = $cmp(prev_pos, position)` com `prev_pos` default `9999999` quando nulo → `1`→`"up"`, `0`→`"stop"`, `-1`→`"down"`.
152
+
153
+ ### 2.5 Interação entre componentes (Motor A)
154
+
155
+ ```mermaid
156
+ sequenceDiagram
157
+ participant C as Cliente
158
+ participant R as LeaderboardRest
159
+ participant CM as CrowningManager
160
+ participant T as TriggerManager
161
+ participant DB as MongoDB (leader/expire/leader_header)
162
+
163
+ C->>R: GET /{id}/leaders
164
+ R->>CM: calculateLeaderBoardV3(board, sub, ...)
165
+ CM->>DB: findOne expire {extra.reference}
166
+ alt cache expirado
167
+ CM->>T: execute(board, "before_render")
168
+ CM->>CM: calculateLeaderBoardLeadersIfNecessaryV3
169
+ CM->>DB: remove + save leaders, save expire, save header
170
+ CM->>T: execute(board, "after_render")
171
+ end
172
+ CM->>DB: aggregate leader {extra.cache}
173
+ CM-->>R: List<Leader>
174
+ R->>CM: findHeaderV3(board, sub)
175
+ R-->>C: { leaderboard, header, leaders }
176
+ ```
177
+
178
+ ---
179
+
180
+ ## 3. Estrutura dos Objetos
181
+
182
+ ### 3.1 `LeaderBoard_V2` — documento raiz (coleção `leaderboard`)
183
+
184
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
185
+ | --- | --- | --- | --- | --- |
186
+ | `_id` | String | auto (`Guid.shortTimeMillis()` se ausente) | — | Identificador. POST com `_id` existente faz **full replace**. |
187
+ | `title` | String | — | recomendado | Título exibido. |
188
+ | `description` | String | — | não | Descrição. **Persiste, mas não tem getter/setter** (acesso por campo via Jackson). |
189
+ | `principalType` | int | `0` | sim (na prática) | `0` = Player, `1` = Team. |
190
+ | `operation` | `Operation` | — | sim (para cálculo) | Define como calcular o valor. Ver 3.2. |
191
+ | `period` | `Period` | — | sim (para cálculo) | Janela de tempo do ranking. Ver 3.3. |
192
+ | `principals` | List<String> | `null` | não | Restringe o escopo a jogadores/equipes específicos. **Usado só em rankings de player; ignorado em team.** |
193
+ | `techniques` | List<String> | `null` | não | Códigos de técnica (GT). Backfill automático com `GT03` por rotina de manutenção (ver 3.8). |
194
+ | `timeout` | String | `null` | não | Expressão keyword de TTL do cache (ex.: `30m`, `3h`, `1d`). Sem ele: `+1h` (motor A) ou `+3h` (motor B). |
195
+ | `extra` | Map<String,Object> | `{}` | não | Mapa de extensão **livre**. Persistido, mas **nenhuma chave é consumida** pelo motor de cálculo. |
196
+
197
+ Campos do schema **não consumidos em runtime**: `extra` (apenas armazenado).
198
+ Não há campos computados persistidos no documento raiz.
199
+
200
+ ### 3.2 `Operation` — fórmula de cálculo
201
+
202
+ | Campo | Tipo | Obrigatório | Descrição |
203
+ | --- | --- | --- | --- |
204
+ | `type` | int | sim | Tipo de operação (enum abaixo). |
205
+ | `achievement_type` | int | quando `type=3` | Tipo de conquista (enum abaixo). |
206
+ | `item` | String | depende | `type=3`: `_id` do ponto/conquista; `type=1/2/4`: `_id` da action. |
207
+ | `attribute` | String | quando `type=2/4` | Nome do atributo numérico da action a somar/mediar. **É `attribute`, NÃO `kpi`.** |
208
+ | `filters` | List<`Filter`> | não | Filtros sobre `attributes.*` da action (ver 3.4). |
209
+ | `sort` | int | não | `-1` = maior primeiro (desc), `1` = menor primeiro (asc). Qualquer valor ≠ 1 vira `-1`. |
210
+ | `sub` | boolean | não | Habilita **sub-rankings** por atributo de action. Ver correção em 3.7. |
211
+ | `subAttribute` | String | quando `sub=true` | Atributo da action que define o sub-ranking (ex.: `restaurante`). |
212
+ | `prepared` | String | quando `type=10` | `_id` de um `challenge_rule_prepared` (pipeline de agregação). |
213
+
214
+ **`operation.type` (enum, `Operation.java`):**
215
+
216
+ | Valor | Constante | Significado | Coleção agregada |
217
+ | --- | --- | --- | --- |
218
+ | `1` | TYPE_COUNT_ACTIONS | Conta execuções da action | `action_log` |
219
+ | `2` | TYPE_SUM_ATTRIBUTE | Soma `attributes.<attribute>` da action | `action_log` |
220
+ | `3` | TYPE_SUM_ACHIEVEMENTS | Soma o `total` de conquistas | `achievement` |
221
+ | `4` | TYPE_AVG_ATTRIBUTE | Média `attributes.<attribute>` | `action_log` |
222
+ | `10` | TYPE_PREPARED_KPI | Executa pipeline `prepared` | `prepared.entity` |
223
+
224
+ > ⚠️ `type=4` (média) **não é uniformemente suportado** — ver seção 7.
225
+
226
+ **`operation.achievement_type` (usado direto como `type` no match de `achievement`; aceita todo o enum de `Achievement`):**
227
+
228
+ | Valor | Significado |
229
+ | --- | --- |
230
+ | `-1` | ACHIEVEMENT_TYPE_NONE — não filtra por tipo |
37
231
  | `0` | Point |
38
232
  | `1` | Challenge |
39
233
  | `2` | Catalog Item |
40
234
  | `3` | Level |
235
+ | `4` | Crown |
41
236
  | `5` | Lottery |
237
+ | `6` | Mystery Box |
238
+ | `7` | Character Star Stat |
239
+ | `8` | Bonus |
42
240
  | `9` | Competition |
241
+ | `50` | Lottery Ticket |
242
+ | `99` | Custom |
243
+
244
+ Legado: a constante `subType` está comentada no código (`Operation.java:38`) — funcionalidade não existe.
245
+
246
+ ### 3.3 `Period` — janela de tempo
43
247
 
44
- ### `operation.item` o que rankear
248
+ | Campo | Tipo | Descrição |
249
+ | --- | --- | --- |
250
+ | `type` | int | `0` = Variable (relativa), `1` = Fixed (intervalo). |
251
+ | `timeAmount` | int | Quantidade de unidades (variable). |
252
+ | `timeScale` | int | Escala (variable): `4`=Hour, `5`=Day, `6`=Week, `7`=Month, `8`=Year (`TimeScale`). |
253
+ | `startDate` | Date | Início (fixed). |
254
+ | `endDate` | Date | Fim (fixed). |
255
+ | `expression` | String | Expressão keyword (v3) ex.: `-0M-;+3d` (start;end). |
45
256
 
46
- - Quando `operation.type = 3` e `achievement_type = 0` (Point): use o `_id` do ponto (ex: `"xp"`, `"coin"`)
47
- - Quando `operation.type = 1, 2 ou 4` (action-based): use o `_id` da action
257
+ Resolução de faixa (`Period.getCurrentRange`): **`expression` tem prioridade** (se `length > 5`), depois `Variable`, depois `Fixed`.
258
+ ⚠️ **`expression` é honrada pelo Motor B e por `getCurrentRange`. O Motor A (`/leaders`) ignora `expression`** e trata apenas `Variable`/`Fixed` (`CrowningManager.java:225`).
48
259
 
49
- ### `operation.sort` — ordem do ranking
260
+ ### 3.4 `Filter`filtro de atributo de action
50
261
 
51
- | Valor | Label |
52
- |---|---|
53
- | `-1` | With more maior valor primeiro (padrão) |
54
- | `1` | With less menor valor primeiro |
262
+ | Campo | Tipo | Descrição |
263
+ | --- | --- | --- |
264
+ | `param` | String | Nome do atributo. Aplicado como `attributes.<param>`. |
265
+ | `operator` | int | Operador (tabela abaixo). |
266
+ | `value` | String | Valor (numérico se parseável, senão string). |
55
267
 
56
- ### `operation.sub` — saldo líquido
268
+ `operator` — **somente estes são tratados** por `buildJsonFilter` (`CrowningManager.java:1279`):
57
269
 
58
- | Valor | Comportamento |
59
- |---|---|
60
- | `false` | Total bruto acumulado (padrão) |
61
- | `true` | Saldo líquido — desconta débitos (útil para pontos trocáveis GT75) |
270
+ | Valor | Operador | Mongo |
271
+ | --- | --- | --- |
272
+ | `1` | Equal | igualdade direta |
273
+ | `3` | Regex (Contains) | `$regex` |
274
+ | `4` | Greater Than | `$gt` |
275
+ | `5` | Greater Than or Equal | `$gte` |
276
+ | `6` | Less Than | `$lt` |
277
+ | `7` | Less Than or Equal | `$lte` |
62
278
 
63
- ### `period.type` tipo de período
279
+ > ⚠️ Operadores existentes em `Operator` mas **não tratados** (`2`=Like, `8`=Not Equal, `9`=Exist, `10`=Not Exist, `20`=Max Distance, `40`=Mod) geram uma cláusula JSON malformada (`{attributes.<param>: }`) que **quebra o parse do pipeline**. Ver seção 7.
280
+ > Filtros **não são aplicados** quando `operation.type=3` (Sum achievements) — o branch de achievements não anexa `buildJsonFilter`.
64
281
 
65
- | Valor | Label | Comportamento |
66
- |---|---|---|
67
- | `0` | Variable | Janela relativa: últimos N períodos (ex: última semana) |
68
- | `1` | Fixed | Intervalo fixo com data de início e fim |
282
+ ### 3.5 `Leader` registro calculado (coleção `leader`, somente leitura)
69
283
 
70
- ### `period.timeScale` escala de tempo
284
+ Saída do cálculo; **não é criado manualmente**.
71
285
 
72
- | Valor | Label |
73
- |---|---|
74
- | `4` | Hour |
75
- | `5` | Day |
76
- | `6` | Week |
77
- | `7` | Month |
78
- | `8` | Year |
286
+ | Campo | Tipo | Descrição |
287
+ | --- | --- | --- |
288
+ | `_id` | String | Motor A: GUID. Motor B: composto `"<principalId>_<cache>"`. |
289
+ | `boardId` | String | `_id` do leaderboard. |
290
+ | `player` | String | Id do jogador ou da equipe. |
291
+ | `name` | String | Nome do jogador/equipe. |
292
+ | `image` | String | URL da imagem. Varia por branch: player ~ `image.medium` (Motor A) / `image.small` (Motor B); equipes e paths `Prepared KPI` usam `image.original`. |
293
+ | `position` | int | Posição atual (1-based). |
294
+ | `total` | **long** | Valor agregado. ⚠️ Tipo inteiro — ver nota. |
295
+ | `previous_position` | int | Posição na janela anterior (`0` se ausente). |
296
+ | `previous_total` | long | Total na janela anterior. |
297
+ | `move` | String | `up` / `down` / `stop`. |
298
+ | `sub` | String | Valor do sub-ranking (quando aplicável). |
299
+ | `period` | `Period` | Período do cálculo (geralmente não populado). |
300
+ | `extra.cache` | String | Id do `expire` que originou o registro (chave de cache). |
79
301
 
80
- `period.timeAmount` = quantidade de unidades (ex: `timeAmount: 1, timeScale: 6` = última semana).
302
+ > **Nota sobre `total` (long):** `$sum`/`$avg` sobre atributos decimais retornam `double`, mas `Leader.total` é `long`. O comportamento exato na desserialização (truncamento) **não foi verificado** — valide para casos com valores fracionários, especialmente `Avg attribute`.
81
303
 
82
- ### `operation.filters[].operator` — filtros de action
304
+ ### 3.6 `LeaderHeader`metadados do cálculo (coleção `leader_header`)
83
305
 
84
- | Valor | Label |
85
- |---|---|
86
- | `1` | Equals |
87
- | `3` | Contains (Regex) |
88
- | `4` | Greater Than |
89
- | `5` | Greater Than or Equal |
90
- | `6` | Less Than |
91
- | `7` | Less Than or Equal |
306
+ | Campo | Tipo | Descrição |
307
+ | --- | --- | --- |
308
+ | `_id` | String | = `expire.id`. |
309
+ | `board` | String | Id do leaderboard/crown. |
310
+ | `creation_datetime` | String | Quando foi calculado (ISO zoned). |
311
+ | `expiration_datetime` | String | Quando expira/recalcula. |
312
+ | `start_datetime` | String | Início da faixa de registros usada. |
313
+ | `end_datetime` | String | Fim da faixa. |
314
+ | `unit` | Map | Unidade do valor (ponto/action). Ausente para `type=4` e `type=10`. |
92
315
 
93
- ## Exemplos por caso de uso
316
+ ### 3.7 Correção: `sub`/`subAttribute` saldo líquido
94
317
 
95
- ### Ranking por pontos (caso mais comum)
318
+ ⚠️ Documentações anteriores descreviam `operation.sub=true` como "saldo líquido — desconta débitos (GT75)". **Isto está incorreto.** No código, `sub`+`subAttribute` habilitam **sub-rankings filtrados por um atributo da action** (ex.: ranking por restaurante). O filtro é aplicado em `getSubValueFilter` / nos helpers, combinando `subAttribute` (config) com o query param `sub` (valor). As classes `Lost`/`LostDebit`/`LostManager` **não têm nenhuma relação** com leaderboard.
319
+
320
+ ### 3.8 Técnica de jogo (`techniques`)
321
+
322
+ - Código relevante: **`GT03`** (LEADERBOARD_CODE).
323
+ - **Não é obrigatório no insert** e `addLeaderboard` não o define nem valida.
324
+ - É preenchido automaticamente por `GameTechniqueManager.autoConfigureMissingTechniqueFields()` (rotina de manutenção): boards sem `techniques` recebem `["GT03"]`. Serve para categorização/mapa de técnicas no Studio.
325
+
326
+ ---
327
+
328
+ ## 4. Endpoints
329
+
330
+ Todos exigem `Authorization: Bearer <token>` (via `AuthBean`).
331
+
332
+ ### `POST /v3/leaderboard`
333
+
334
+ | Aspecto | Detalhe |
335
+ | --- | --- |
336
+ | Finalidade | Cria ou substitui um leaderboard |
337
+ | Full replace ou patch | **Full replace** (`c.save`) — envie o objeto completo |
338
+ | Comportamento real | Gera `_id` se ausente; **purga** `leader`/`expire`/`leader_header` do board; resposta sem campos nulos; status `201` |
96
339
 
97
340
  ```json
98
341
  {
99
- "title": "Top Players",
100
- "description": "Jogadores com mais XP na semana",
342
+ "title": "General",
101
343
  "principalType": 0,
102
- "operation": {
103
- "type": 3,
104
- "achievement_type": 0,
105
- "item": "xp",
106
- "sort": -1,
107
- "sub": false,
108
- "filters": []
109
- },
110
- "period": { "type": 0, "timeAmount": 1, "timeScale": 6 },
111
- "techniques": ["GT03"]
344
+ "operation": { "type": 3, "achievement_type": 0, "item": "xp", "sort": -1, "sub": false, "filters": [] },
345
+ "period": { "type": 0, "timeAmount": 1, "timeScale": 7 },
346
+ "principals": []
112
347
  }
113
348
  ```
114
349
 
115
- ### Ranking mensal por equipe
350
+ ### `DELETE /v3/leaderboard/{id}`
351
+
352
+ Remove o board e purga as 3 coleções auxiliares. Status `204`.
353
+
354
+ ### `GET /v3/leaderboard`
355
+
356
+ Lista/filtra leaderboards (pipeline de agregação).
357
+
358
+ | Param | Tipo | Descrição |
359
+ | --- | --- | --- |
360
+ | `id` | String | Filtra por `_id` |
361
+ | `title` | String | Filtra por título (`$regex`, case-insensitive) |
362
+ | `type` | String | `"team"` → `principalType=1`; qualquer outro → `0` |
363
+ | `fields` | String | Projeção: campos separados por vírgula (`$project`) |
364
+ | `orderby` | String | Campo de ordenação (injetado direto no `$sort` — ver seção 8) |
365
+ | `reverse` | boolean | `true` → ordem descendente |
366
+ | `max_results` | int | Limite (default `100`) |
367
+
368
+ ### `GET /v3/leaderboard/{id}`
369
+
370
+ Retorna a configuração do board (sem campos nulos).
371
+
372
+ ### `GET /v3/leaderboard/{id}/leaders` — **Motor A**
373
+
374
+ | Aspecto | Detalhe |
375
+ | --- | --- |
376
+ | Finalidade | Retorna o ranking calculado (com cache, triggers `before/after_render`) |
377
+ | Comportamento real | Recalcula se cache expirou; TTL default `+1h`; ignora `period.expression` |
378
+
379
+ | Query param | Tipo | Descrição |
380
+ | --- | --- | --- |
381
+ | `player` | String | Jogador de referência (`"me"` resolve do token); obrigatório para `view=1/2` |
382
+ | `team` | String | Restringe a membros de uma equipe |
383
+ | `sub` | String | Valor do sub-ranking |
384
+ | `time_amount` + `time_scale` | int | Período variável que sobrescreve o default |
385
+ | `start_date` + `end_date` | String (RFC 3339 ou keyword) | Período fixo |
386
+ | `max_results` | int | Limite (default `100`) |
387
+ | `view` | int | `0`=Default, `1`=Middle (vizinhos), `2`=Friends |
116
388
 
117
389
  ```json
118
390
  {
119
- "title": "Top Teams - Month",
120
- "principalType": 1,
121
- "operation": {
122
- "type": 3,
123
- "achievement_type": 0,
124
- "item": "xp",
125
- "sort": -1,
126
- "sub": false,
127
- "filters": []
128
- },
129
- "period": { "type": 0, "timeAmount": 1, "timeScale": 7 },
130
- "techniques": ["GT03"]
391
+ "leaderboard": { "_id": "...", "title": "...", "operation": {...} },
392
+ "creation_datetime": "2026-05-20T10:00:00-03:00",
393
+ "expiration_datetime": "2026-05-20T11:00:00-03:00",
394
+ "start_datetime": "2026-05-13T00:00:00-03:00",
395
+ "end_datetime": "2026-05-20T23:59:59-03:00",
396
+ "operation_unit": { "type": "point", "item": "xp" },
397
+ "leaders": [
398
+ { "player": "ana@x.com", "name": "Ana", "position": 1, "total": 100, "previous_position": 2, "previous_total": 80, "move": "up" }
399
+ ]
131
400
  }
132
401
  ```
133
402
 
134
- ### Ranking por contagem de ações
403
+ ### `POST /v3/leaderboard/{id}/leader/aggregate` **Motor B**
404
+
405
+ | Aspecto | Detalhe |
406
+ | --- | --- |
407
+ | Finalidade | Retorna o ranking permitindo **estágios de agregação extras** no corpo |
408
+ | Comportamento real | `live=true` calcula em tempo real; default usa/gera cache (TTL `+3h`); paginação via header `Range` |
409
+
410
+ | Query param | Tipo | Descrição |
411
+ | --- | --- | --- |
412
+ | `live` | boolean | `true` = tempo real; default = cache |
413
+ | `strict` | boolean | `true` = serialização via Gson |
414
+ | `player` | String | Referência para view (`"me"` resolve do token) |
415
+ | `view` | int | `0`/`1`/`2` (default/middle/friends) |
416
+ | `limit` | int | Limite (default `100`) |
417
+ | `period` | String | Expressão keyword de período |
418
+ | `Range` (header) | String | Paginação, ex.: `items=0-99` |
419
+
420
+ Corpo: array JSON de estágios de agregação Mongo anexados ao final do pipeline. Ex.: `[{"$match": {"total": {"$gte": 10}}}]`.
421
+
422
+ ### `GET /v3/leaderboard/reset`
423
+
424
+ `CrowningManager.reset` → **dropa as coleções `expire`, `leader_header` e `leader` inteiras**. Afeta **todos** os leaderboards e crownings do tenant. Ver seção 5.
425
+
426
+ > Legado: existe um endpoint antigo `GET /2.0.0/leaderboard/reset` (`rest.engine.LeaderboardRest`) — apenas reset, API v2 obsoleta.
427
+
428
+ ---
429
+
430
+ ## 5. Regras de Negócio
431
+
432
+ Regras presentes no código e ausentes do schema:
433
+
434
+ - **Validação deferida.** `addLeaderboard` não valida `type`/`item`/`period`/`principalType`. Configurações inválidas são aceitas no insert e só falham (ou retornam vazio) no cálculo.
435
+ - **TTL de cache diverge por motor:** Motor A default `+1h`, Motor B default `+3h`. Com `board.timeout` definido, ambos usam a expressão (com `+` forçado). Operadores que consomem os dois endpoints veem janelas de atualização diferentes.
436
+ - **Caches separados por motor.** As chaves de `expire.extra.reference` são diferentes (`leaderboard_<id>_<sub>_<period>` no A; `leaderboard_<id>[_<period>]` no B). Recalcular por um endpoint não atualiza o cache do outro.
437
+ - **Somente jogadores registrados aparecem** (Motor A): líderes sem `Player` correspondente são descartados.
438
+ - **Equipes são excluídas de rankings de player** (`$nin` sobre todos os `teamId`).
439
+ - **`principals` é ignorado em rankings de equipe** (`linkedTeams` vazio → todas as equipes).
440
+ - **Filtros não se aplicam a `Sum achievements`** (type 3).
441
+ - **Sub-ranking** exige `operation.sub=true` **e** `operation.subAttribute` **e** o query param `sub`; o tipo do valor é convertido conforme o tipo do atributo da action (number/boolean/string).
442
+ - **Ordenação com desempate:** empates em `total` são desempatados pelo `time` mais recente (`max:1`).
443
+ - **Isolamento multi-tenant:** toda operação resolve o cliente por `apiKey` (`FrontController.getInstance(apiKey)`), apontando para o banco do tenant.
444
+ - **Consistência eventual:** o ranking exibido é o do último cálculo; mudanças em ações/conquistas só refletem após expiração do cache ou `reset`.
445
+
446
+ ---
447
+
448
+ ## 6. Comportamentos Automáticos
449
+
450
+ | Comportamento | Trigger | Impacto | Persistência |
451
+ | --- | --- | --- | --- |
452
+ | Geração de `_id` | POST sem `_id` | `Guid.shortTimeMillis()` | Sim |
453
+ | Purga de cache | POST/DELETE do board | Remove `leader`/`expire`/`leader_header` do board | Sim (remoção) |
454
+ | Backfill de técnica | `autoConfigureMissingTechniqueFields()` (manutenção) | Adiciona `GT03` a boards sem `techniques` | Sim |
455
+ | Trigger `before_render` | Cache expirado, **Motor A** | Executa triggers da coleção `leaderboard` antes do cálculo | Conforme trigger |
456
+ | Trigger `after_render` | Após recálculo, **Motor A** | Executa triggers depois do cálculo | Conforme trigger |
457
+ | Recálculo + cache | Cache expirado, qualquer motor | Materializa `leader`, grava `expire`/`leader_header` | Sim |
458
+ | Cálculo de `move` | Cada cálculo | Compara posição atual vs anterior (`up`/`down`/`stop`) | Sim (no `leader`) |
459
+ | Inserção em lote | Recálculo Motor B | Insere `leader` em lotes de 5.000 | Sim |
460
+
461
+ ### Fluxo de behaviors automáticos (recálculo)
462
+
463
+ ```mermaid
464
+ flowchart LR
465
+ A["cache expirado"] --> B{"motor?"}
466
+ B -- "A (/leaders)" --> C["before_render"]
467
+ C --> D["calcula líderes"]
468
+ D --> E["salva leader/expire/header"]
469
+ E --> F["after_render"]
470
+ B -- "B (aggregate)" --> G["calcula pipeline"]
471
+ G --> H["insere leader em lotes de 5000"]
472
+ ```
473
+
474
+ > **Código legado/aparente bug:** em `findCacheLeadersAggregate` (`LeaderBoardManager.java:230-233`) há um bloco que **sempre** reatribui `a = aggregate({$match:{extra.cache: expire.id}})` logo após o bloco condicional. É redundante (mesmo resultado), mas chama atenção numa primeira leitura.
475
+
476
+ ---
477
+
478
+ ## 7. Suportado vs NÃO Suportado
479
+
480
+ ### ✅ Suportado
481
+
482
+ - Rankings de **player** e **team** (motor A).
483
+ - Operações: Count actions (1), Sum attribute (2), Sum achievements (3), Prepared KPI (10).
484
+ - `Avg attribute` (4) **apenas para team via `/leaders`**.
485
+ - Períodos variáveis e fixos; `expression` (apenas Motor B).
486
+ - Filtros de atributo com operadores `1,3,4,5,6,7`.
487
+ - Sub-rankings por atributo de action.
488
+ - Views: default, middle (vizinhos), friends.
489
+ - Estágios de agregação customizados no corpo do `/leader/aggregate`.
490
+ - Cache com TTL configurável (`timeout`).
491
+ - Comparação com período anterior (`previous_*`, `move`).
492
+
493
+ ### ❌ NÃO Suportado / quebrado
494
+
495
+ - **Rankings de equipe via `/leader/aggregate` (Motor B):** o `$match` de equipe está **hardcoded** como `{"team":{"$in":["t1","t2"]}}` em todos os branches de team (`LeaderBoardManager.java:398, 414, 490, 506, 581, 596`). Resultado: só retorna equipes literalmente chamadas `t1`/`t2` — na prática, **vazio**. Use o Motor A (`/leaders`) para equipes.
496
+ - **`Avg attribute` (4) para player:** `calculatePlayerLeadersHelper` (Motor A) e `findLiveLeadersAggregate` (Motor B) **não têm branch para type 4**. No Motor A, a execução chega a `a.and(...)` com `a==null` (`CrowningManager.java:921`) → **NullPointerException**. (O helper singular `calculatePlayerLeaderHelper`, que suporta avg, é usado só pelo `BonusManager`, não por leaderboard.)
497
+ - **`Prepared KPI` (10) sem `prepared` válido:** `a` permanece nulo → mesmo NPE/erro.
498
+ - **`period.expression` no Motor A (`/leaders`):** ignorada — só `Variable`/`Fixed` são tratados.
499
+ - **Operadores de filtro `2,8,9,10,20,40`:** não tratados em `buildJsonFilter`; produzem JSON malformado e quebram o pipeline.
500
+ - **Filtros em `Sum achievements` (3):** silenciosamente não aplicados.
501
+ - **Validação no insert:** inexistente — configs inválidas passam por POST/GET sem erro e só falham no cálculo.
502
+ - **`board.extra`:** persistido, mas nenhuma chave é lida pelo motor.
503
+ - **`description`:** persiste, mas sem getter/setter (não exposto programaticamente além do campo).
504
+ - **Totais decimais:** `Leader.total` é `long` — soma/média de atributos fracionários pode truncar (não verificado).
505
+ - **Endpoint legado `2.0.0/leaderboard`:** somente `/reset`.
506
+
507
+ ---
508
+
509
+ ## 8. Segurança e Permissões
510
+
511
+ - **Autenticação:** Bearer token via `AuthBean` em todos os endpoints. `player="me"` resolve o jogador do token.
512
+ - **Isolamento multi-tenant:** `FrontController.getInstance(apiKey)` direciona para o banco do tenant; leaderboards, leaders e cache são por tenant.
513
+ - **`GET /reset` é destrutivo e global:** dropa `expire`, `leader_header` e `leader` inteiras — afeta todos os rankings e crownings do tenant. Não há filtro por board.
514
+ - **Superfícies de injeção (concatenação de string em queries Mongo):**
515
+
516
+ | Origem | Local | Risco |
517
+ | --- | --- | --- |
518
+ | `orderby` (query param) | `{$sort:{<orderby>:...}}` (`LeaderBoardManager.java:88`) | Campo injetado cru no `$sort` |
519
+ | `operation.attribute` | `$sum/$avg:'$attributes.<attribute>'` (vários) | Caminho de campo injetado cru |
520
+ | `operation.subAttribute` + `sub` | `attributes.<subAttribute>` (`getSubValueFilter`, helpers) | Nome e valor injetados crus |
521
+ | `Filter.param` / `Filter.value` | `buildJsonFilter` (`CrowningManager.java:1279`) | Pode injetar operadores Mongo; valor não-numérico só é "quotado", sem sanitização |
522
+ | corpo de `/leader/aggregate` | estágios anexados ao pipeline | Permite agregação arbitrária sobre `leader` (escopo do tenant) |
523
+ | `prepared` (type 10) | pipeline armazenado em `challenge_rule_prepared` | Executa agregação arbitrária definida na config |
524
+
525
+ > Esses campos vêm de configuração administrativa (Studio) ou de tokens autenticados do tenant; ainda assim, são concatenados sem sanitização — tratar como superfície de injeção dentro do escopo do tenant.
526
+
527
+ ---
528
+
529
+ ## 9. Observabilidade e Troubleshooting
530
+
531
+ ### Diagnóstico
532
+
533
+ 1. Confirme a configuração: `GET /v3/leaderboard/{id}`.
534
+ 2. Force recálculo do cache: `GET /v3/leaderboard/reset` (⚠️ global — derruba o cache de tudo).
535
+ 3. Compare os dois motores: se `/leaders` retorna dados e `/leader/aggregate` não (em board de equipe), é o bug do `t1/t2`.
536
+
537
+ ### Inspeção direta no MongoDB
538
+
539
+ ```js
540
+ // configuração
541
+ db.leaderboard.findOne({_id: "<id>"})
542
+
543
+ // registros de cache do ranking (resultado materializado)
544
+ db.leader.find({boardId: "<id>"}).sort({position: 1})
545
+
546
+ // metadados/validade do cálculo
547
+ db.expire.find({"extra.item": "<id>"})
548
+ db.leader_header.find({board: "<id>"})
549
+ ```
550
+
551
+ ### Erros comuns e causas
552
+
553
+ | Sintoma | Causa provável |
554
+ | --- | --- |
555
+ | Ranking vazio em board de **equipe** via `/leader/aggregate` | Bug do filtro hardcoded `t1/t2` — use `/leaders` |
556
+ | `500`/NPE ao consultar `/leaders` | `operation.type=4` em board de **player**, ou `type=10` sem `prepared` válido |
557
+ | Pipeline quebra / erro de parse | Filtro com operador não suportado (`2,8,9,10,...`) |
558
+ | Período "errado" só em `/leaders` | `period.expression` ignorada nesse motor — use `Variable`/`Fixed` |
559
+ | Scores zerados | `operation.item` incorreto (`_id` do ponto/action, não o nome) ou `achievement_type` divergente |
560
+ | Resultados desatualizados | Cache ainda válido (TTL `+1h`/`+3h`); aguarde ou `reset` |
561
+ | Jogador não aparece | Sem `Player` registrado, ou é uma equipe num board de player, ou fora de `principals` |
562
+
563
+ ### Comandos de verificação
564
+
565
+ ```
566
+ GET /v3/leaderboard/<id>
567
+ GET /v3/leaderboard/<id>/leaders?max_results=10
568
+ POST /v3/leaderboard/<id>/leader/aggregate?live=true (body: [])
569
+ GET /v3/leaderboard/reset
570
+ ```
571
+
572
+ ---
573
+
574
+ ## 10. Exemplos Práticos
575
+
576
+ ### Exemplo mínimo — ranking de pontos por semana
135
577
 
136
578
  ```json
137
579
  {
138
- "title": "Mais Vendas",
580
+ "title": "Top XP - Semana",
139
581
  "principalType": 0,
140
- "operation": {
141
- "type": 1,
142
- "item": "sell",
143
- "sort": -1,
144
- "filters": []
145
- },
146
- "period": { "type": 0, "timeAmount": 1, "timeScale": 5 },
147
- "techniques": ["GT03"]
582
+ "operation": { "type": 3, "achievement_type": 0, "item": "xp", "sort": -1 },
583
+ "period": { "type": 0, "timeAmount": 1, "timeScale": 6 }
148
584
  }
149
585
  ```
150
586
 
151
- ### Ranking por soma de atributo numérico
587
+ ### Exemplo avançado sub-ranking por atributo, com filtro e timeout
588
+
589
+ Ranking de receita (`Sum attribute`) por loja, somente vendas acima de 100, cache de 30 min:
152
590
 
153
591
  ```json
154
592
  {
155
- "title": "Maior Receita",
593
+ "title": "Receita por Loja",
156
594
  "principalType": 0,
595
+ "timeout": "30m",
157
596
  "operation": {
158
597
  "type": 2,
159
- "item": "sell",
160
- "kpi": "price",
598
+ "item": "venda",
599
+ "attribute": "valor",
600
+ "sub": true,
601
+ "subAttribute": "loja",
161
602
  "sort": -1,
162
- "filters": []
603
+ "filters": [
604
+ { "param": "valor", "operator": 5, "value": "100" }
605
+ ]
163
606
  },
164
- "period": { "type": 0, "timeAmount": 1, "timeScale": 6 },
165
- "techniques": ["GT03"]
607
+ "period": { "type": 0, "timeAmount": 1, "timeScale": 7 }
166
608
  }
167
609
  ```
168
610
 
169
- ## API Endpoints
611
+ Consulta de um sub-ranking específico:
170
612
 
171
- ### Listar Leaderboards
172
- **Método:** GET — `/v3/leaderboard`
173
-
174
- ### Criar / Atualizar
175
- **Método:** POST — `/v3/leaderboard`
613
+ ```
614
+ GET /v3/leaderboard/<id>/leaders?sub=Loja-Centro&max_results=20
615
+ ```
176
616
 
177
- ### Excluir
178
- **Método:** DELETE — `/v3/leaderboard/:id`
617
+ ### Exemplo de equipe — usar o motor correto
179
618
 
180
- ### Obter líderes
181
- **Método:** POST — `/v3/leaderboard/:id/leader/aggregate?period=&live=true`
619
+ ```json
620
+ {
621
+ "title": "Top Equipes - Mês",
622
+ "principalType": 1,
623
+ "operation": { "type": 3, "achievement_type": 0, "item": "xp", "sort": -1 },
624
+ "period": { "type": 0, "timeAmount": 1, "timeScale": 7 }
625
+ }
626
+ ```
182
627
 
183
- ### Resetar cache
184
- **Método:** GET — `/v3/leaderboard/reset`
628
+ Consulta (✅ use `/leaders`; ❌ não use `/leader/aggregate` para equipes):
185
629
 
186
- ## Troubleshooting
630
+ ```
631
+ GET /v3/leaderboard/<id>/leaders
632
+ ```
187
633
 
188
- - **Ranking vazio**: verifique se o `operation.item` é o `_id` do ponto/action não o `category` nem o nome amigável
189
- - **Scores errados**: confirme `operation.type` e `achievement_type` — usar `type: 3` com `achievement_type: 0` é o caminho para pontos
190
- - **Período incorreto**: `timeScale: 6` = semana, `timeScale: 7` = mês — fácil de confundir
191
- - **Cache desatualizado**: use `GET /v3/leaderboard/reset` para forçar recálculo
634
+ ### Anti-pattern — o que NÃO fazer
192
635
 
193
- ## Checklist
636
+ ```json
637
+ {
638
+ "title": "Média de Vendas (player)",
639
+ "principalType": 0,
640
+ "operation": { "type": 4, "item": "venda", "attribute": "valor", "sort": -1 },
641
+ "period": { "type": 0, "timeAmount": 1, "timeScale": 6 }
642
+ }
643
+ ```
194
644
 
195
- - [ ] `techniques: ["GT03"]` presente
196
- - [ ] `principalType` definido (0=jogador, 1=equipe)
197
- - [ ] `operation.type` correto para o caso de uso
198
- - [ ] `operation.item` é o `_id` do ponto ou action (não o nome amigável)
199
- - [ ] `period.type` e `period.timeScale` configurados
200
- - [ ] Confirmado com `funifier_list type=leaderboard search=<título>`
645
+ `type=4` (Avg) em board de **player** causa **NullPointerException** ao consultar `/leaders` (não há branch para média no helper de jogadores). Alternativas: usar `Sum attribute` (2), ou um `Prepared KPI` (10) que calcule a média, ou aplicar a média via estágio extra no `/leader/aggregate`.
646
+
647
+ Outros anti-patterns:
648
+ - Usar `"kpi"` em vez de `"attribute"` para somar atributo o campo correto é **`attribute`** (`kpi` é ignorado).
649
+ - Esperar que `/leader/aggregate` traga equipes — está quebrado (`t1/t2`).
650
+ - Confiar em `operation.sub` para "saldo líquido" — `sub` é sub-ranking por atributo, não desconto de débitos.
651
+ - Usar operador de filtro `2`/`8` — quebra o pipeline.
652
+
653
+ ---
654
+
655
+ ## Checklist de Configuração
656
+
657
+ - [ ] `principalType` definido (`0` player, `1` team).
658
+ - [ ] `operation.type` válido para o motor que você vai consultar (evite `type=4` em player).
659
+ - [ ] `operation.item` = `_id` do ponto/action (não o nome amigável).
660
+ - [ ] `operation.type=3` → `achievement_type` correto (ex.: `0` para pontos).
661
+ - [ ] `operation.type=2/4` → use **`attribute`** (não `kpi`).
662
+ - [ ] `operation.type=10` → `prepared` aponta para um `challenge_rule_prepared` existente.
663
+ - [ ] `period.type` + (`timeAmount`/`timeScale`) ou (`startDate`/`endDate`) preenchidos. `expression` só funciona no `/leader/aggregate`.
664
+ - [ ] Filtros usam apenas operadores `1,3,4,5,6,7`.
665
+ - [ ] Sub-ranking: `sub=true` **e** `subAttribute` definidos; passe o valor em `?sub=`.
666
+ - [ ] Para equipes, consulte por **`/leaders`** (não `/leader/aggregate`).
667
+ - [ ] Ciente de que POST faz **full replace** e **purga o cache** do board.
668
+ - [ ] Ciente de que `GET /reset` é **global e destrutivo**.
669
+ - [ ] Armadilha: omitir `timeout` deixa TTL em `+1h` (`/leaders`) ou `+3h` (`/leader/aggregate`).