funifier-mcp 0.2.26 → 0.2.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/.cursor/rules/funifier.mdc +38 -41
  2. package/.github/copilot-instructions.md +38 -41
  3. package/AGENTS.md +56 -49
  4. package/README.md +40 -22
  5. package/datasource-funifier-docs/.coverage.json +326 -0
  6. package/datasource-funifier-docs/.validation.json +593 -0
  7. package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
  8. package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
  9. package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
  10. package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
  11. package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
  12. package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
  13. package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
  14. package/datasource-funifier-docs/knowledge/index.md +4 -1
  15. package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
  16. package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
  17. package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
  18. package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
  19. package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
  20. package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
  21. package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
  22. package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
  23. package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
  24. package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
  25. package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
  26. package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
  27. package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
  28. package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
  29. package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
  30. package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
  31. package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
  32. package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
  33. package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
  34. package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
  35. package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
  36. package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
  37. package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
  38. package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
  39. package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
  40. package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
  41. package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
  42. package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
  43. package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
  44. package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
  45. package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
  46. package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
  47. package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
  48. package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
  49. package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
  50. package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
  51. package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
  52. package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
  53. package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
  54. package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
  55. package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
  56. package/dist/cli/init.d.ts.map +1 -1
  57. package/dist/cli/init.js +42 -1
  58. package/dist/cli/init.js.map +1 -1
  59. package/dist/cli/init.test.js +74 -3
  60. package/dist/cli/init.test.js.map +1 -1
  61. package/dist/cli/persona.d.ts +3 -0
  62. package/dist/cli/persona.d.ts.map +1 -0
  63. package/dist/cli/persona.js +25 -0
  64. package/dist/cli/persona.js.map +1 -0
  65. package/dist/mcp/bundle.js +119 -93
  66. package/dist/mcp/check-update.d.ts +5 -0
  67. package/dist/mcp/check-update.d.ts.map +1 -1
  68. package/dist/mcp/check-update.js +21 -10
  69. package/dist/mcp/check-update.js.map +1 -1
  70. package/dist/mcp/check-update.test.d.ts +2 -0
  71. package/dist/mcp/check-update.test.d.ts.map +1 -0
  72. package/dist/mcp/check-update.test.js +33 -0
  73. package/dist/mcp/check-update.test.js.map +1 -0
  74. package/dist/mcp/index.js +2 -2
  75. package/dist/mcp/index.js.map +1 -1
  76. package/dist/mcp/prompts/templates.d.ts.map +1 -1
  77. package/dist/mcp/prompts/templates.js +35 -0
  78. package/dist/mcp/prompts/templates.js.map +1 -1
  79. package/dist/mcp/resources/documentation.d.ts +1 -1
  80. package/dist/mcp/resources/documentation.d.ts.map +1 -1
  81. package/dist/mcp/resources/documentation.js +39 -3
  82. package/dist/mcp/resources/documentation.js.map +1 -1
  83. package/dist/mcp/tools/connect.d.ts.map +1 -1
  84. package/dist/mcp/tools/connect.js +18 -8
  85. package/dist/mcp/tools/connect.js.map +1 -1
  86. package/dist/mcp/tools/database.d.ts.map +1 -1
  87. package/dist/mcp/tools/database.js +59 -47
  88. package/dist/mcp/tools/database.js.map +1 -1
  89. package/dist/mcp/tools/database.test.js +2 -2
  90. package/dist/mcp/tools/database.test.js.map +1 -1
  91. package/dist/mcp/tools/delete.d.ts.map +1 -1
  92. package/dist/mcp/tools/delete.js +13 -3
  93. package/dist/mcp/tools/delete.js.map +1 -1
  94. package/dist/mcp/tools/execute.d.ts.map +1 -1
  95. package/dist/mcp/tools/execute.js +20 -9
  96. package/dist/mcp/tools/execute.js.map +1 -1
  97. package/dist/mcp/tools/folder.d.ts.map +1 -1
  98. package/dist/mcp/tools/folder.js +22 -12
  99. package/dist/mcp/tools/folder.js.map +1 -1
  100. package/dist/mcp/tools/get.d.ts.map +1 -1
  101. package/dist/mcp/tools/get.js +16 -6
  102. package/dist/mcp/tools/get.js.map +1 -1
  103. package/dist/mcp/tools/index.d.ts +1 -1
  104. package/dist/mcp/tools/index.d.ts.map +1 -1
  105. package/dist/mcp/tools/index.js +28 -1
  106. package/dist/mcp/tools/index.js.map +1 -1
  107. package/dist/mcp/tools/list.d.ts.map +1 -1
  108. package/dist/mcp/tools/list.js +38 -14
  109. package/dist/mcp/tools/list.js.map +1 -1
  110. package/dist/mcp/tools/logs.d.ts.map +1 -1
  111. package/dist/mcp/tools/logs.js +15 -5
  112. package/dist/mcp/tools/logs.js.map +1 -1
  113. package/dist/mcp/tools/save.d.ts.map +1 -1
  114. package/dist/mcp/tools/save.js +14 -4
  115. package/dist/mcp/tools/save.js.map +1 -1
  116. package/dist/mcp/tools/save.test.js +3 -3
  117. package/dist/mcp/tools/save.test.js.map +1 -1
  118. package/dist/mcp/tools/search-docs.d.ts +3 -0
  119. package/dist/mcp/tools/search-docs.d.ts.map +1 -0
  120. package/dist/mcp/tools/search-docs.js +102 -0
  121. package/dist/mcp/tools/search-docs.js.map +1 -0
  122. package/package.json +6 -2
  123. package/skills/acquire-funifier-knowledge/SKILL.md +155 -0
  124. package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
  125. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
  126. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
  127. package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
  128. package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
  129. package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
  130. package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +86 -0
  131. package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
  132. package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
  133. package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
  134. package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
  135. package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
  136. package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
  137. package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
  138. package/skills/funifier/SKILL.md +88 -0
  139. package/skills/funifier/references/configure-security.md +96 -0
  140. package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
  141. package/skills/funifier/references/create-aggregate.md +144 -0
  142. package/skills/funifier/references/create-challenge.md +116 -0
  143. package/skills/funifier/references/create-competition.md +98 -0
  144. package/skills/funifier/references/create-crossword.md +574 -0
  145. package/skills/funifier/references/create-custom-object.md +91 -0
  146. package/skills/funifier/references/create-custom-page.md +135 -0
  147. package/skills/funifier/references/create-folder.md +104 -0
  148. package/skills/funifier/references/create-lastmile.md +643 -0
  149. package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
  150. package/skills/funifier/references/create-level.md +94 -0
  151. package/skills/funifier/references/create-lottery.md +913 -0
  152. package/skills/funifier/references/create-mystery.md +769 -0
  153. package/skills/funifier/references/create-notification.md +75 -0
  154. package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
  155. package/skills/funifier/references/create-quiz.md +98 -0
  156. package/skills/funifier/references/create-scheduler.md +141 -0
  157. package/skills/funifier/references/create-story.md +636 -0
  158. package/skills/funifier/references/create-swap.md +95 -0
  159. package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
  160. package/skills/funifier/references/create-virtual-good.md +96 -0
  161. package/skills/funifier/references/create-webhook.md +72 -0
  162. package/skills/funifier/references/create-websocket.md +71 -0
  163. package/skills/funifier/references/create-widget.md +76 -0
  164. package/skills/funifier/references/debug.md +87 -0
  165. package/skills/funifier/references/help.md +81 -0
  166. package/skills/funifier/references/implement-frontend.md +106 -0
  167. package/skills/funifier/references/import-csv.md +75 -0
  168. package/skills/funifier/references/manage-player.md +82 -0
  169. package/skills/funifier/references/manage-team.md +76 -0
  170. package/skills/funifier/references/upload-file.md +91 -0
  171. package/skills/funifier-create-aggregate/SKILL.md +0 -127
  172. package/skills/funifier-create-challenge/SKILL.md +0 -88
  173. package/skills/funifier-create-custom-page/SKILL.md +0 -127
  174. package/skills/funifier-create-level/SKILL.md +0 -87
  175. package/skills/funifier-create-quiz/SKILL.md +0 -87
  176. package/skills/funifier-create-scheduler/SKILL.md +0 -127
  177. package/skills/funifier-create-virtual-good/SKILL.md +0 -87
  178. package/skills/funifier-debug/SKILL.md +0 -92
  179. package/skills/funifier-help/SKILL.md +0 -86
  180. package/skills/funifier-implement-frontend/SKILL.md +0 -90
  181. package/skills/funifier-index/SKILL.md +0 -58
@@ -1,149 +1,851 @@
1
- # Competition (Competição)
1
+ # `competition`
2
2
 
3
3
  **Acesso Studio:** `/studio/competition`
4
4
  **API Endpoint:** `/v3/competition`
5
+ **Coleção MongoDB:** `competition` (configuração) + `competition_join` (inscrições)
5
6
 
6
- ## O que é
7
+ ---
7
8
 
8
- Configuração de disputas diretas entre jogadores ou equipes. Permite criar competições com regras próprias, prêmios para diferentes posições e critérios de apuração customizáveis. Geralmente, o jogador ou equipe precisa se inscrever para participar.
9
+ ## 1. Visão Geral
9
10
 
