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.
- package/.cursor/rules/funifier.mdc +38 -41
- package/.github/copilot-instructions.md +38 -41
- package/AGENTS.md +56 -49
- package/README.md +40 -22
- package/datasource-funifier-docs/.coverage.json +326 -0
- package/datasource-funifier-docs/.validation.json +593 -0
- package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
- package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
- package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
- package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
- package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
- package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
- package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
- package/datasource-funifier-docs/knowledge/index.md +4 -1
- package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
- package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
- package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
- package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
- package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
- package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
- package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
- package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
- package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
- package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
- package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
- package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
- package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
- package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
- package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
- package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
- package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
- package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
- package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
- package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
- package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
- package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
- package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
- package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
- package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
- package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
- package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
- package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
- package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
- package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
- package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
- package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
- package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
- package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
- package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
- package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
- package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
- package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
- package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
- package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
- package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +42 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/init.test.js +74 -3
- package/dist/cli/init.test.js.map +1 -1
- package/dist/cli/persona.d.ts +3 -0
- package/dist/cli/persona.d.ts.map +1 -0
- package/dist/cli/persona.js +25 -0
- package/dist/cli/persona.js.map +1 -0
- package/dist/mcp/bundle.js +119 -93
- package/dist/mcp/check-update.d.ts +5 -0
- package/dist/mcp/check-update.d.ts.map +1 -1
- package/dist/mcp/check-update.js +21 -10
- package/dist/mcp/check-update.js.map +1 -1
- package/dist/mcp/check-update.test.d.ts +2 -0
- package/dist/mcp/check-update.test.d.ts.map +1 -0
- package/dist/mcp/check-update.test.js +33 -0
- package/dist/mcp/check-update.test.js.map +1 -0
- package/dist/mcp/index.js +2 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/prompts/templates.d.ts.map +1 -1
- package/dist/mcp/prompts/templates.js +35 -0
- package/dist/mcp/prompts/templates.js.map +1 -1
- package/dist/mcp/resources/documentation.d.ts +1 -1
- package/dist/mcp/resources/documentation.d.ts.map +1 -1
- package/dist/mcp/resources/documentation.js +39 -3
- package/dist/mcp/resources/documentation.js.map +1 -1
- package/dist/mcp/tools/connect.d.ts.map +1 -1
- package/dist/mcp/tools/connect.js +18 -8
- package/dist/mcp/tools/connect.js.map +1 -1
- package/dist/mcp/tools/database.d.ts.map +1 -1
- package/dist/mcp/tools/database.js +59 -47
- package/dist/mcp/tools/database.js.map +1 -1
- package/dist/mcp/tools/database.test.js +2 -2
- package/dist/mcp/tools/database.test.js.map +1 -1
- package/dist/mcp/tools/delete.d.ts.map +1 -1
- package/dist/mcp/tools/delete.js +13 -3
- package/dist/mcp/tools/delete.js.map +1 -1
- package/dist/mcp/tools/execute.d.ts.map +1 -1
- package/dist/mcp/tools/execute.js +20 -9
- package/dist/mcp/tools/execute.js.map +1 -1
- package/dist/mcp/tools/folder.d.ts.map +1 -1
- package/dist/mcp/tools/folder.js +22 -12
- package/dist/mcp/tools/folder.js.map +1 -1
- package/dist/mcp/tools/get.d.ts.map +1 -1
- package/dist/mcp/tools/get.js +16 -6
- package/dist/mcp/tools/get.js.map +1 -1
- package/dist/mcp/tools/index.d.ts +1 -1
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +28 -1
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/list.d.ts.map +1 -1
- package/dist/mcp/tools/list.js +38 -14
- package/dist/mcp/tools/list.js.map +1 -1
- package/dist/mcp/tools/logs.d.ts.map +1 -1
- package/dist/mcp/tools/logs.js +15 -5
- package/dist/mcp/tools/logs.js.map +1 -1
- package/dist/mcp/tools/save.d.ts.map +1 -1
- package/dist/mcp/tools/save.js +14 -4
- package/dist/mcp/tools/save.js.map +1 -1
- package/dist/mcp/tools/save.test.js +3 -3
- package/dist/mcp/tools/save.test.js.map +1 -1
- package/dist/mcp/tools/search-docs.d.ts +3 -0
- package/dist/mcp/tools/search-docs.d.ts.map +1 -0
- package/dist/mcp/tools/search-docs.js +102 -0
- package/dist/mcp/tools/search-docs.js.map +1 -0
- package/package.json +6 -2
- package/skills/acquire-funifier-knowledge/SKILL.md +155 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +86 -0
- package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
- package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
- package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
- package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
- package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
- package/skills/funifier/SKILL.md +88 -0
- package/skills/funifier/references/configure-security.md +96 -0
- package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
- package/skills/funifier/references/create-aggregate.md +144 -0
- package/skills/funifier/references/create-challenge.md +116 -0
- package/skills/funifier/references/create-competition.md +98 -0
- package/skills/funifier/references/create-crossword.md +574 -0
- package/skills/funifier/references/create-custom-object.md +91 -0
- package/skills/funifier/references/create-custom-page.md +135 -0
- package/skills/funifier/references/create-folder.md +104 -0
- package/skills/funifier/references/create-lastmile.md +643 -0
- package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
- package/skills/funifier/references/create-level.md +94 -0
- package/skills/funifier/references/create-lottery.md +913 -0
- package/skills/funifier/references/create-mystery.md +769 -0
- package/skills/funifier/references/create-notification.md +75 -0
- package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
- package/skills/funifier/references/create-quiz.md +98 -0
- package/skills/funifier/references/create-scheduler.md +141 -0
- package/skills/funifier/references/create-story.md +636 -0
- package/skills/funifier/references/create-swap.md +95 -0
- package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
- package/skills/funifier/references/create-virtual-good.md +96 -0
- package/skills/funifier/references/create-webhook.md +72 -0
- package/skills/funifier/references/create-websocket.md +71 -0
- package/skills/funifier/references/create-widget.md +76 -0
- package/skills/funifier/references/debug.md +87 -0
- package/skills/funifier/references/help.md +81 -0
- package/skills/funifier/references/implement-frontend.md +106 -0
- package/skills/funifier/references/import-csv.md +75 -0
- package/skills/funifier/references/manage-player.md +82 -0
- package/skills/funifier/references/manage-team.md +76 -0
- package/skills/funifier/references/upload-file.md +91 -0
- package/skills/funifier-create-aggregate/SKILL.md +0 -127
- package/skills/funifier-create-challenge/SKILL.md +0 -88
- package/skills/funifier-create-custom-page/SKILL.md +0 -127
- package/skills/funifier-create-level/SKILL.md +0 -87
- package/skills/funifier-create-quiz/SKILL.md +0 -87
- package/skills/funifier-create-scheduler/SKILL.md +0 -127
- package/skills/funifier-create-virtual-good/SKILL.md +0 -87
- package/skills/funifier-debug/SKILL.md +0 -92
- package/skills/funifier-help/SKILL.md +0 -86
- package/skills/funifier-implement-frontend/SKILL.md +0 -90
- package/skills/funifier-index/SKILL.md +0 -58
|
@@ -1,149 +1,851 @@
|
|
|
1
|
-
#
|
|
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
|
-
|
|
7
|
+
---
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
## 1. Visão Geral
|
|
9
10
|
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
- Para premiar os melhores colocados em um período
|
|
14
|
-
- Para engajar equipes com competições internas
|
|
13
|
+
Papel arquitetural:
|
|
15
14
|
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
"
|
|
91
|
-
"autoExecute": true,
|
|
92
|
-
"extra": {},
|
|
93
|
-
"techniques": ["GT26"],
|
|
94
|
-
"_id": "race"
|
|
758
|
+
"autoExecute": true
|
|
95
759
|
}
|
|
96
760
|
```
|
|
97
761
|
|
|
98
|
-
|
|
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
|
-
###
|
|
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
|
-
"
|
|
114
|
-
"
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
"
|
|
126
|
-
"
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
**
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
**
|
|
137
|
-
|
|
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
|
-
|
|
140
|
-
**Método:** DELETE
|
|
141
|
-
**Endpoint:** `/v3/competition/:id/execute`
|
|
842
|
+
---
|
|
142
843
|
|
|
143
|
-
##
|
|
844
|
+
## Regras de Ouro (resumo operacional)
|
|
144
845
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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: só 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.
|