10
- ## Quando usar
11
+ O módulo `competition` configura **disputas temporais entre jogadores ou times**. Uma competição define um período (`period`), um critério de classificação (`operation`, idêntico ao de um leaderboard), um número máximo de vencedores (`maxWinners`) e participantes (`maxPlayers`), um custo de inscrição opcional (`requires`) e prêmios por posição (`rewards`). Jogadores precisam se inscrever (`join`) para serem considerados na apuração. Ao final, a competição é **executada** — os vencedores são apurados, premiados e marcados com um achievement do tipo `9 (TYPE_COMPETITION)`.
11
12
 
12
- - Para criar disputas temporais (ex: competição de vendas mensal)
13
- - Para premiar os melhores colocados em um período
14
- - Para engajar equipes com competições internas
13
+ Papel arquitetural:
15
14
 
16
- ## Dependências
15
+ - O ranking é calculado **sob demanda** por `findLeadersAggregate` — uma aggregation MongoDB construída em runtime sobre `action_log` ou `achievement`, restrita ao `period` e aos jogadores inscritos. Não há materialização de placar; cada leitura recalcula.
16
+ - A apuração final (`execute`) é **idempotente por flag**: só roda se `executed == null`. Pode ser disparada manualmente (`GET /v3/competition/{id}/execute`) ou automaticamente pelo `AsyncProcessor` (thread que roda a cada 5 minutos por gamificação).
17
17
 
18
- - **Point** ou **Action**: métrica de avaliação deve existir
19
- - **Player** ou **Team**: participantes devem existir
18
+ **Diferença arquitetural crítica competição NÃO usa `fireAction`.** Tanto as deduções de inscrição quanto as recompensas dos vencedores são gravadas como documentos `achievement` **diretamente via Jongo (`ac.save(...)`)**, sem passar por `AchievementManager.addAchievement`/`fireAction`. Consequências (ver seções 5 e 7):
20
19
 
21
- ## Checklist de Configuração no Studio
20
+ - **Não recalcula `player_status`** saldos materializados ficam defasados após inscrição/premiação até o próximo evento natural do jogador.
21
+ - **Não avalia level-up** — pontos de prêmio não promovem o jogador de nível imediatamente.
22
+ - **Não dispara webhook `achievement_created`**.
23
+ - **Não dispara triggers `before_win`/`after_win` da `point_category`** — apenas os triggers próprios da competição (entidade `"competition"`).
22
24
 
23
- - [ ] Definir título e descrição
24
- - [ ] Definir período de apuração (period.expression)
25
- - [ ] Definir número máximo de vencedores (maxWinners)
26
- - [ ] Definir número máximo de participantes (maxPlayers)
27
- - [ ] Definir critério de ranking (operation: tipo, item, ordenação)
28
- - [ ] Definir custo de inscrição (requires) se aplicável
29
- - [ ] Definir recompensas por posição (rewards com position_start/position_ends)
30
- - [ ] Definir se é competição de equipes (teamCompetition)
31
- - [ ] Ativar competição (active: true)
32
- - [ ] Definir execução automática (autoExecute)
25
+ Relação com outros módulos:
33
26
 
34
- ## API Endpoints
27
+ - `achievement` — competição grava (e remove, na exclusão/rollback) documentos nesta coleção diretamente. `TYPE_COMPETITION = 9`.
28
+ - `action_log` — fonte de dados para `operation.type` ∈ {COUNT_ACTIONS, SUM_ATTRIBUTE, AVG_ATTRIBUTE}.
29
+ - `crowning` — reusa `Operation`, `Period`, `Filter` e `CrowningManager.buildJsonFilter`/`findPlayerIds`.
30
+ - `challenge` — reusa `Requirement` (custo e prêmio) e `ChallengeRulePrepared` (operation `PREPARED_KPI`).
31
+ - `team` / `team_player` — em competições de time, resolve membros via `findUserIdsByTeamIds` e reagrupa o placar por time.
32
+ - `trigger` — eventos `before_create`/`after_create` (competition e competition_join) e `before_win`/`after_win`.
33
+ - `notification` — apenas o evento `EVENT_WIN` é notificado na execução.
34
+ - `principal` — valida o tipo (player vs team) e a lista de autorizados (`principals`).
35
35
 
36
- ### Listar Competições
37
- **Método:** GET
38
- **Endpoint:** `/v3/competition`
36
+ Os únicos chamadores de `CompetitionManager` no código são `CompetitionRest` (API) e `AsyncProcessor` (scheduler) — o módulo é autocontido, nenhum outro manager o invoca.
39
37
 
40
- ### Criar Competição
41
- **Método:** POST
42
- **Endpoint:** `/v3/competition`
38
+ ---
39
+
40
+ ## 2. Arquitetura e Fluxos
41
+
42
+ ### 2.1 Classes envolvidas
43
+
44
+ | Classe | Arquivo | Papel |
45
+ |---|---|---|
46
+ | `Competition` | `engine/competition/Competition.java` | Entidade raiz (coleção `competition`) |
47
+ | `CompetitionJoin` | `engine/competition/CompetitionJoin.java` | Inscrição (coleção `competition_join`) |
48
+ | `TieBreaker` | `engine/competition/TieBreaker.java` | Critério de desempate (`field`, `sort`) |
49
+ | `AutoJoin` | `engine/competition/AutoJoin.java` | Inscrição automática por aggregation |
50
+ | `CompetitionManager` | `engine/competition/CompetitionManager.java` | Toda a lógica: CRUD, join, apuração, ranking |
51
+ | `CompetitionRest` | `rest/v3/rest/CompetitionRest.java` | Controller REST v3 |
52
+ | `Operation` | `engine/crowning/Operation.java` | Critério de classificação (reusado do crowning) |
53
+ | `Period` | `engine/crowning/Period.java` | Período (reusado do crowning) |
54
+ | `Filter` | `engine/crowning/Filter.java` | Filtro de atributos da action |
55
+ | `Requirement` | `engine/challenge/Requirement.java` | Custo (`requires`) e prêmio (`rewards`) |
56
+ | `Leader` | `engine/leader/Leader.java` | Linha do ranking projetada pela aggregation |
57
+ | `AsyncProcessor` | `controller/AsyncProcessor.java` | Thread que auto-executa competições vencidas |
58
+
59
+ Não há `Repository`/`Dao` — todo acesso ao MongoDB é via Jongo dentro do `CompetitionManager`.
60
+
61
+ ### 2.2 Pipeline — `insert(Competition)` (criação e atualização)
62
+
63
+ `POST /v3/competition` cobre **criação e atualização** (upsert por `_id`). Não existe `PUT`.
64
+
65
+ ```
66
+ [1] Se id null/vazio → competition.id = Guid.newShortGuid()
67
+ [2] current = competition.findOne({_id: id})
68
+ - se current == null → created = now; create = true
69
+ - senão → created = current.created (preserva)
70
+ [3] updated = new Date() (sempre)
71
+ [4] CÁLCULO DE PERÍODO (ver 2.2.1) — pode recalcular startDate/endDate
72
+ [5] se create → trigger before_create (entidade "competition")
73
+ [6] c.save(competition) (upsert)
74
+ [7] se create → trigger after_create
75
+ ```
76
+
77
+ **Triggers só disparam na criação.** Atualizações (quando `current != null`) **não** disparam `before_update`/`after_update` — esses eventos não são chamados pelo módulo.
78
+
79
+ #### 2.2.1 Cálculo de período — bug de comparação por referência
80
+
81
+ O bloco de período usa **comparação de referência Java (`!=`) entre Strings**, não `.equals()`:
82
+
83
+ ```java
84
+ if(competition.period.expression != null && competition.period.expression.trim().length() > 0
85
+ || (current != null && current.period != null && current.period.expression != competition.period.expression)) {
86
+
87
+ if(current != null && current.period != null && current.period.expression != competition.period.expression) {
88
+ competition.period.startDate = null; // limpa
89
+ competition.period.endDate = null;
90
+ }
91
+ String[] dts = competition.period.expression.split(";");
92
+ if(competition.period.startDate == null && dts.length > 0) {
93
+ Date dt0 = DateUtil.fromKeyword(dts[0], competition.updated); // keyword relativo a AGORA
94
+ if(dt0 == null) dt0 = DateUtil.parseZonedDateToLocalDate(dts[0]); // ou data absoluta
95
+ competition.period.startDate = dt0;
96
+ }
97
+ if(competition.period.startDate != null && competition.period.endDate == null && dts.length > 1) {
98
+ Date dt1 = DateUtil.fromKeyword(dts[1], competition.period.startDate);
99
+ if(dt1 == null) dt1 = DateUtil.parseZonedDateToLocalDate(dts[1]);
100
+ competition.period.endDate = dt1;
101
+ }
102
+ }
103
+ ```
104
+
105
+ Como `competition` é desserializado a cada request, `competition.period.expression` é sempre um objeto String distinto de `current.period.expression` lido do Mongo. Portanto `!=` é **quase sempre verdadeiro** quando o documento já existe. Efeito prático:
106
+
107
+ - **Toda atualização de uma competição existente recalcula `startDate`/`endDate` a partir da `expression`.**
108
+ - Se a expressão usa **keyword relativa** (ex.: `-0M-;+1M+`), o `startDate` é recalculado relativo ao **momento da edição** (`updated`), deslocando a janela a cada save.
109
+ - Se a expressão usa **datas absolutas** (ex.: `2018-01-30T00:00:00-03:00;+1M+`), `startDate` fica estável (parse absoluto), mas `endDate` (keyword) é recalculado relativo ao novo `startDate`.
110
+
111
+ > Campos legados comentados na entidade: `start` (Date), `end` (Date) e `period` (String) estão comentados — não existem em runtime. O período atual é o objeto `Period`.
112
+
113
+ ### 2.3 Pipeline — `insertJoin(CompetitionJoin)`
114
+
115
+ ```
116
+ [1] ci = competition.findOne({_id: join.competition})
117
+ - se ci == null → adiciona "competition_does_not_exist" (porém ver ARMADILHA abaixo)
118
+ [2] Se join.player == "FUNIFIER_RANDOM_PLAYER":
119
+ sorteia (até 5 tentativas) um player ainda não inscrito que passe em checkRestrictions
120
+ [3] Verifica existência:
121
+ - !teamCompetition && player não existe → "player_does_not_exist"
122
+ - teamCompetition && team não existe → "team_does_not_exist"
123
+ [4] restrictions += checkRestrictions(ci, join.player) (ver 2.3.1)
124
+ [5] SE restrictions vazio:
125
+ [5.1] Debita requires (custo de inscrição) — ver 2.3.2
126
+ [5.2] join.id = Guid.newShortGuid(); join.created = now
127
+ [5.3] trigger before_create (entidade "competition_join", player)
128
+ [5.4] cj.save(join)
129
+ [5.5] trigger after_create
130
+ [6] Retorna { join, status: "OK"|"UNAUTHORIZED", restrictions?: [...] }
131
+ ```
132
+
133
+ **ARMADILHA (NPE):** quando a competição **não existe**, `ci == null`, mas o código continua até `if(!ci.teamCompetition && ...)` (linha 223) e lança `NullPointerException` ao acessar `ci.teamCompetition`. A restrição `"competition_does_not_exist"` é **inalcançável** — o cliente recebe erro 500, não a restrição.
134
+
135
+ #### 2.3.1 `checkRestrictions` — árvore de restrições do join
136
+
137
+ Avaliada em ordem; cada falha adiciona um código (todos são acumulados):
138
+
139
+ | Verificação | Método | Código de restrição | Observação |
140
+ |---|---|---|---|
141
+ | Competição ativa | `competition.active` | `competition_disabled` | |
142
+ | Atende aos `requires` | `evaluateRequirements` | `insufficient_requirements` | usa `Requirement.period` se presente |
143
+ | Dentro do prazo | `checkIsInInterval` | `competition_out_of_time` | **só compara `endDate`** — ver abaixo |
144
+ | Vagas disponíveis | `checkIsInMaxPlayers` | `max_players_exceeded` | só checa se `maxPlayers > -1`; conta `competition_join` |
145
+ | Ainda não inscrito | `checkNotJoinedYet` | `player_already_joined` | `count({competition, player}) >= 1` |
146
+ | Tipo de principal correto | `checkIsCorrectPrincipalType` | `team_is_not_allowed` / `player_is_not_allowed` | lookup em `principal` |
147
+ | Está nos `principals` | `checkIsPrincipalAllowed` | `principal_not_allowed` | inclui membros de times listados |
148
+
149
+ `checkIsInInterval` **só valida `endDate`** (`now <= endDate`); se `endDate == null` retorna `true`. Portanto **é possível se inscrever ANTES de `startDate` começar** — o início do período não bloqueia inscrições.
150
+
151
+ `checkIsCorrectPrincipalType` consulta a coleção `principal` por `userId`/`teamId`. Se o principal não existir lá, retorna `false` → restrição. Player/team precisam estar registrados em `principal`.
152
+
153
+ #### 2.3.2 Débito do custo de inscrição (`requires`)
154
+
155
+ Para cada `Requirement` em `requires` com `operation == OPERATION_DEDUCT (1)`, `total != 0` e `type` ∈ {`POINT(0)`, `CATALOG_ITEM(2)`, `CHALLENGE(1)`}:
156
+
157
+ ```js
158
+ // achievement de dedução (total negativo) — gravado DIRETO via ac.save()
159
+ db.achievement.save({
160
+ _id: <shortGuid>, player: <join.player>, total: -|r.total|,
161
+ type: <r.type>, item: <r.item>, time: <now>,
162
+ extra: { origin_item: <join.competition>, origin_type: 9 }
163
+ })
164
+ ```
165
+
166
+ Tipos fora de {POINT, CATALOG_ITEM, CHALLENGE} são **silenciosamente ignorados** — não há débito. Como é `ac.save()` direto, **`player_status` não é recalculado** após o débito.
167
+
168
+ ### 2.4 Pipeline — `execute(id)` (apuração final)
169
+
170
+ ```
171
+ [1] competition = find(id)
172
+ [2] SÓ executa se: competition != null && active && executed == null
173
+ [3] now = new Date()
174
+ [4] leaders = findLeadersAggregate(id).and("{$limit: #}", maxWinners) ← ver ARMADILHA maxWinners
175
+ [5] Para cada leader (na ordem do ranking):
176
+ SE minScore <= 0 OU leader.total >= minScore:
177
+ [5.1] rewards = getRewardByWinnerPosition(leader.position) ← ver 3.x position
178
+ [5.2] credita cada reward (POINT/CATALOG_ITEM/CHALLENGE, total positivado):
179
+ ac.save({player: leader.player, total: |r.total|, type, item, time: now,
180
+ extra:{origin_item: competition.id, origin_type: 9}})
181
+ [5.3] win = achievement(total:1, type:9 COMPETITION, item: competition.id,
182
+ extra:{position: leader.position, total: leader.total})
183
+ [5.4] trigger before_win (entidade "competition", TriggerContext: item, leader, rewards, leaders)
184
+ [5.5] ac.save(win); winners.add(win)
185
+ [5.6] trigger after_win
186
+ [5.7] notifica EVENT_WIN (scope != CUSTOM) — mustache em params
187
+ [6] re-find competition; competition.executed = now; save
188
+ ```
189
+
190
+ **ARMADILHA (`maxWinners`):** o `execute` repassa `competition.maxWinners` direto para `{$limit: maxWinners}` **sem nenhuma guarda**. O default da entidade é `-1` (documentado como "ilimitado"). Como o `$limit` do MongoDB exige um inteiro **> 0**, uma competição com `maxWinners` não definido (`-1`) ou `0` **não pode ser executada** — a aggregation falha. **Sempre defina `maxWinners >= 1`.**
191
+
192
+ Quando a falha ocorre na auto-execução, a exceção sobe até o `try/catch` único do `AsyncProcessor.run()`, abortando **o restante daquele ciclo de 5 minutos** (demais competições da iteração, processamento de `lost` e limpeza de auditoria). O ciclo seguinte tentará de novo — e falhará novamente enquanto `maxWinners <= 0`.
193
+
194
+ Ordem das gravações: **prêmios são gravados ANTES** do achievement de vitória. Tudo via `ac.save()` direto → sem recálculo de `player_status`, sem level-up, sem webhook.
195
+
196
+ ### Fluxo de apuração — `execute`
197
+
198
+ ```mermaid
199
+ flowchart TD
200
+ S[execute id] --> A{competition != null<br/>&& active<br/>&& executed == null?}
201
+ A -- não --> Z[retorna lista vazia<br/>nada é gravado]
202
+ A -- sim --> L[findLeadersAggregate + $limit maxWinners]
203
+ L --> LOOP{para cada leader}
204
+ LOOP --> MS{minScore <= 0<br/>OU total >= minScore?}
205
+ MS -- não --> LOOP
206
+ MS -- sim --> RW[grava rewards da posição<br/>ac.save total positivo]
207
+ RW --> WIN[grava win achievement<br/>type=9, total=1]
208
+ WIN --> TB[trigger before_win]
209
+ TB --> SV[ac.save win]
210
+ SV --> TA[trigger after_win]
211
+ TA --> NT[notificações EVENT_WIN]
212
+ NT --> LOOP
213
+ LOOP -- fim --> EX[executed = now; save]
214
+ ```
215
+
216
+ ### 2.5 Pipeline — `undoExecute(id)` (rollback)
217
+
218
+ ```
219
+ SE competition.executed != null:
220
+ [1] db.achievement.remove({type:9, item: id}) // achievements de vitória
221
+ [2] db.achievement.remove({"extra.origin_item": id}) // prêmios (e deduções de join!)
222
+ [3] competition.executed = null; competition.autoExecute = false; save
223
+ [4] db.notification.remove({"item.type":"competition", "item.id": id})
224
+ ```
225
+
226
+ Observações:
227
+
228
+ - `[3]` força `autoExecute = false` — após um rollback, a competição **não será re-executada automaticamente** pelo scheduler. É preciso reativar `autoExecute` manualmente.
229
+ - `[2]` remove por `extra.origin_item` **sem filtrar `origin_type`** — isso também apaga as **deduções de custo de inscrição** (que carregam o mesmo `origin_item`). O rollback, portanto, devolve implicitamente o custo de quem se inscreveu, além de remover os prêmios.
230
+ - Inconsistência com `delete()`: `delete` remove prêmios por `{origin_type:9, origin_item:id}` (mais estrito); `undoExecute` remove por `{origin_item:id}` apenas.
231
+
232
+ ### 2.6 Pipeline — `delete(id)` (exclusão em cascata)
233
+
234
+ ```
235
+ [1] db.achievement.remove({type:9, item: id})
236
+ [2] db.achievement.remove({"extra.origin_type":9, "extra.origin_item": id})
237
+ [3] db.competition_join.remove({competition: id})
238
+ [4] db.notification.remove({"item.type":"competition", "item.id": id})
239
+ [5] db.competition.remove({_id: id})
240
+ ```
241
+
242
+ `delete` **não dispara** triggers `before_delete`/`after_delete` — esses eventos não são chamados.
243
+
244
+ ### 2.7 Motor de ranking — `findLeadersAggregate(id, jongo)`
245
+
246
+ Constrói a aggregation que produz o placar. Etapas:
247
+
248
+ ```
249
+ [1] competition = find(id)
250
+ [2] players = db.competition_join.distinct("player", {competition: id})
251
+ [3] autoJoinMemberIds = findAutoJoinIds(competition)
252
+ SE != null → players = autoJoinMemberIds ← SUBSTITUI a lista (não soma!)
253
+ [4] filter = CrowningManager.buildJsonFilter(operation) (ver 2.8)
254
+ [5] Ramifica por teamCompetition e por operation.type (ver tabela)
255
+ [6] Ordenação + paginação por facet/unwind para anexar a posição (1-based)
256
+ ```
257
+
258
+ **`autoJoin` substitui os inscritos.** Se `autoJoin` estiver configurado e retornar ids, as inscrições manuais (`competition_join`) são **ignoradas** no ranking — o conjunto de participantes passa a ser exclusivamente o resultado da aggregation do `autoJoin`.
259
+
260
+ Estágio de agrupamento por `operation.type` (ramo **player**; o ramo **team** é análogo, mas filtra por membros e reagrupa por time):
261
+
262
+ | `operation.type` | Coleção | `$match` (resumo) | `$group` |
263
+ |---|---|---|---|
264
+ | `1` `COUNT_ACTIONS` | `action_log` | `time ∈ [start,end]`, `userId ∈ players`, `actionId = item` + filtros | `total: {$sum: 1}`, `max: {$max:"$time"}` |
265
+ | `2` `SUM_ATTRIBUTE` | `action_log` | idem | `total: {$sum: "$attributes.<attribute>"}` |
266
+ | `4` `AVG_ATTRIBUTE` | `action_log` | idem | `total: {$avg: "$attributes.<attribute>"}` |
267
+ | `3` `SUM_ACHIEVEMENTS` | `achievement` | `time ∈ [start,end]`, `type = achievement_type` (se != -1), `player ∈ players`, `item = item` | `total: {$sum: "$total"}` |
268
+ | `10` `PREPARED_KPI` | `prepared.entity` | aggregation do `ChallengeRulePrepared` com tokens substituídos | definido pelo prepared |
269
+
270
+ **Filtros (`operation.filters`) só se aplicam aos tipos baseados em `action_log`** (COUNT, SUM_ATTRIBUTE, AVG_ATTRIBUTE). Em `SUM_ACHIEVEMENTS` e `PREPARED_KPI` os filtros **não são aplicados** (o `filter` não é concatenado nesses ramos).
271
+
272
+ Ordenação final:
273
+
274
+ ```js
275
+ // sem tieBreaker:
276
+ { $sort: { total: <dir>, max: 1 } } // dir = 1 se operation.sort==1 (asc), senão -1 (desc)
277
+ // com tieBreaker (field != "" e sort != 0):
278
+ { $sort: { total: <dir>, <tieBreaker.field>: <tieBreaker.sort> } }
279
+ ```
280
+
281
+ - `operation.sort == 1` (`SORT_ASC`) → ranqueia do **menor** total para o maior (ex.: tempo/erros, "menor é melhor").
282
+ - Qualquer outro valor (`0` default, ou `-1`) → **maior** total primeiro (placar padrão).
283
+ - **Desempate padrão (sem `tieBreaker`):** `max: 1` (ascendente) — entre totais iguais, vence quem tem o **menor `max`** (o timestamp do evento qualificador mais recente é o mais antigo), ou seja, quem atingiu o total **primeiro**. Isso **contradiz o comentário** da classe `TieBreaker` ("o padrão é max:-1").
284
+
285
+ PREPARED_KPI substitui os tokens: `FUNIFIER_PLAYER_IDS`, `FUNIFIER_TRIGGER_TIME`, `FUNIFIER_PERIOD_START_TIME`, `FUNIFIER_PERIOD_END_TIME`. **Não substitui** `FUNIFIER_TRIGGER_ACTION_ID` nem `FUNIFIER_RULE_ACTION_ID` (usados por challenge) — um prepared escrito para challenge não funciona aqui sem ajuste.
286
+
287
+ ### 2.8 `buildJsonFilter(operation)` — tradução de `operation.filters`
288
+
289
+ ```java
290
+ // produz: , $and: [ {attributes.<param>: <expr>}, ... ]
291
+ ```
292
+
293
+ | `operator` (código) | Expressão gerada |
294
+ |---|---|
295
+ | `EQUAL (1)` | `{attributes.X: <value>}` |
296
+ | `REGEX (3)` | `{attributes.X: { $regex: <value>}}` |
297
+ | `GREATER_THAN (4)` | `{attributes.X: { $gt: <value>}}` |
298
+ | `GREATER_THAN_OR_EQUAL (5)` | `{attributes.X: { $gte: <value>}}` |
299
+ | `LESS_THAN (6)` | `{attributes.X: { $lt: <value>}}` |
300
+ | `LESS_THAN_OR_EQUAL (7)` | `{attributes.X: { $lte: <value>}}` |
301
+
302
+ O `value` é numérico se fizer parse como `Double`; caso contrário vira string entre aspas.
303
+
304
+ **Operadores não suportados geram JSON malformado.** `NONE(0)`, `LIKE(2)`, `NOT_EQUAL(8)`, `EXIST(9)`, `NOT_EXIST(10)`, `MAX_DISTANCE_IN_METERS(20)`, `MOD(40)` não têm ramo no `buildJsonFilter` usado pela competição: o código emite `{attributes.X : }` (sem valor) → a aggregation falha ao ser parseada. Use apenas os 6 operadores da tabela em filtros de competição.
305
+
306
+ ### 2.9 Auto-execução — `AsyncProcessor` + `findCompetitionsToExecute`
307
+
308
+ ```mermaid
309
+ sequenceDiagram
310
+ participant T as AsyncProcessor (thread)
311
+ participant CM as CompetitionManager
312
+ participant DB as MongoDB
313
+ participant TR as TriggerManager
314
+ participant NM as NotificationManager
315
+ loop a cada 5 minutos
316
+ T->>CM: findCompetitionsToExecute()
317
+ CM->>DB: match active:true, autoExecute:true,<br/>executed:null, period.endDate <= now
318
+ DB-->>CM: competições vencidas
319
+ loop cada competição
320
+ T->>CM: execute(competition.id)
321
+ CM->>DB: findLeadersAggregate + $limit(maxWinners)
322
+ CM->>DB: ac.save(rewards + win)
323
+ CM->>TR: before_win / after_win
324
+ CM->>NM: send(EVENT_WIN)
325
+ CM->>DB: set executed = now
326
+ end
327
+ end
328
+ ```
329
+
330
+ Query do scheduler:
331
+
332
+ ```js
333
+ db.competition.aggregate([
334
+ { $match: { active: true, autoExecute: true, executed: null, "period.endDate": { $lte: <now> } } }
335
+ ])
336
+ ```
337
+
338
+ ---
339
+
340
+ ## 3. Estrutura dos Objetos
341
+
342
+ ### 3.1 `Competition` — documento raiz (coleção `competition`)
343
+
344
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
345
+ |---|---|---|---|---|
346
+ | `_id` | String | `Guid.newShortGuid()` se ausente | Não (auto) | Id curto interno (não é ObjectId) |
347
+ | `title` | String | — | Não | Título exibido |
348
+ | `description` | String | — | Não | Descrição |
349
+ | `image` | String | — | Não | URL da imagem |
350
+ | `period` | `Period` | `new Period()` | **Sim (na prática)** | Janela da competição; ver 3.2 |
351
+ | `maxWinners` | long | `-1` | **Sim p/ executar** | Nº máximo de vencedores. Vira `$limit`; **deve ser >= 1** para `execute` funcionar |
352
+ | `maxPlayers` | long | `-1` | Não | Nº máximo de inscritos. `-1` = ilimitado (não checado) |
353
+ | `minScore` | long | `0` | Não | Score mínimo para ser premiado. `<= 0` desativa a checagem |
354
+ | `operation` | `Operation` | — | **Sim** | Critério de ranking; ver 3.3 |
355
+ | `requires` | List<`Requirement`> | `[]` | Não | Custo de inscrição (deduzido no join) |
356
+ | `rewards` | List<`Requirement`> | `[]` | Não | Prêmios por posição (creditados na execução) |
357
+ | `notifications` | List<`NotificationDefinition`> | `[]` | Não | **Apenas `EVENT_WIN (0)` é disparado** |
358
+ | `active` | boolean | `false` | **Sim** | Só competições ativas recebem join e podem ser executadas |
359
+ | `teamCompetition` | boolean | `false` | Não | Se `true`, participantes são times; ranking reagrupa por time |
360
+ | `autoExecute` | boolean | `false` | Não | Auto-execução pelo `AsyncProcessor` ao vencer `period.endDate` |
361
+ | `extra` | Map | `{}` | Não | Atributos livres |
362
+ | `techniques` | List<String> | `null` | Não | Códigos de game techniques (GT…); apenas metadado |
363
+ | `created` | Date | `now` na criação | Auto | Data de criação (preservada em updates) |
364
+ | `updated` | Date | `now` em todo save | Auto | Última atualização |
365
+ | `executed` | Date | `null` | Auto | Data da apuração; bloqueia re-execução enquanto != null |
366
+ | `tieBreaker` | `TieBreaker` | `null` | Não | Desempate; ver 3.4 |
367
+ | `principals` | List<String> | `null` | Não | Ids de players/times autorizados a participar |
368
+ | `autoJoin` | `AutoJoin` | `null` | Não | Inscrição automática por aggregation; ver 3.5 |
369
+ | `now` | Date | — | Runtime | **Não persistido de forma útil** — preenchido só na listagem (ver abaixo) |
370
+
371
+ #### Campos computados / assimétricos
372
+
373
+ - **`now`**: a entidade tem o campo, e o endpoint **de listagem** (`GET /v3/competition`) seta `c.now = new Date()` em cada item da resposta (para o frontend comparar com `period.startDate/endDate`). O endpoint de **busca individual** (`GET /v3/competition/{id}`) **não** seta `now`. Não há uso de `now` persistido.
374
+
375
+ #### Campos aceitos mas ignorados (silenciosos)
376
+
377
+ - **`techniques`**: aceito e persistido, porém puramente informativo — nenhuma lógica de competição o consulta.
378
+ - **Campos legados comentados** na entidade: `start` (Date), `end` (Date), `period` (String) — não existem em runtime.
379
+ - `@JsonIgnoreProperties(ignoreUnknown=true)`: qualquer campo extra fora do mapeado é **descartado silenciosamente** no POST.
380
+
381
+ ### 3.2 `Period` — subdocumento `period`
382
+
383
+ | Campo | Tipo | Usado pela competição? | Descrição |
384
+ |---|---|---|---|
385
+ | `expression` | String | **Sim** | `"<start>;<end>"`. Start relativo à criação; ex.: `-0M-;+1M+` ou `2018-01-30T00:00:00-03:00;+1M+` |
386
+ | `startDate` | Date | **Sim** | Calculado da expression (ou informado). Início da janela |
387
+ | `endDate` | Date | **Sim** | Calculado da expression (ou informado). Fim da janela e gatilho do auto-execute |
388
+ | `type` | int | **Não** (ignorado) | `0 VARIABLE`, `1 FIXED` — usado pelo crowning, não pela competição |
389
+ | `timeAmount` | int | **Não** (ignorado) | Idem |
390
+ | `timeScale` | int | **Não** (ignorado) | Idem |
391
+
392
+ A competição calcula `startDate`/`endDate` em `insert` (ver 2.2.1) — **não** usa `Period.getCurrentRange()`. Os campos `type`/`timeAmount`/`timeScale` são aceitos mas nunca lidos aqui.
393
+
394
+ ### 3.3 `Operation` — subdocumento `operation` (critério de ranking)
395
+
396
+ | Campo | Tipo | Descrição |
397
+ |---|---|---|
398
+ | `type` | int | `1` COUNT_ACTIONS, `2` SUM_ATTRIBUTE, `3` SUM_ACHIEVEMENTS, `4` AVG_ATTRIBUTE, `10` PREPARED_KPI |
399
+ | `achievement_type` | int | Só para `SUM_ACHIEVEMENTS`: `-1` NONE (qualquer), `0` POINT, `1` CHALLENGE, `2` CATALOG_ITEM, `3` LEVEL |
400
+ | `item` | String | COUNT/SUM/AVG: `actionId`. SUM_ACHIEVEMENTS: `item` do achievement (ex.: id da point_category) |
401
+ | `attribute` | String | Só SUM/AVG_ATTRIBUTE: caminho em `attributes.<attribute>` |
402
+ | `filters` | List<`Filter`> | Filtros de `attributes` (só action_log; ver 2.8) |
403
+ | `sort` | int | `1` asc (menor primeiro); demais valores → desc (maior primeiro) |
404
+ | `prepared` | String | Só PREPARED_KPI: id de `ChallengeRulePrepared` |
405
+ | `sub` | boolean | **Ignorado pela competição** (recurso de sub-rankings do crowning) |
406
+ | `subAttribute` | String | **Ignorado pela competição** |
407
+
408
+ ### 3.4 `TieBreaker` — subdocumento `tieBreaker`
409
+
410
+ | Campo | Tipo | Descrição |
411
+ |---|---|---|
412
+ | `field` | String | Campo numérico (somado no `$group`) usado como desempate |
413
+ | `sort` | int | Direção do desempate (`1` asc / `-1` desc). Só ativa se `field != ""` **e** `sort != 0` |
414
+
415
+ Sem `tieBreaker`, o desempate padrão é `max: 1` (quem atingiu o total primeiro). O comentário da classe diz "padrão max:-1" — **desatualizado** em relação ao código.
416
+
417
+ ### 3.5 `AutoJoin` — subdocumento `autoJoin`
418
+
419
+ | Campo | Tipo | Descrição |
420
+ |---|---|---|
421
+ | `entity` | String | Coleção onde a aggregation roda (ex.: `"player"`) |
422
+ | `aggregate` | String | Pipeline (JSON array) que retorna documentos com campo `_id` = id do participante |
423
+
424
+ Se `principals` estiver preenchido, o resultado é filtrado por `{_id: {$in: <findPlayerIds(principals)>}}`. **Quando `autoJoin` retorna ids, eles substituem totalmente os inscritos manuais** no ranking (ver 2.7).
425
+
426
+ ### 3.6 `Requirement` — usado em `requires` e `rewards`
427
+
428
+ | Campo | Tipo | Usado? | Descrição |
429
+ |---|---|---|---|
430
+ | `total` | int | Sim | Quantidade. Em reward é positivado; em require (DEDUCT) é negativado |
431
+ | `type` | int | Sim | Só `POINT(0)`, `CHALLENGE(1)`, `CATALOG_ITEM(2)` têm efeito; demais ignorados |
432
+ | `item` | String | Sim | Id do item (point_category, challenge, catalog_item) |
433
+ | `operation` | int | Sim (em `requires`) | `0` VERIFY, `1` DEDUCT. Só `DEDUCT` debita no join |
434
+ | `extra` | Map | Sim (em `rewards`) | **Apenas `position_start`/`position_ends`** controlam a posição; ver 3.7 |
435
+ | `period` | String | Sim (em `requires`) | Janela para `evaluateRequirements` (ex.: `-30d-`) |
436
+ | `folder` | String | **Não** (ignorado pela competição) | |
437
+ | `restrict` | boolean | **Não** (ignorado) | Sem checagem de estoque de item (diferente de challenge/catalog) |
438
+ | `perPlayer` | boolean | **Não** (ignorado) | |
439
+
440
+ Tipos de `Requirement`: `POINT 0`, `CHALLENGE 1`, `CATALOG_ITEM 2`, `LEVEL 3`, `CROWN 4`, `LOTTERY 5`, `MYSTERY_BOX 6`, `CHARACTER_STAR_STAT 7`, `LOTTERY_TICKET 50`. **Competição só processa 0/1/2.** Não há suporte a `FUNIFIER_RANDOM` em prêmios (diferente de challenge).
441
+
442
+ ### 3.7 Distribuição de prêmio por posição — `getRewardByWinnerPosition(position)`
443
+
444
+ A regra real para decidir se um `reward` se aplica a um vencedor:
445
+
446
+ ```
447
+ para cada reward:
448
+ se reward.extra é null OU vazio:
449
+ aplica a TODOS os vencedores (prêmio geral)
450
+ senão (extra tem alguma chave):
451
+ start = parseInt(extra.position_start) // -1 se ausente/inválido
452
+ ends = parseInt(extra.position_ends) // -1 se ausente/inválido
453
+ se position_start == null E position_ends == null:
454
+ aplica a TODOS ← inclui o caso extra.position!
455
+ senão se position_start != null E start <= position E position_ends == null:
456
+ aplica de start em diante
457
+ senão se position_start != null E start <= position E position_ends != null E position <= ends:
458
+ aplica no intervalo [start, ends]
459
+ ```
460
+
461
+ **`extra.position` é código morto.** Apenas `position_start` e `position_ends` são lidos. Um reward com `extra: {position: 1}` cai no ramo "`position_start == null && position_ends == null`" → é distribuído a **todos** os vencedores, não só ao 1º colocado. O exemplo do apidoc do controller e o `CompetitionTest` usam `position` — e portanto premiam todo mundo igualmente (bug demonstrado no próprio teste).
462
+
463
+ ### 3.8 `CompetitionJoin` — inscrição (coleção `competition_join`)
464
+
465
+ | Campo | Tipo | Padrão | Descrição |
466
+ |---|---|---|---|
467
+ | `_id` | String | `Guid.newShortGuid()` | Id da inscrição |
468
+ | `competition` | String | — | Id da competição |
469
+ | `player` | String | — | Id do player (ou time, se `teamCompetition`) |
470
+ | `created` | Date | `now` | Data da inscrição |
471
+
472
+ ---
473
+
474
+ ## 4. Endpoints
475
+
476
+ Todos os endpoints exigem `Authorization: Bearer <token>` e respondem JSON. As respostas usam `JsonUtil.toJsonRemoveNullFields` — **campos nulos são omitidos** (ex.: `executed`, `tieBreaker`, `principals`, `autoJoin`, `techniques` aparecem só quando preenchidos).
477
+
478
+ ### 4.1 `POST /v3/competition`
479
+
480
+ | Aspecto | Detalhe |
481
+ |---|---|
482
+ | Finalidade | Criar **ou atualizar** competição (upsert por `_id`) |
483
+ | Full replace ou patch | **Full replace** — o body inteiro substitui o documento |
484
+ | Triggers | `before_create`/`after_create` **só na criação** (não em update) |
485
+ | Resposta | `201 Created` com a competição (campos nulos removidos) |
486
+
487
+ Comportamento real: gera `_id` se ausente; preserva `created`; recalcula `period` (ver 2.2.1, atenção ao bug de referência); dispara triggers só na criação.
488
+
489
+ ### 4.2 `GET /v3/competition`
490
+
491
+ | Aspecto | Detalhe |
492
+ |---|---|
493
+ | Finalidade | Listar competições |
494
+
495
+ **Query params:**
496
+
497
+ | Param | Tipo | Descrição |
498
+ |---|---|---|
499
+ | `id` | String | Filtra por `_id` exato |
500
+ | `to` | String | Competições disponíveis para o player. `me` resolve via token. Aplica `$or` sobre `principals` (null/ausente/[]/contém player ou seus times) |
501
+ | `q` | String | Critério MongoDB raw injetado no `$match` (ver seção 8) |
502
+ | `fields` | CSV | Projeção `{$project}` |
503
+ | `orderby` / `reverse` | String | Ordenação |
504
+ | `max_results` | int | Limite (default 100; `<= 0` vira 100) |
505
+
506
+ Cada item retornado recebe `now = <hora do servidor>`.
507
+
508
+ ### 4.3 `GET /v3/competition/{id}`
509
+
510
+ Busca uma competição. **Não** seta `now`. Retorna `200`.
511
+
512
+ ### 4.4 `DELETE /v3/competition/{id}`
513
+
514
+ Exclusão em cascata (ver 2.6): remove achievements de vitória, prêmios, joins, notificações e a competição. Retorna `204`. Sem triggers de delete.
515
+
516
+ ### 4.5 `POST /v3/competition/join`
517
+
518
+ | Aspecto | Detalhe |
519
+ |---|---|
520
+ | Finalidade | Inscrever player/time na competição |
521
+ | Body | `{ "competition": "<id>", "player": "<id|FUNIFIER_RANDOM_PLAYER>" }` |
522
+ | Resposta | `201` com `{ join, status, restrictions? }` |
523
+
524
+ **Comportamento real:** mesmo quando há restrições, o status HTTP é `201` — o resultado de negócio está no corpo (`status: "OK"` ou `"UNAUTHORIZED"` + lista `restrictions`). Custo (`requires` DEDUCT) é debitado só quando não há restrições. **Inscrever em competição inexistente lança 500 (NPE)** — ver 2.3.
525
+
526
+ **Exemplo de resposta UNAUTHORIZED:**
43
527
 
44
- **Exemplo de Body:**
45
528
  ```json
46
529
  {
47
- "title": "Sales Race",
48
- "description": "Leads the competition who has the highest amount of closed sales in the month.",
49
- "period": {
50
- "expression": "-0M-;+1M+"
51
- },
52
- "maxWinners": 1,
53
- "maxPlayers": 100,
54
- "minScore": 0,
55
- "operation": {
56
- "type": 1,
57
- "achievement_type": 0,
58
- "item": "sell",
59
- "filters": [],
60
- "sort": -1,
61
- "sub": false
62
- },
63
- "requires": [
64
- {
65
- "total": 10,
66
- "type": 0,
67
- "item": "coin",
68
- "operation": 1,
69
- "extra": {},
70
- "restrict": false,
71
- "perPlayer": false
72
- }
73
- ],
530
+ "join": { "competition": "race", "player": "tom" },
531
+ "restrictions": ["player_already_joined"],
532
+ "status": "UNAUTHORIZED"
533
+ }
534
+ ```
535
+
536
+ ### 4.6 `DELETE /v3/competition/join`
537
+
538
+ Remove a inscrição. Body: `{ "competition": "<id>", "player": "<id>" }`. Query: `db.competition_join.remove({competition, player})`. **Não estorna** o custo de inscrição debitado. Retorna `204`.
539
+
540
+ ### 4.7 `GET /v3/competition/join`
541
+
542
+ Lista inscrições. Params: `id`, `competition`, `player` (`me` resolve via token), `q`, `fields`, `published_min`/`published_max` (RFC 3339 ou keyword, sobre `created`), `orderby`/`reverse`, `max_results`.
543
+
544
+ ### 4.8 `GET /v3/competition/leader?id=<id>`
545
+
546
+ Placar em tempo real (paginado via header `Range: items=<from>-<size>`). Roda `findLeadersAggregate` (ver 2.7). Resposta no formato `paginatedCallback`.
547
+
548
+ ### 4.9 `POST /v3/competition/leader/aggregate?id=<id>`
549
+
550
+ Placar com **estágios de aggregation adicionais** (body = lista de estágios). Os estágios do body são anexados **após** o pipeline de `findLeadersAggregate`. Paginação via `Range` (default 0–100).
551
+
552
+ ### 4.10 `GET /v3/competition/{id}/execute`
553
+
554
+ Apura e premia (ver 2.4). Retorna `{ competition, winners }`. Idempotente: se `executed != null`, `winners` volta vazio e nada é gravado.
555
+
556
+ ### 4.11 `DELETE /v3/competition/{id}/execute`
557
+
558
+ Rollback da execução (ver 2.5). Remove achievements (vitória + prêmios + deduções com mesmo `origin_item`) e notificações, zera `executed` e força `autoExecute=false`. Retorna `{ competition, message: "Execution Rollback Done" }`.
559
+
560
+ ---
561
+
562
+ ## 5. Regras de Negócio
563
+
564
+ Regras presentes no código, não no schema:
565
+
566
+ ### 5.1 Competição não passa por `fireAction`
567
+
568
+ Prêmios e deduções são `ac.save()` diretos. **Não** recalculam `player_status`, **não** avaliam level-up, **não** disparam webhook nem os triggers de `point_category`. O saldo materializado do jogador fica defasado até o próximo evento natural.
569
+
570
+ ### 5.2 `extra.position` não funciona — use `position_start`/`position_ends`
571
+
572
+ Ver 3.7. Reward com `extra.position` é tratado como prêmio geral (todos os vencedores).
573
+
574
+ ### 5.3 `maxWinners` deve ser `>= 1`
575
+
576
+ Vira `$limit` sem guarda. Default `-1` impede a execução. Ver 2.4.
577
+
578
+ ### 5.4 Inscrição permitida antes do início (`startDate` não barra join)
579
+
580
+ `checkIsInInterval` só compara `endDate`. Ver 2.3.1.
581
+
582
+ ### 5.5 Apenas `EVENT_WIN` é notificado; `SCOPE_CUSTOM` é ignorado
583
+
584
+ O comentário da entidade fala em eventos "create, join, end, win", mas `execute` só dispara `EVENT_WIN (0)`. E `getNotificationsByEvent` exclui `SCOPE_CUSTOM (99)` — uma notificação de vitória com scope custom **não** é enviada.
585
+
586
+ ### 5.6 Tipos de `Requirement` limitados a POINT/CHALLENGE/CATALOG_ITEM
587
+
588
+ Em `requires` (DEDUCT) e `rewards`, outros tipos são ignorados sem erro.
589
+
590
+ ### 5.7 `autoExecute` é desligado após rollback
591
+
592
+ `undoExecute` força `autoExecute=false`. Reativação é manual.
593
+
594
+ ### 5.8 Multi-tenant por conexão
595
+
596
+ Não há `apiKey` no documento. O isolamento vem do roteamento de conexão Jongo por tenant (`manager.getJongoConnection()`).
597
+
598
+ ### 5.9 Edição recalcula o período (bug de `!=`)
599
+
600
+ Ver 2.2.1. Editar uma competição com expressão por keyword desloca a janela.
601
+
602
+ ### 5.10 `autoJoin` substitui inscrições manuais no ranking
603
+
604
+ Ver 2.7. Não há merge entre `competition_join` e `autoJoin`.
605
+
606
+ ---
607
+
608
+ ## 6. Comportamentos Automáticos
609
+
610
+ | Comportamento | Trigger | Impacto | Persistência |
611
+ |---|---|---|---|
612
+ | `_id` (ShortGuid) | `insert`/`insertJoin` se id ausente | Id curto interno | Sim |
613
+ | Cálculo de `period.startDate/endDate` | `insert` (ver 2.2.1) | Recalculado a cada update (bug `!=`) | Sim |
614
+ | `created`/`updated` | `insert` | created preservado; updated sempre = now | Sim |
615
+ | Débito de `requires` | `insertJoin` sem restrições | achievement(s) negativo(s) com `origin_item`/`origin_type` | Sim (sem recalc de status) |
616
+ | Crédito de `rewards` | `execute`, por vencedor/posição | achievement(s) positivo(s) | Sim (sem recalc de status) |
617
+ | Achievement de vitória | `execute` | `type=9`, `total=1`, `extra.position`, `extra.total` | Sim |
618
+ | Trigger `before_create`/`after_create` | criação de competition e de competition_join | scripts customizados | Side effects |
619
+ | Trigger `before_win`/`after_win` | `execute`, por vencedor (entidade `"competition"`) | scripts com `TriggerContext` (item, leader, rewards, leaders) | Side effects |
620
+ | Notificação `EVENT_WIN` | `execute`, por vencedor (scope != CUSTOM) | documento em `notification` (mustache em `params`) | Sim |
621
+ | Auto-execução | `AsyncProcessor` a cada 5 min, se vencida e `autoExecute` | apura e marca `executed` | Sim |
622
+ | `autoExecute=false` | `undoExecute` | bloqueia re-execução automática | Sim |
623
+
624
+ ### Cascata de gravações em `execute` (por vencedor)
625
+
626
+ ```mermaid
627
+ flowchart LR
628
+ W[vencedor x] --> R[rewards da posição<br/>ac.save total positivo]
629
+ R --> WIN[win achievement type=9]
630
+ WIN --> BW[trigger before_win]
631
+ BW --> SV[ac.save win]
632
+ SV --> AW[trigger after_win]
633
+ AW --> N[notification EVENT_WIN]
634
+ ```
635
+
636
+ ---
637
+
638
+ ## 7. Suportado vs NÃO Suportado
639
+
640
+ ### ✅ Suportado
641
+
642
+ - CRUD via `/v3/competition` (POST upsert, GET list, GET by id, DELETE com cascata).
643
+ - Inscrição/desinscrição (`/join`), com custo (`requires` DEDUCT) e validação de restrições.
644
+ - Inscrição de player aleatório (`player: "FUNIFIER_RANDOM_PLAYER"`, até 5 tentativas).
645
+ - Inscrição automática por aggregation (`autoJoin`).
646
+ - Placar em tempo real (`/leader`, `/leader/aggregate`) com paginação por `Range`.
647
+ - 5 critérios de ranking: COUNT_ACTIONS, SUM_ATTRIBUTE, AVG_ATTRIBUTE, SUM_ACHIEVEMENTS, PREPARED_KPI.
648
+ - Competições de time (`teamCompetition`) com reagrupamento por time via `team_player`.
649
+ - Filtros de atributo (6 operadores) em rankings baseados em `action_log`.
650
+ - Desempate por `tieBreaker` (campo + direção) ou padrão (quem chegou primeiro).
651
+ - Prêmios por faixa de posição (`position_start`/`position_ends`).
652
+ - `minScore` para premiar só quem atingiu um mínimo.
653
+ - Execução manual e automática (`autoExecute` + `AsyncProcessor`), com rollback (`undoExecute`).
654
+ - Triggers de create (competition/join) e win; notificações de vitória.
655
+ - Lista restrita por `principals` e endpoint `to=me`.
656
+
657
+ ### ❌ NÃO Suportado / Pegadinhas
658
+
659
+ - **`extra.position` em rewards** — código morto; só `position_start`/`position_ends` funcionam (3.7).
660
+ - **`maxWinners` ilimitado (`-1`)** — quebra `execute` (`$limit` exige > 0). Default não é executável (2.4).
661
+ - **Recálculo de `player_status`/level-up/webhook** após prêmio ou dedução — não acontece (5.1).
662
+ - **Inscrição em competição inexistente** — lança NPE (500), não devolve `competition_does_not_exist` (2.3).
663
+ - **`startDate` não barra inscrições** — só `endDate` (2.3.1, 5.4).
664
+ - **Notificações de `create`/`join`/`end`** — não existem; só `EVENT_WIN` é disparado (5.5).
665
+ - **Notificação de vitória com `SCOPE_CUSTOM (99)`** — filtrada, nunca enviada (5.5).
666
+ - **Triggers `before_update`/`after_update`/`before_delete`/`after_delete`** — não chamados pela competição.
667
+ - **`Trigger.ENTITY_COMPETITION`** — constante **comentada** no código; triggers devem usar a string crua `"competition"` (ou `"competition_join"`).
668
+ - **Tipos de Requirement** fora de POINT/CHALLENGE/CATALOG_ITEM — ignorados (5.6).
669
+ - **`Requirement.restrict`/`perPlayer`/`folder` e `FUNIFIER_RANDOM` em prêmios** — não suportados (diferente de challenge/catalog).
670
+ - **`operation.sub`/`subAttribute`** — aceitos, mas ignorados (sub-rankings não existem em competição).
671
+ - **`Period.type`/`timeAmount`/`timeScale`** — aceitos, mas ignorados (só `expression`/`startDate`/`endDate`).
672
+ - **Operadores de filtro além de EQUAL/REGEX/GT/GTE/LT/LTE** — geram JSON malformado e quebram o ranking (2.8).
673
+ - **Estorno de custo ao desinscrever** (`DELETE /join`) — não devolve o que foi debitado.
674
+ - **`PUT`** — não existe; atualização é via `POST` (não dispara triggers de update).
675
+
676
+ ---
677
+
678
+ ## 8. Segurança e Permissões
679
+
680
+ - **Autenticação:** todos os endpoints v3 exigem `Authorization: Bearer <token>`. Não há endpoint legado `/2.0.0/competition`.
681
+ - **Isolamento multi-tenant:** por conexão Jongo (`getJongoConnection()`); não há `apiKey` no documento. Documentos cross-tenant são impossíveis enquanto o roteamento estiver correto.
682
+ - **Autorização de participação:** `principals` (lista de players/times) + `checkIsCorrectPrincipalType` (lookup em `principal`). Sem `principals`, qualquer principal do tipo correto pode entrar.
683
+ - **Triggers para autorização de competição:** como `Trigger.ENTITY_COMPETITION` está comentado, use a string `"competition"`/`"competition_join"` no campo `entity` do trigger.
684
+
685
+ ### Superfícies de injeção
686
+
687
+ - **`q` (raw query)** em `GET /v3/competition` e `GET /v3/competition/join`: concatenado **literalmente** no `$match` (`{ _id:{$exists:true}, <q> }`). Permite injeção de operadores MongoDB. Restringir quem pode chamar com `q` arbitrário.
688
+ - **`operation.attribute`, `tieBreaker.field`, `orderby`, `fields`**: concatenados diretamente na string da aggregation (ex.: `'$attributes.' + operation.getAttribute()`, `"{ $sort : {" + orderby + " : #}}"`). Valores controlados por quem cria a competição/consulta entram sem sanitização no pipeline.
689
+ - **`POST /v3/competition/leader/aggregate`**: aceita estágios de aggregation arbitrários do cliente, anexados ao pipeline — permite consultas livres sobre as coleções do tenant.
690
+ - **`autoJoin.aggregate`**: pipeline arbitrário executado sobre `autoJoin.entity` (qualquer coleção do tenant) na apuração/ranking.
691
+
692
+ ---
693
+
694
+ ## 9. Observabilidade e Troubleshooting
695
+
696
+ ### Diagnóstico
697
+
698
+ ```
699
+ # A competição está ativa e dentro do prazo?
700
+ GET /v3/competition/{id} # confira active=true, period.startDate/endDate, executed
701
+
702
+ # Quem se inscreveu?
703
+ GET /v3/competition/join?competition={id}&max_results=1000
704
+
705
+ # Placar atual (recalculado sob demanda)
706
+ GET /v3/competition/leader?id={id}
707
+ # Header: Range: items=0-100
708
+ ```
709
+
710
+ ### Queries úteis (MongoDB)
711
+
712
+ ```js
713
+ // Inscritos
714
+ db.competition_join.find({ competition: "<id>" })
715
+
716
+ // Achievements gerados pela competição (vitórias)
717
+ db.achievement.find({ type: 9, item: "<id>" })
718
+
719
+ // Prêmios + deduções (rastreados por origin_item)
720
+ db.achievement.find({ "extra.origin_item": "<id>" })
721
+
722
+ // Competições que o scheduler deveria executar agora
723
+ db.competition.find({ active: true, autoExecute: true, executed: null,
724
+ "period.endDate": { $lte: new Date() } })
725
+ ```
726
+
727
+ ### Erros comuns e causas
728
+
729
+ | Sintoma | Causa provável |
730
+ |---|---|
731
+ | `execute` não premia ninguém | `maxWinners <= 0` (quebra `$limit`); ou `executed != null`; ou `active=false`; ou ninguém atingiu `minScore` |
732
+ | Auto-execução nunca roda | `autoExecute=false` (inclusive após um `undoExecute`); `period.endDate` no futuro; thread `AsyncProcessor` abortou o ciclo por exceção de outra competição |
733
+ | Prêmio dado a todos em vez do 1º lugar | usou `extra.position` em vez de `extra.position_start`/`position_ends` (3.7) |
734
+ | Saldo do jogador "errado" após premiação | `player_status` não foi recalculado (5.1) — disparado só no próximo evento natural |
735
+ | 500 ao inscrever | competição não existe → NPE em `ci.teamCompetition` (2.3) |
736
+ | Placar vazio ou erro de aggregation | operador de filtro não suportado (2.8); ou janela `period` recalculada após edição (2.2.1); ou `autoJoin` retornando vazio e substituindo inscritos |
737
+ | Janela de datas mudou sozinha | edição recalculou `period` por causa do bug de comparação `!=` (2.2.1) |
738
+ | Inscrição aceita antes da data de início | esperado — `startDate` não barra join (5.4) |
739
+
740
+ ---
741
+
742
+ ## 10. Exemplos Práticos
743
+
744
+ ### 10.1 Mínimo funcional — competição individual por contagem de ações
745
+
746
+ ```json
747
+ POST /v3/competition
748
+ {
749
+ "_id": "race",
750
+ "title": "Corrida de Vendas",
751
+ "period": { "expression": "-0M-;+1M+" },
752
+ "maxWinners": 3,
753
+ "operation": { "type": 1, "item": "sell", "sort": -1 },
74
754
  "rewards": [
75
- {
76
- "total": 100,
77
- "type": 0,
78
- "item": "xp",
79
- "operation": 0,
80
- "extra": {
81
- "position_start": 1,
82
- "position_ends": 3
83
- },
84
- "restrict": false,
85
- "perPlayer": false
86
- }
755
+ { "total": 100, "type": 0, "item": "xp", "extra": { "position_start": 1, "position_ends": 3 } }
87
756
  ],
88
- "notifications": [],
89
757
  "active": true,
90
- "teamCompetition": false,
91
- "autoExecute": true,
92
- "extra": {},
93
- "techniques": ["GT26"],
94
- "_id": "race"
758
+ "autoExecute": true
95
759
  }
96
760
  ```
97
761
 
98
- ### Deletar Competição
99
- **Método:** DELETE
100
- **Endpoint:** `/v3/competition/:id`
101
-
102
- ### Listar Participações (Joins)
103
- **Método:** GET
104
- **Endpoint:** `/v3/competition/join?competition=:id`
762
+ `operation.type=1` (COUNT_ACTIONS) conta `action_log` com `actionId="sell"` no período; `sort=-1` ranqueia do maior para o menor; o prêmio vai para os colocados 1 a 3.
105
763
 
106
- ### Criar Participação (Join)
107
- **Método:** POST
108
- **Endpoint:** `/v3/competition/join`
764
+ ### 10.2 Avançado — competição de time, custo de inscrição, desempate e SUM_ACHIEVEMENTS
109
765
 
110
- **Exemplo de Body:**
111
766
  ```json
767
+ POST /v3/competition
112
768
  {
113
- "competition": "race",
114
- "player": "jerry"
769
+ "_id": "team_cup",
770
+ "title": "Copa de Times",
771
+ "description": "Maior soma de pontos no mês",
772
+ "image": "https://cdn/holiday.png",
773
+ "period": { "expression": "2026-05-01T00:00:00-03:00;2026-05-31T23:59:59-03:00" },
774
+ "maxWinners": 1,
775
+ "maxPlayers": 8,
776
+ "minScore": 50,
777
+ "teamCompetition": true,
778
+ "operation": { "type": 3, "achievement_type": 0, "item": "point", "sort": -1 },
779
+ "tieBreaker": { "field": "total", "sort": -1 },
780
+ "requires": [
781
+ { "total": 10, "type": 0, "item": "coin", "operation": 1 }
782
+ ],
783
+ "rewards": [
784
+ { "total": 500, "type": 0, "item": "xp", "extra": { "position_start": 1, "position_ends": 1 } },
785
+ { "total": 50, "type": 0, "item": "xp" }
786
+ ],
787
+ "principals": ["team_a", "team_b", "team_c"],
788
+ "notifications": [
789
+ { "event": 0, "type": 0, "scope": 0, "content": "Parabéns, {{player.name}} venceu a {{item.title}}!" }
790
+ ],
791
+ "active": true,
792
+ "autoExecute": true
115
793
  }
116
794
  ```
117
795
 
118
- ### Deletar Participação
119
- **Método:** DELETE
120
- **Endpoint:** `/v3/competition/join`
796
+ Observações: o reward sem `extra` (`total:50`) é **geral** — todos os vencedores recebem; o reward com `position_start/ends=1` é exclusivo do 1º. Custo de 10 `coin` é debitado no join. `SUM_ACHIEVEMENTS` soma achievements `type=0` (point) `item="point"` no período, por membro, reagrupado por time.
797
+
798
+ ### 10.3 Anti-pattern — o que NÃO fazer
121
799
 
122
- **Exemplo de Body:**
123
800
  ```json
124
801
  {
125
- "competition": "race",
126
- "player": "tom"
802
+ "_id": "bad",
803
+ "title": "Competição quebrada",
804
+ "period": { "expression": "-0M-;+1M+" },
805
+ "operation": { "type": 2, "attribute": "valor",
806
+ "filters": [ { "param": "uf", "operator": 2, "value": "SP" } ] },
807
+ "rewards": [
808
+ { "total": 100, "type": 5, "item": "lottery_x", "extra": { "position": 1 } }
809
+ ],
810
+ "active": true,
811
+ "autoExecute": true
127
812
  }
128
813
  ```
129
814
 
130
- ### Listar Líderes da Competição
131
- **Método:** POST
132
- **Endpoint:** `/v3/competition/leader/aggregate?id=:id`
815
+ Por que está errado:
816
+
817
+ - **`maxWinners` ausente** → default `-1` → `execute` falha no `$limit` (5.3).
818
+ - **`filters[].operator: 2` (LIKE)** → não suportado pelo `buildJsonFilter` → JSON malformado quebra o ranking (2.8). Use `1`/`3`/`4`/`5`/`6`/`7`.
819
+ - **`reward.type: 5` (LOTTERY)** → ignorado; só POINT/CHALLENGE/CATALOG_ITEM premiam (5.6).
820
+ - **`extra.position: 1`** → ignorado; vira prêmio geral. Use `position_start`/`position_ends` (3.7).
821
+
822
+ ---
823
+
824
+ ## Checklist de Configuração
133
825
 
134
- ### Executar Competição
135
- **Método:** GET
136
- **Endpoint:** `/v3/competition/:id/execute`
137
- **Descrição:** Finaliza a competição, calcula resultados e premia os vencedores.
826
+ - [ ] `active: true` (sem isso, não recebe join nem executa).
827
+ - [ ] `maxWinners >= 1` (default `-1` quebra a execução).
828
+ - [ ] `period.expression` válida; lembre que **editar** recalcula `startDate`/`endDate` (keywords deslocam a janela).
829
+ - [ ] `operation.type` definido; `item`/`attribute`/`achievement_type` coerentes com o tipo.
830
+ - [ ] `operation.sort`: `-1` (ou `0`) para "maior vence", `1` para "menor vence".
831
+ - [ ] Filtros de `operation` usam apenas operadores EQUAL/REGEX/GT/GTE/LT/LTE.
832
+ - [ ] Prêmios por posição usam **`position_start`/`position_ends`**, nunca `position`.
833
+ - [ ] Prêmios e custos usam apenas `type` 0/1/2 (POINT/CHALLENGE/CATALOG_ITEM).
834
+ - [ ] Itens referenciados (point_category, catalog_item, challenge, actionId) existem antes.
835
+ - [ ] Em `teamCompetition`, os times existem e têm membros em `team_player`; inscreva **times**, não players.
836
+ - [ ] Player/time inscrito existe na coleção `principal` (senão `*_is_not_allowed`).
837
+ - [ ] Notificações de vitória usam `event: 0` e `scope != 99`.
838
+ - [ ] Triggers de competição usam `entity: "competition"` ou `"competition_join"`.
839
+ - [ ] Após premiação, lembre-se de que `player_status` só atualiza no próximo evento do jogador.
840
+ - [ ] Para auto-execução: `autoExecute: true` **e** `period.endDate` definido; reative `autoExecute` após qualquer `undoExecute`.
138
841
 
139
- ### Reverter Execução
140
- **Método:** DELETE
141
- **Endpoint:** `/v3/competition/:id/execute`
842
+ ---
142
843
 
143
- ## Validações e Testes
844
+ ## Regras de Ouro (resumo operacional)
144
845
 
145
- - [ ] Competição aparece na lista GET /v3/competition
146
- - [ ] Jogador consegue se inscrever (join)
147
- - [ ] Lista de líderes retorna dados ordenados
148
- - [ ] Ao executar, vencedores recebem recompensas
149
- - [ ] Custo de inscrição é debitado corretamente
846
+ 1. Competição grava achievements **direto** não confie em level-up/webhook/`player_status` automáticos.
847
+ 2. `maxWinners >= 1` sempre.
848
+ 3. Prêmio por posição = `position_start`/`position_ends`. `position` não existe para o runtime.
849
+ 4. Filtros: 6 operadores. Outros quebram o ranking.
850
+ 5. Editar uma competição recalcula o período cuidado com keywords.
851
+ 6. Só o evento **win** notifica/triga na apuração. "create/join/end" não geram notificação.