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,46 +1,1144 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `achievement`
|
|
2
2
|
|
|
3
|
+
**Acesso Studio:** `/studio/achievement`
|
|
3
4
|
**API Endpoint:** `/v3/achievement`
|
|
5
|
+
**Endpoint Legado:** `/2.0.0/achievement`
|
|
6
|
+
**Coleção MongoDB:** `achievement`
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
---
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
## 1. Visão Geral
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
O módulo `achievement` é o registro imutável de **todo evento de recompensa ou perda** dentro da gamificação Funifier. Cada documento da coleção `achievement` representa um único "movimento de conta" sobre o saldo de um jogador (ou time) em uma das dimensões suportadas: pontos, desafios completos, items de catálogo, níveis, coroamentos, loterias, mystery boxes, character star stats, bônus, competições e tipos customizados.
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
- Para criar relatórios de desempenho
|
|
13
|
-
- Para usar em aggregates e dashboards
|
|
14
|
-
- Para triggers que reagem a conquistas
|
|
14
|
+
Papel arquitetural:
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
- É a **única fonte da verdade** para totais de pontos, badges (challenges), items, levels e demais conquistas — não há um campo `Player.points`. Todos os totais são calculados sob demanda via aggregation sobre a coleção `achievement` (vide seção 2.5).
|
|
17
|
+
- Atua como **log de eventos de gamificação**: cada lançamento é uma linha; estornos são lançamentos com `total` negativo.
|
|
18
|
+
- Concentra o **motor principal de avaliação de desafios** (`AchievementManager.fireAction`) — apesar do nome, é este módulo, não o módulo `challenge`, que decide se uma `action_log` recém-recebida completa algum desafio, contabiliza progresso, aplica deduções e dispara recompensas.
|
|
19
|
+
- Atualiza materializações derivadas: `player_status` (snapshot de carteira por jogador) e `game_status` (totalizador de gamificação).
|
|
20
|
+
|
|
21
|
+
Problemas que resolve:
|
|
22
|
+
|
|
23
|
+
- Auditoria completa de tudo que cada jogador ganhou/perdeu, com timestamp e origem opcional (`extra.origin`).
|
|
24
|
+
- Cálculo idempotente de totais (sempre via `$sum` de `total`), o que permite estornos com `total` negativo sem corromper o saldo.
|
|
25
|
+
- Ponto único de inserção para que triggers (`before_win` / `after_win`), webhooks (`achievement_created`) e progressão de níveis sejam disparados de forma consistente independentemente da origem (desafio, compra, mystery box, ajuste manual, etc.).
|
|
26
|
+
|
|
27
|
+
Relação direta com outros módulos:
|
|
28
|
+
|
|
29
|
+
- `action` — toda chamada a `ActionManager.trackSynchonous(action)` invoca `AchievementManager.fireAction(action)` ao final. Esta é a porta de entrada principal.
|
|
30
|
+
- `challenge` — `fireAction` lê `challenge`, avalia regras e gera achievements do tipo CHALLENGE.
|
|
31
|
+
- `point_category`, `catalog_item`, `lottery`, `mystery_box`, `bonus`, `crown`, `competition`, `character_star_stats` — todos esses módulos inserem achievements via `addAchievement` ou via `fireAction`.
|
|
32
|
+
- `player` — `PlayerManager.updateUserStatus(player)` recalcula `player_status` a partir da coleção `achievement`.
|
|
33
|
+
- `trigger` — `before_win` / `after_win` são disparados em torno de cada `addAchievement` significativo dentro de `fireAction`.
|
|
34
|
+
- `webhook` — `achievement_created` é disparado uma vez ao fim de `fireAction` com a lista completa de achievements gerados.
|
|
35
|
+
- `lottery_ticket` — caso especial: `type=50 (LOTTERY_TICKET)` **não** gera registro em `achievement`; em vez disso, insere documentos na coleção `lottery_ticket`.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 2. Arquitetura e Fluxos
|
|
40
|
+
|
|
41
|
+
### 2.1 Classes envolvidas
|
|
42
|
+
|
|
43
|
+
| Classe | Papel |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `com.funifier.engine.achievement.Achievement` | Entidade/POJO principal — documento raiz |
|
|
46
|
+
| `com.funifier.engine.achievement.AchievementManager` | Manager monolítico (3232 linhas): CRUD, avaliação de desafios, level-up, atualização de status, queries agregadas |
|
|
47
|
+
| `com.funifier.rest.v3.rest.AchievementRest` | Controller REST v3 (`/v3/achievement`) |
|
|
48
|
+
| `com.funifier.rest.engine.AchievementRest` | Controller REST legado (`/2.0.0/achievement`) |
|
|
49
|
+
| `com.funifier.engine.achievement.ChallengeProgress` | Sub-entidade persistida em `challenge_progress` |
|
|
50
|
+
| `com.funifier.engine.achievement.ChallengeRuleProgress` | Sub-elemento de `ChallengeProgress.rules` |
|
|
51
|
+
| `com.funifier.engine.achievement.PlayerStatus` | Snapshot persistido em `player_status` |
|
|
52
|
+
| `com.funifier.engine.achievement.GameStatus` | Snapshot global persistido em `game_status` |
|
|
53
|
+
| `com.funifier.engine.achievement.GroupAttribute` | DTO interno de aggregation (`total`, `max`) |
|
|
54
|
+
| `com.funifier.engine.achievement.GroupLog` | DTO interno legado de aggregation (`total`) |
|
|
55
|
+
|
|
56
|
+
Não há `Repository`/`Dao` dedicado — a manipulação MongoDB é feita diretamente via `Jongo` dentro do `AchievementManager`.
|
|
57
|
+
|
|
58
|
+
### 2.2 Pipeline principal — `fireAction(ActionLog trigger)`
|
|
59
|
+
|
|
60
|
+
Comentário do código (linha 105-116 de `AchievementManager.java`) descreve as 7 etapas oficiais:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
1. PRE CONTROLLERS : principal, requirements
|
|
64
|
+
2. FIND CHALLENGES : action, active, when, teamchallenge
|
|
65
|
+
3. EVALUATE PLAYER : player, principals, level, limit, requirements
|
|
66
|
+
4. EVALUATE RULES : rules, every, filters, (simplify ignore operators)
|
|
67
|
+
5. REGISTER REWARDS : achievement, points, itens, requirements (deductions)
|
|
68
|
+
6. UPDATE STATUS : challenge_progress, level_up, player_status
|
|
69
|
+
7. POS PROCESS : notifications, trigger_url
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Detalhamento numerado conforme o código real:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
[1] Resolve principal (jogador OU time) por userId / teamId em `principal`.
|
|
76
|
+
Se principal não existir → sai e retorna lista vazia. (linhas 122-129)
|
|
77
|
+
|
|
78
|
+
[2] Se principal é PLAYER e a Action tem RewardPoints configurados:
|
|
79
|
+
[2.1] Para cada RewardPoint:
|
|
80
|
+
- operation = NONE → totalPoints = p.total
|
|
81
|
+
- operation = MULTIPLY_BY_ATTRIBUTE → totalPoints = p.total * trigger.attributes[p.value]
|
|
82
|
+
- operation = DIVIDE_BY_ATTRIBUTE → totalPoints = p.total / trigger.attributes[p.value]
|
|
83
|
+
- ATENÇÃO: parsing de atributo silencioso — exceção é engolida (linhas 162-176)
|
|
84
|
+
[2.2] Se totalPoints != 0:
|
|
85
|
+
- trigger.execute(point_category, item, BEFORE_WIN)
|
|
86
|
+
- addAchievement (TYPE_POINT)
|
|
87
|
+
- trigger.execute(point_category, item, AFTER_WIN)
|
|
88
|
+
[2.3] Dispara NotificationDefinition.EVENT_WIN das notifications da Action
|
|
89
|
+
|
|
90
|
+
[3] FIND CHALLENGES (findValidChallenges):
|
|
91
|
+
db.challenge.find({ active: true, rules: { $elemMatch: { actionId: # } } })
|
|
92
|
+
Filtra por `Challenge.when.isDateAllowed(...)` em memória (não em query).
|
|
93
|
+
Conversão de timezone considera Security.getTimeZone() do tenant.
|
|
94
|
+
|
|
95
|
+
[4] EVALUATE PRINCIPAL (evaluatePrincipal):
|
|
96
|
+
Para cada challenge:
|
|
97
|
+
[4.1] teamChallenge match (player vs team) ← evaluateLimit + isUserChallengePrincipal
|
|
98
|
+
[4.2] limit (LIMIT_PER_PLAYER/TEAM/GAME)
|
|
99
|
+
[4.3] principals contém player/time ?
|
|
100
|
+
[4.4] requirements (sumTotalRewards >= r.total para cada Requirement do Challenge)
|
|
101
|
+
[4.5] join obrigatório? (challenge.join.required → JoinLog existente E em timeframe)
|
|
102
|
+
Reprovados são removidos silenciosamente.
|
|
103
|
+
|
|
104
|
+
[5] EVALUATE RULES — para cada challenge aprovado:
|
|
105
|
+
Para cada rule do challenge:
|
|
106
|
+
- rule_total computado: literal numérico, ou expressão Mustache+exp4j com {player, team, rule, challenge}
|
|
107
|
+
- tipo de regra (getRuleType):
|
|
108
|
+
RULE_TYPE_EVERY (2): everyAmount > 0 && everyScale != NONE && timeAmount > 0
|
|
109
|
+
RULE_TYPE_SEVERAL (1): rule_total > 1 && everyScale == NONE
|
|
110
|
+
RULE_TYPE_ONE (0): caso restante
|
|
111
|
+
- se rule.prepared está setado → carrega ChallengeRulePrepared e injeta entity/aggregate
|
|
112
|
+
- se rule.entity + rule.aggregate → executa aggregation customizada substituindo:
|
|
113
|
+
FUNIFIER_PLAYER_IDS, FUNIFIER_TRIGGER_TIME, FUNIFIER_TRIGGER_ACTION_ID,
|
|
114
|
+
FUNIFIER_RULE_ACTION_ID, FUNIFIER_PERIOD_START_TIME, FUNIFIER_PERIOD_END_TIME
|
|
115
|
+
- senão usa o caminho countActionLogsRegisteredBetweenDatesV3 ou
|
|
116
|
+
checkActionLogsFrequencyRegisteredBetweenDates conforme o tipo.
|
|
117
|
+
|
|
118
|
+
[6] AVALIAÇÃO DE COMPLETUDE (linhas 425-453):
|
|
119
|
+
- RANGE_ALL (0) → _true == rules.length
|
|
120
|
+
- RANGE_ANY (1) → _true > 0
|
|
121
|
+
- RANGE_ALL_IN_ORDER (2) → _true == rules.length E orderTime[i] <= orderTime[i+1]
|
|
122
|
+
Se ordem quebra: marca todas rules >= i como uncompletedBecauseOfTime (zera percent, completed=false)
|
|
123
|
+
|
|
124
|
+
[7] SE COMPLETED:
|
|
125
|
+
[7.1] deleteProgress(player, challenge)
|
|
126
|
+
[7.2] Se challenge.join.required:
|
|
127
|
+
- completeAll(joinLog)
|
|
128
|
+
- guarda extra.join = joinLog.id no achievement
|
|
129
|
+
- SE NÃO HÁ JoinLog NÃO COMPLETO → "continue" (pula este challenge inteiramente)
|
|
130
|
+
[7.3] addAchievement(TYPE_CHALLENGE, total=1) com trigger.execute(challenge, BEFORE_WIN/AFTER_WIN)
|
|
131
|
+
[7.4] REGISTER POINTS (challenge.points[]):
|
|
132
|
+
mesma lógica do passo [2.1] com perPlayer→winners para teamChallenge
|
|
133
|
+
se challenge.propagateOrigin == true → achievement.extra.origin = parent_achievement.id
|
|
134
|
+
atualiza updatePlayerStatus(winner) para CADA winner que recebe pontos
|
|
135
|
+
[7.5] REGISTER REWARDS (challenge.rewards[]):
|
|
136
|
+
Tipos suportados: TYPE_POINT, TYPE_CATALOG_ITEM, TYPE_CHALLENGE, TYPE_LOTTERY_TICKET
|
|
137
|
+
Casos especiais com item == "FUNIFIER_RANDOM":
|
|
138
|
+
- CATALOG_ITEM: catalogManager.findValidRandom(...) ou findRandom — filtra por r.folder
|
|
139
|
+
- LOTTERY_TICKET: lotteryManager.findValidRandom(...) e cria N tickets em loop
|
|
140
|
+
Caso item específico tipo LOTTERY_TICKET: cria N LotteryTickets, NÃO cria Achievement
|
|
141
|
+
Trigger collection é resolvida por type (point_category, challenge, catalog_item)
|
|
142
|
+
updatePlayerStatus(winner) é chamado APÓS cada reward
|
|
143
|
+
[7.6] REGISTER NOTIFICATIONS de NotificationDefinition.EVENT_WIN do challenge
|
|
144
|
+
[7.7] DEDUCT REQUIREMENTS (deductRequirements):
|
|
145
|
+
Para cada Requirement do Challenge com operation=DEDUCT e tipo válido:
|
|
146
|
+
insere achievement com total = -|r.total| (deduz)
|
|
147
|
+
a.extra.origin = challenge_achievement.id (se propagateOrigin)
|
|
148
|
+
|
|
149
|
+
[8] SE NÃO COMPLETED MAS (rules.length==1 || _true>0 || challenge.trackFullProgress):
|
|
150
|
+
deleteProgress + addProgress com percent_completed calculado
|
|
151
|
+
progress só é salvo se percent_completed > 0 OU trackFullProgress
|
|
152
|
+
|
|
153
|
+
[9] UPDATE PLAYER STATUS (se algum challenge foi avaliado, evaluated=true):
|
|
154
|
+
Se principal é PLAYER → updatePlayerStatus(userId)
|
|
155
|
+
Se principal é TEAM → TODO (sem implementação) ← marcador no código
|
|
156
|
+
|
|
157
|
+
[10] AVALIAÇÃO RECURSIVA DE TIME:
|
|
158
|
+
Se principal é PLAYER, busca times do jogador e re-invoca fireAction(new ActionLog(actionId, teamId, time, attributes))
|
|
159
|
+
Isto causa segunda passagem com principal=TEAM, avaliando challenges com teamChallenge=true.
|
|
160
|
+
|
|
161
|
+
[11] WEBHOOK:
|
|
162
|
+
Se result.size() > 0 → WebhookManager.execute("achievement_created", result)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Fluxo de avaliação — `fireAction`
|
|
166
|
+
|
|
167
|
+
```mermaid
|
|
168
|
+
flowchart TB
|
|
169
|
+
A[ActionLog trigger] --> B{Principal existe?}
|
|
170
|
+
B -- não --> Z[Retorna lista vazia]
|
|
171
|
+
B -- player --> C[Reward points da Action]
|
|
172
|
+
B -- team --> D[Pula reward points individuais]
|
|
173
|
+
C --> E[findValidChallenges]
|
|
174
|
+
D --> E
|
|
175
|
+
E --> F[evaluatePrincipal: limit, principals, requirements, join]
|
|
176
|
+
F --> G[Loop rules: countActionLogs / aggregate / frequency]
|
|
177
|
+
G --> H{Completed?}
|
|
178
|
+
H -- sim --> I[deleteProgress + addAchievement CHALLENGE]
|
|
179
|
+
H -- não --> J{trackFullProgress OR _true>0 OR 1 rule?}
|
|
180
|
+
I --> K[Distribui pontos + rewards + tickets]
|
|
181
|
+
K --> L[deductRequirements]
|
|
182
|
+
J -- sim --> M[addProgress challenge_progress]
|
|
183
|
+
J -- não --> N[Nenhum progress salvo]
|
|
184
|
+
L --> O[updatePlayerStatus]
|
|
185
|
+
M --> O
|
|
186
|
+
N --> O
|
|
187
|
+
O --> P{Principal é player?}
|
|
188
|
+
P -- sim --> Q[Loop teams do player → fireAction recursivo]
|
|
189
|
+
P -- não --> R[Skip recursão]
|
|
190
|
+
Q --> S[Webhook achievement_created]
|
|
191
|
+
R --> S
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### 2.3 Pipeline — `addAchievement(Achievement)` (POST direto)
|
|
195
|
+
|
|
196
|
+
Caminho usado pelo endpoint REST `POST /v3/achievement` e também internamente como ajuste manual de saldo:
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
[1] Se achievement.id == null → Guid.newShortGuid() (SHORT GUID, NÃO ObjectId Mongo)
|
|
200
|
+
[2] Se achievement.time == null → new Date() (hora do servidor)
|
|
201
|
+
[3] jongo.getCollection("achievement").save(achievement) (upsert por _id)
|
|
202
|
+
[4] (chamado pelo Rest, NÃO pelo Manager): updateUserStatus(achievement.player)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Importante:** `POST /v3/achievement` **não dispara**:
|
|
206
|
+
|
|
207
|
+
- Triggers (`before_win`/`after_win`) — só são disparados quando `addAchievement` é chamado de dentro do `fireAction` ou de cálculos derivados (purchase, mystery box, etc.).
|
|
208
|
+
- Webhooks (`achievement_created`) — só dentro do `fireAction`.
|
|
209
|
+
- Avaliação de level-up (exceto via `updateUserStatus` que internamente roda `evaluateLevelUp` em loop).
|
|
210
|
+
|
|
211
|
+
### 2.4 Pipeline — `updatePlayerStatus(player)`
|
|
212
|
+
|
|
213
|
+
Atualização materializada de `player_status` (coleção `player_status`):
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
[1] Busca Player.findById(player)
|
|
217
|
+
[2] Loop while(evaluateLevelUp(player, achievements)):
|
|
218
|
+
- busca level corrente (último Achievement TYPE_LEVEL ordenado por time DESC)
|
|
219
|
+
- busca próximo level por position
|
|
220
|
+
- lê LevelConfig {_id:"global"}.pointCategory para decidir categoria de pontos
|
|
221
|
+
- calcula points = sumTotalRewards(player, TYPE_POINT, pointCategory)
|
|
222
|
+
- se points >= nextLevel.minPoints E evaluateRequirements(nextLevel.requirements):
|
|
223
|
+
addAchievement(TYPE_LEVEL) + trigger BEFORE_WIN/AFTER_WIN em ENTITY_LEVEL
|
|
224
|
+
envia notificações EVENT_WIN do level
|
|
225
|
+
retorna true → loop continua (permite múltiplos levels em sequência)
|
|
226
|
+
[3] Calcula totais por tipo via aggregation:
|
|
227
|
+
- total_challenges, challenges{} via sumTotalRewards/getTotalRewardsGroupedByType (TYPE_CHALLENGE)
|
|
228
|
+
- total_points, point_categories{} via sumTotalPoints/getTotalPointsGroupedByType (TYPE_POINT)
|
|
229
|
+
- total_catalog_items, catalog_items{} via sumTotalRewards/getTotalRewardsGroupedByType (TYPE_CATALOG_ITEM)
|
|
230
|
+
[4] Monta LevelProgress com percent_completed calculado entre level atual e próximo
|
|
231
|
+
[5] Carrega challenge_progress (lista filtrada por player de challenge_progress)
|
|
232
|
+
[6] Copia p.teams, p.friends
|
|
233
|
+
[7] Carrega positions (leaderboards) via CrowningManager.findLeadersByPlayerId
|
|
234
|
+
[8] Salva: c.remove("{_id:#}") + c.save(status) ← DELETE + INSERT, não upsert
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### 2.5 Sumarização de saldos — `sumTotalRewards` (cálculo central)
|
|
238
|
+
|
|
239
|
+
Todo "saldo" do jogador é calculado via aggregation sobre `achievement`:
|
|
240
|
+
|
|
241
|
+
```js
|
|
242
|
+
// sumTotalRewards(player, type, item)
|
|
243
|
+
db.achievement.aggregate([
|
|
244
|
+
{ $match: { player: <player>, type: <type>, item: <item> } },
|
|
245
|
+
{ $group: { _id: "$item", total: { $sum: "$total" } } }
|
|
246
|
+
])
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
// sumTotalPoints(player) — equivalente para TYPE_POINT
|
|
251
|
+
db.achievement.aggregate([
|
|
252
|
+
{ $match: { player: <player>, type: 0 } },
|
|
253
|
+
{ $group: { _id: "$type", total: { $sum: "$total" } } }
|
|
254
|
+
])
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
// getTotalRewardsGroupedByType — retorna mapa item→total
|
|
259
|
+
db.achievement.aggregate([
|
|
260
|
+
{ $match: { player: <player>, type: <type> } },
|
|
261
|
+
{ $group: { _id: "$item", total: { $sum: "$total" } } }
|
|
262
|
+
])
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Consequências práticas:
|
|
266
|
+
|
|
267
|
+
- Lançamentos com `total` negativo subtraem do saldo (mecanismo de estorno / dedução).
|
|
268
|
+
- Não há "snapshot" de saldo armazenado no `Player`. Performance de leitura depende de índices em `{player:1, type:1, item:1}` (não documentados no código).
|
|
269
|
+
|
|
270
|
+
### 2.6 Interações entre módulos (vista de chamadas)
|
|
271
|
+
|
|
272
|
+
```mermaid
|
|
273
|
+
sequenceDiagram
|
|
274
|
+
participant Client
|
|
275
|
+
participant ActionRest
|
|
276
|
+
participant ActionManager
|
|
277
|
+
participant AchievementManager
|
|
278
|
+
participant ChallengeColl as challenge<br/>(MongoDB)
|
|
279
|
+
participant Trigger
|
|
280
|
+
participant Webhook
|
|
281
|
+
|
|
282
|
+
Client->>ActionRest: POST /v3/action/log
|
|
283
|
+
ActionRest->>ActionManager: trackSynchonous(action)
|
|
284
|
+
ActionManager->>ActionManager: addLog(action)
|
|
285
|
+
ActionManager->>AchievementManager: fireAction(action)
|
|
286
|
+
AchievementManager->>ChallengeColl: find({active:true, rules.actionId})
|
|
287
|
+
AchievementManager->>Trigger: BEFORE_WIN (point_category)
|
|
288
|
+
AchievementManager->>AchievementManager: addAchievement(POINT)
|
|
289
|
+
AchievementManager->>Trigger: AFTER_WIN (point_category)
|
|
290
|
+
loop para cada challenge completado
|
|
291
|
+
AchievementManager->>AchievementManager: addAchievement(CHALLENGE)
|
|
292
|
+
AchievementManager->>AchievementManager: addAchievement(POINT/CATALOG_ITEM/...)
|
|
293
|
+
AchievementManager->>AchievementManager: deductRequirements
|
|
294
|
+
end
|
|
295
|
+
AchievementManager->>AchievementManager: updatePlayerStatus (loop evaluateLevelUp)
|
|
296
|
+
AchievementManager->>AchievementManager: fireAction recursivo para cada team
|
|
297
|
+
AchievementManager->>Webhook: execute("achievement_created", result)
|
|
298
|
+
ActionManager-->>Client: 200 + lista de Achievements
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Outros pontos de entrada (chamadores de `addAchievement` fora do `fireAction`):
|
|
302
|
+
|
|
303
|
+
| Origem | Quando dispara |
|
|
304
|
+
|---|---|
|
|
305
|
+
| `PurchaseManager.buy` | Compra de catalog item — dedução de pontos + ganho de CATALOG_ITEM |
|
|
306
|
+
| `LotteryManager.execute` | Ganhador de loteria — TYPE_LOTTERY com prêmio |
|
|
307
|
+
| `MysteryBoxManager.execute` | Mystery box aberto — TYPE_MYSTERY_BOX + recompensas (PrizesType) |
|
|
308
|
+
| `LostManager.execute` | Perda de itens — total negativo (estorno) |
|
|
309
|
+
| `CrowningManager.calculateCrown` | Coroamento de leaderboard — TYPE_CROWN |
|
|
310
|
+
| `CharacterStarManager.evaluateLevelUp` | Subiu nível de Character Star — TYPE_CHARACTER_STAR_STAT |
|
|
311
|
+
| `BetManager.execute` / `purchase` / `delete` / `undoExecute` | Apostas — múltiplos tipos com origem propagada |
|
|
312
|
+
| `SwapManager.*` | Troca entre players — pares de achievements (positivo/negativo) |
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## 3. Estrutura dos Objetos
|
|
317
|
+
|
|
318
|
+
### 3.1 `Achievement` — documento raiz (coleção `achievement`)
|
|
319
|
+
|
|
320
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
321
|
+
|---|---|---|---|---|
|
|
322
|
+
| `_id` | String | `Guid.newShortGuid()` se ausente | Não (auto) | Identificador SHORT GUID interno do Funifier. **Não é** ObjectId do Mongo |
|
|
323
|
+
| `player` | String | — | **Sim** | Identificador do jogador OU time que recebeu/perdeu a conquista |
|
|
324
|
+
| `total` | double | — | Sim (significativo) | Quantidade da conquista. Pode ser **negativo** (estorno/dedução). `total == 0` faz o registro ser ignorado em alguns fluxos internos |
|
|
325
|
+
| `type` | int | — | Sim | Tipo de conquista (ver enum abaixo) |
|
|
326
|
+
| `item` | String | — | Não em runtime | Identificador do item conquistado (ex: id de point_category, challenge, level…). Para `TYPE_POINT` é o id da category |
|
|
327
|
+
| `time` | Date | `new Date()` se ausente em `addAchievement(Achievement)` (a sobrecarga com 1 arg) | Não (auto) | Timestamp do evento. Sempre serializado pelo Jackson como número (epoch ms) na resposta JSON |
|
|
328
|
+
| `extra` | Map<String,Object> | — | Não | Atributos arbitrários do achievement. Convenções de uso conhecidas: `origin` (id do achievement pai quando `challenge.propagateOrigin=true`), `origin_item`, `origin_type`, `join` (id do JoinLog completado) |
|
|
329
|
+
|
|
330
|
+
Anotação Jackson na classe: `@JsonIgnoreProperties(ignoreUnknown=true)` — campos extras enviados no POST são **silenciosamente descartados** se não estiverem mapeados acima.
|
|
331
|
+
|
|
332
|
+
Construtor com 6 args (`id, player, total, type, item, time`) **não** popula `extra` — `extra` é setado depois via mutação direta.
|
|
333
|
+
|
|
334
|
+
#### Campos computados / não persistidos
|
|
335
|
+
|
|
336
|
+
Nenhum — todos os campos da entidade `Achievement` são persistidos.
|
|
337
|
+
|
|
338
|
+
#### Campos removidos silenciosamente
|
|
339
|
+
|
|
340
|
+
Nenhum no `Achievement`. Porém: a sobrecarga `addAchievement(Achievement)` (sem Jongo) preenche `time = new Date()` se vier `null`, **substituindo** silenciosamente o valor do cliente quando ele envia `time: null` (mas mantém se enviar uma data inválida que o Jackson interprete como null).
|
|
341
|
+
|
|
342
|
+
#### Constantes legadas comentadas no código
|
|
343
|
+
|
|
344
|
+
```java
|
|
345
|
+
//public static final int TYPE_ACTION = 100; // linha 31 — declarado/removido
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
A constante `TYPE_ACTION = 100` está comentada na entidade — não existe em runtime. Documentos persistidos com `type: 100` são tecnicamente possíveis (o campo é `int`, sem validação de enum), mas nenhum código produzirá ou consumirá esse valor.
|
|
349
|
+
|
|
350
|
+
### 3.2 Tabela completa de `type` (Achievement.TYPE_*)
|
|
351
|
+
|
|
352
|
+
| Valor | Constante Java | Significado operacional | Onde é gerado |
|
|
353
|
+
|---|---|---|---|
|
|
354
|
+
| `0` | `TYPE_POINT` | Ponto ganho em uma `point_category` específica. `item` = id da category | `fireAction` (action.points + challenge.points + challenge.rewards); `PurchaseManager`; `LostManager` |
|
|
355
|
+
| `1` | `TYPE_CHALLENGE` | Desafio completado. `item` = id do challenge. `total` é sempre 1 nos fluxos automáticos | `fireAction` (challenge completo) |
|
|
356
|
+
| `2` | `TYPE_CATALOG_ITEM` | Item de catálogo adquirido. `item` = id do catalog_item | `PurchaseManager.buy`; `fireAction` (challenge.rewards) |
|
|
357
|
+
| `3` | `TYPE_LEVEL` | Level atingido. `item` = id do level. `total` = 1 | `evaluateLevelUp` (chamado dentro de `updatePlayerStatus`) |
|
|
358
|
+
| `4` | `TYPE_CROWN` | Coroamento obtido em leaderboard. `item` = id do crown | `CrowningManager.calculateCrown` |
|
|
359
|
+
| `5` | `TYPE_LOTTERY` | Vencedor de loteria. `item` = id da loteria | `LotteryManager.execute` |
|
|
360
|
+
| `6` | `TYPE_MYSTERY_BOX` | Mystery box aberta. `item` = id do mystery_box | `MysteryBoxManager.execute` |
|
|
361
|
+
| `7` | `TYPE_CHARACTER_STAR_STAT` | Subiu nível de Character Star. `item` = id do level | `CharacterStarManager.evaluateLevelUp` |
|
|
362
|
+
| `8` | `TYPE_BONUS` | Bônus recebido (Last Mile). `item` = id do bonus | `BonusManager` |
|
|
363
|
+
| `9` | `TYPE_COMPETITION` | Conquista de competição. `item` = id da competição | `CompetitionManager` |
|
|
364
|
+
| `99` | `TYPE_CUSTOM` | Tipo customizado / livre. Sem semântica do core. Pode ser usado para integrações | Exclusivamente externo (`POST /v3/achievement`) |
|
|
365
|
+
| `50` | `TYPE_LOTTERY_TICKET` | **NÃO É REGISTRADO COMO ACHIEVEMENT.** Comentário do código (linha 29): *"nao e registrado como achievement, fica em uma colecao a parte lottery_ticket"*. Aparece em `Requirement.type` e em `findOptionsByType` mas o `fireAction` insere documentos em `lottery_ticket` em vez disso | Convenção interna em `Requirement` |
|
|
366
|
+
|
|
367
|
+
### 3.3 `ChallengeProgress` — sub-entidade (coleção `challenge_progress`)
|
|
368
|
+
|
|
369
|
+
Persiste o **progresso parcial** de um jogador em um desafio não-completo.
|
|
370
|
+
|
|
371
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
372
|
+
|---|---|---|---|---|
|
|
373
|
+
| `_id` | String | `Guid.newShortGuid()` em `addProgress` | Não (auto) | Identificador |
|
|
374
|
+
| `player` | String | — | Sim | Jogador (ou time) que está progredindo |
|
|
375
|
+
| `challenge` | String | — | Sim | Id do challenge |
|
|
376
|
+
| `name` | String | — | Não | Cópia do `challenge.challenge` (nome) |
|
|
377
|
+
| `image` | Image | — | Não | Cópia de `challenge.badgeUrl` no momento do progresso (snapshot) |
|
|
378
|
+
| `rules_completed` | int | 0 | — | Quantas rules estão completas |
|
|
379
|
+
| `rules_total` | int | 0 | — | Total de rules do challenge |
|
|
380
|
+
| `percent_completed` | double | 0 | — | Calculado em `calculatePercentCompleted()` (ver pseudocódigo abaixo) |
|
|
381
|
+
| `time` | Date | — | — | Hora do trigger que produziu este progresso |
|
|
382
|
+
| `rules` | List<ChallengeRuleProgress> | `[]` | — | Detalhe por rule |
|
|
383
|
+
|
|
384
|
+
Cálculo de `percent_completed`:
|
|
385
|
+
|
|
386
|
+
```
|
|
387
|
+
se rules_total == 1 E rules_completed == 0:
|
|
388
|
+
percent_completed = rules[0].percent_completed
|
|
389
|
+
senão:
|
|
390
|
+
percent_completed = (rules_completed/rules_total)*100 (limitado a 100)
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Comportamentos importantes:
|
|
394
|
+
|
|
395
|
+
- A cada `fireAction` que avalia este (player, challenge): primeiro chama `deleteProgress(player, challenge)` para garantir 1 documento por par. **Não há histórico de progresso.**
|
|
396
|
+
- Só é salvo se `percent_completed > 0` OU `challenge.trackFullProgress == true`.
|
|
397
|
+
- Quando o desafio é completado, o progresso é apagado (não preservado como "100%").
|
|
398
|
+
|
|
399
|
+
### 3.4 `ChallengeRuleProgress` — sub-elemento de `ChallengeProgress.rules`
|
|
400
|
+
|
|
401
|
+
| Campo | Tipo | Padrão | Descrição |
|
|
402
|
+
|---|---|---|---|
|
|
403
|
+
| `rule` | String | — | Id da rule (corresponde a `ChallengeRule.id`) |
|
|
404
|
+
| `completed` | boolean | false | Se a rule foi cumprida nesta avaliação |
|
|
405
|
+
| `times_completed` | long | 0 | Quantas vezes a action foi feita |
|
|
406
|
+
| `times_required` | long | 0 | Quantas vezes era necessária |
|
|
407
|
+
| `percent_completed` | double | 0 | `(times_completed/times_required)*100`, limitado a 100. `NaN` → 0 |
|
|
408
|
+
|
|
409
|
+
Método `setUncompletedBecauseOfTime()`: chamado quando rules em ordem (`RANGE_ALL_IN_ORDER`) são quebradas. Zera tudo: `completed=false`, `times_completed=0`, `percent_completed=0`.
|
|
410
|
+
|
|
411
|
+
#### Campos legados comentados (linhas 5-10 e 47-53)
|
|
412
|
+
|
|
413
|
+
```java
|
|
414
|
+
//String rule;
|
|
415
|
+
//long times;
|
|
416
|
+
//long every_times;
|
|
417
|
+
//boolean completed;
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
```java
|
|
421
|
+
//public ChallengeRuleProgress(String rule, long times, long every_times, boolean completed)
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Esses campos da versão antiga (`times`, `every_times`) não existem mais — registros antigos em `challenge_progress` podem ter esses campos no Mongo mas não são lidos pelo código atual.
|
|
425
|
+
|
|
426
|
+
### 3.5 `PlayerStatus` — snapshot (coleção `player_status`)
|
|
427
|
+
|
|
428
|
+
Materialização de leitura de tudo do jogador. **Persistido em `player_status`, não em `player`.**
|
|
429
|
+
|
|
430
|
+
| Campo | Tipo | Descrição |
|
|
431
|
+
|---|---|---|
|
|
432
|
+
| `_id` | String | Mapeado de `player` (id do jogador OU time) |
|
|
433
|
+
| `name` | String | Nome do jogador no momento do snapshot |
|
|
434
|
+
| `image` | Image | Imagem do jogador no momento do snapshot |
|
|
435
|
+
| `total_challenges` | long | `sumTotalRewards(player, TYPE_CHALLENGE)` |
|
|
436
|
+
| `challenges` | Map<String,Long> | itemId → soma; via `getTotalRewardsGroupedByType` |
|
|
437
|
+
| `total_points` | double | `sumTotalPoints(player)` |
|
|
438
|
+
| `point_categories` | Map<String,Double> | categoryId → soma de pontos |
|
|
439
|
+
| `total_catalog_items` | long | `sumTotalRewards(player, TYPE_CATALOG_ITEM)` |
|
|
440
|
+
| `catalog_items` | Map<String,Long> | catalogItemId → quantidade |
|
|
441
|
+
| `level_progress` | LevelProgress | `{level, percent, next_level, next_points, total_levels}` |
|
|
442
|
+
| `challenge_progress` | List<ChallengeProgress> | Cópia dos documentos de `challenge_progress` do jogador |
|
|
443
|
+
| `teams` | List<String> | `Player.teams` |
|
|
444
|
+
| `friends` | List<String> | `Player.friends` |
|
|
445
|
+
| `positions` | List<Leader> | Posições nos leaderboards (via `CrowningManager.findLeadersByPlayerId`) |
|
|
446
|
+
| `time` | Date | Hora do snapshot |
|
|
447
|
+
| `attributes` | Object | Reservado — não populado no código atual (comentários antigos referenciam mas a atribuição está comentada) |
|
|
448
|
+
| `extra` | Map<String,Object> | Copia de `Player.extra` |
|
|
449
|
+
|
|
450
|
+
Comportamento de save em `updatePlayerStatus`:
|
|
451
|
+
|
|
452
|
+
```
|
|
453
|
+
c.remove("{_id:#}", player); // remove antes
|
|
454
|
+
c.save(status); // insere depois
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
**Não é upsert**. Existe uma janela curta entre o `remove` e o `save` em que o documento não existe — leituras concorrentes podem retornar `null`.
|
|
458
|
+
|
|
459
|
+
Quando chamado para um time (`calculateTeamStatus`): retorna o objeto **sem persistir** (não salva em `player_status`) — o método é só de leitura projetada.
|
|
460
|
+
|
|
461
|
+
### 3.6 `GameStatus` — snapshot global (coleção `game_status`)
|
|
462
|
+
|
|
463
|
+
| Campo | Tipo | Descrição |
|
|
464
|
+
|---|---|---|
|
|
465
|
+
| `_id` | String | Mapeado para `api_key` do tenant |
|
|
466
|
+
| `total_players` | long | `db.player_status.count()` |
|
|
467
|
+
| `total_players_by_achievement_item` | Map<String,Long> | Para cada `item` distinto, conta quantos players distintos receberam — via aggregation sobre `achievement` (excluindo player ids que sejam ids de teams) |
|
|
468
|
+
|
|
469
|
+
Comportamento: cache lazy. `findGameStatus()` retorna o documento se existir; senão chama `updateGameStatus(jongo)` que salva. **Não há invalidação automática** — o `game_status` só é atualizado nas chamadas manuais a `updateGameStatus` ou na primeira leitura após `deleteGameStatus`. No `fireAction`, as chamadas a `updateGameStatus` e `deleteGameStatus` estão **comentadas** (linhas 811-812):
|
|
470
|
+
|
|
471
|
+
```java
|
|
472
|
+
//updateGameStatus(jongo);
|
|
473
|
+
//deleteGameStatus(jongo);
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Consequência: **`game_status` fica desatualizado em produção** até alguém invocar `deleteGameStatus` + `findGameStatus` manualmente.
|
|
477
|
+
|
|
478
|
+
### 3.7 `GroupAttribute` / `GroupLog` — DTOs de aggregation interna
|
|
479
|
+
|
|
480
|
+
Não são entidades persistidas — apenas mapeamento de resultados de aggregations.
|
|
481
|
+
|
|
482
|
+
- `GroupAttribute { long total; Date max; }` — usado por `countWithAggregateRegisteredBetweenDatesV3` e `checkActionLogsFrequencyRegisteredBetweenDates`
|
|
483
|
+
- `GroupLog { int total; }` — usado pelo método legado `checkActionLogsFrequencyRegisteredBetweenDatesOLD`
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## 4. Endpoints
|
|
488
|
+
|
|
489
|
+
### 4.1 `GET /v3/achievement`
|
|
490
|
+
|
|
491
|
+
**Listar conquistas paginadas.**
|
|
492
|
+
|
|
493
|
+
| Aspecto | Detalhe |
|
|
494
|
+
|---|---|
|
|
495
|
+
| Finalidade | Buscar achievements filtrados |
|
|
496
|
+
| Autenticação | Bearer token |
|
|
497
|
+
| Paginação | Header `Range: items=0-100` (zero-based, inclusivo) |
|
|
498
|
+
|
|
499
|
+
**Query params:**
|
|
500
|
+
|
|
501
|
+
| Param | Tipo | Descrição |
|
|
502
|
+
|---|---|---|
|
|
503
|
+
| `player` | String | Filtro por jogador. **Valor especial `me`** → resolve para `authBean.getPlayerFromTokenIfExist()` |
|
|
504
|
+
| `type` | int | Filtro por tipo. **ATENÇÃO**: o código aplica filtro apenas se `type > 0` (linha 1246, 1305) — **não é possível filtrar por `type=0` (POINT)** via este endpoint |
|
|
505
|
+
| `item` | String | Filtro por item |
|
|
506
|
+
| `q` | String | Critério MongoDB raw (injetado no `$match`) — vide seção 8 |
|
|
507
|
+
| `fields` | CSV String | Projeção: `field1,field2,...` |
|
|
508
|
+
| `published_min` | String | RFC 3339 OU keyword (`-1d`, `-30m`…). Mapeia para `time >= X` |
|
|
509
|
+
| `published_max` | String | Idem, mapeia para `time <= X` |
|
|
510
|
+
| `orderby` | String | Campo de ordenação (player, total, type, item, time) |
|
|
511
|
+
| `reverse` | bool | true = desc, false = asc |
|
|
512
|
+
| `max_results` | int | Limite (default 100; valores ≤ 0 são tratados como 100) |
|
|
513
|
+
|
|
514
|
+
**Comportamento real:**
|
|
515
|
+
|
|
516
|
+
- Sempre injeta `player: {$exists:true}` no `$match` — documentos sem `player` (corrompidos) nunca aparecem.
|
|
517
|
+
- `q` é concatenado **literalmente** com prefixo `, ` no `$match`: `{ player: {$exists:true}, q-aqui }` — injeção de pipeline MongoDB; vide seção 8.
|
|
518
|
+
- Resposta segue formato `paginatedCallback` (header `Content-Range` + body com lista).
|
|
519
|
+
|
|
520
|
+
**Exemplo:**
|
|
521
|
+
|
|
522
|
+
```
|
|
523
|
+
GET /v3/achievement?type=1&player=me&orderby=time&reverse=true&max_results=10
|
|
524
|
+
Authorization: Bearer eyJhbGciOi...
|
|
525
|
+
Range: items=0-9
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
**Response (200):**
|
|
529
|
+
|
|
530
|
+
```json
|
|
531
|
+
[
|
|
532
|
+
{
|
|
533
|
+
"_id": "62c0a91c8b00",
|
|
534
|
+
"player": "joao@empresa.com",
|
|
535
|
+
"total": 1.0,
|
|
536
|
+
"type": 1,
|
|
537
|
+
"item": "challenge_first_login",
|
|
538
|
+
"time": 1685011053000
|
|
539
|
+
}
|
|
540
|
+
]
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### 4.2 `GET /v3/achievement/frequency`
|
|
544
|
+
|
|
545
|
+
**Conquistas agrupadas por janela de tempo.**
|
|
546
|
+
|
|
547
|
+
| Aspecto | Detalhe |
|
|
548
|
+
|---|---|
|
|
549
|
+
| Finalidade | Curva de frequência de conquistas |
|
|
550
|
+
| Autenticação | Bearer token |
|
|
551
|
+
|
|
552
|
+
**Query params:**
|
|
553
|
+
|
|
554
|
+
| Param | Tipo | Descrição |
|
|
555
|
+
|---|---|---|
|
|
556
|
+
| `player` | String | (suporta `me`) |
|
|
557
|
+
| `type` | int | Filtro de tipo (mesmo bug do `type=0` se aplica) |
|
|
558
|
+
| `item` | String | Filtro de item |
|
|
559
|
+
| `q` | String | Critério Mongo raw |
|
|
560
|
+
| `published_min` / `published_max` | String | RFC 3339 ou keyword |
|
|
561
|
+
| `every` | String | **Obrigatório** se quiser agrupamento. Formato `<n><unit>` onde unit ∈ `y, M, d, h, m, s, w`. Ex: `1d`, `3M` |
|
|
562
|
+
| `time_zone` | String | Timezone IANA (ex: `America/Sao_Paulo`). Aplica offset ao `time` antes de agrupar |
|
|
563
|
+
| `sum_field` | String | Se preenchido, soma este campo em vez de `$sum: 1` |
|
|
564
|
+
|
|
565
|
+
**Comportamento real:**
|
|
566
|
+
|
|
567
|
+
- `every` é parseado: último caractere = unit, restante = amount. Parse pode lançar `NumberFormatException` se o formato for inválido — **não há tratamento** no método.
|
|
568
|
+
- Cada combinação de `everyAmount + everyScale` gera uma estratégia distinta de `$project` + `$group` (vide linhas 3096-3181 com switch de TimeScale).
|
|
569
|
+
- Resposta: lista de HashMaps com `_id` (chave composta de date parts) + `total` (ou `total` somando `sum_field`) + `start`/`end` (min/max do bucket).
|
|
570
|
+
|
|
571
|
+
### 4.3 `POST /v3/achievement/aggregate`
|
|
572
|
+
|
|
573
|
+
**Aggregation pipeline customizado sobre `achievement`.**
|
|
574
|
+
|
|
575
|
+
| Aspecto | Detalhe |
|
|
576
|
+
|---|---|
|
|
577
|
+
| Finalidade | Permitir queries arbitrárias via Mongo Aggregation Framework |
|
|
578
|
+
| Autenticação | Bearer token |
|
|
579
|
+
| Body | `List<String>` — cada string é um estágio JSON do pipeline |
|
|
580
|
+
|
|
581
|
+
**Query params:**
|
|
582
|
+
|
|
583
|
+
| Param | Tipo | Descrição |
|
|
584
|
+
|---|---|---|
|
|
585
|
+
| `player` | String | Filtro |
|
|
586
|
+
| `team` | String | Filtro: resolve para `player: {$in: <playersDoTeam>}` via lookup em `team_player.linkId` |
|
|
587
|
+
| `type` | String | Filtro (parse silencioso — exceção engolida) |
|
|
588
|
+
| `item` | String | Filtro |
|
|
589
|
+
| `q` | String | Critério raw injetado |
|
|
590
|
+
| `published_min` / `published_max` | String | Datas |
|
|
591
|
+
|
|
592
|
+
**Comportamento real:**
|
|
593
|
+
|
|
594
|
+
- Os estágios do body são concatenados **após** o `$match` inicial (que inclui `player: {$exists:true}` + filtros derivados de query params).
|
|
595
|
+
- Bug-bait: o filtro `team` é aplicado **antes** dos estágios do body — se o time não tem jogadores em `team_player`, a query roda **sem o `$in`** (o filtro é omitido quando `players.size() == 0`), retornando dados do tenant inteiro.
|
|
596
|
+
|
|
597
|
+
### 4.4 `POST /v3/achievement`
|
|
598
|
+
|
|
599
|
+
**Criar achievement (ajuste manual).**
|
|
600
|
+
|
|
601
|
+
| Aspecto | Detalhe |
|
|
602
|
+
|---|---|
|
|
603
|
+
| Finalidade | Inserção direta de Achievement (não dispara fireAction) |
|
|
604
|
+
| Autenticação | Bearer token |
|
|
605
|
+
| Persistência | `c.save(achievement)` — upsert por `_id` |
|
|
606
|
+
|
|
607
|
+
**Body:** objeto `Achievement` (campos da seção 3.1).
|
|
608
|
+
|
|
609
|
+
**Comportamento real:**
|
|
610
|
+
|
|
611
|
+
- Se `_id` é null/ausente → gera novo `Guid.newShortGuid()`.
|
|
612
|
+
- Se `time` é null → o construtor de 6 args **não** atribui automaticamente; só a sobrecarga `addAchievement(Achievement)` sem Jongo atribui `new Date()`. **No fluxo do REST**, o caminho usado é o `manager.getAchievementManager().addAchievement(achievement)` (a sobrecarga com 1 arg), portanto `time` ausente vira `now`.
|
|
613
|
+
- **Sempre** chama `playerManager.updateUserStatus(achievement.player)` ao final — isto dispara `evaluateLevelUp` em loop. Inserir um achievement de pontos pode causar level-ups em cascata.
|
|
614
|
+
- **Não dispara** triggers `before_win`/`after_win`.
|
|
615
|
+
- **Não dispara** webhook `achievement_created`.
|
|
616
|
+
- **Não valida** `type` contra os enums conhecidos — aceita qualquer `int`.
|
|
617
|
+
- **Não valida** existência de `item` referenciado.
|
|
618
|
+
|
|
619
|
+
**Exemplo:**
|
|
620
|
+
|
|
621
|
+
```json
|
|
622
|
+
POST /v3/achievement
|
|
623
|
+
Content-Type: application/json
|
|
624
|
+
Authorization: Bearer ...
|
|
625
|
+
{
|
|
626
|
+
"player": "joao@empresa.com",
|
|
627
|
+
"total": 100,
|
|
628
|
+
"type": 0,
|
|
629
|
+
"item": "5fa2c7e1b8c00012345abcde"
|
|
630
|
+
}
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### 4.5 `POST /v3/achievement/bulk`
|
|
634
|
+
|
|
635
|
+
**Inserção em lote.**
|
|
636
|
+
|
|
637
|
+
| Aspecto | Detalhe |
|
|
638
|
+
|---|---|
|
|
639
|
+
| Finalidade | Importação em massa de achievements |
|
|
640
|
+
| Autenticação | Bearer token |
|
|
641
|
+
|
|
642
|
+
**Body:** `List<Achievement>`.
|
|
643
|
+
|
|
644
|
+
**Comportamento real:**
|
|
645
|
+
|
|
646
|
+
- Loop simples: para cada item da lista, chama `addAchievement(item)`.
|
|
647
|
+
- **NÃO chama `updateUserStatus` por item** (`manager.getPlayerManager().updateUserStatus(achievement.player)` está comentado no código, linha 294) — `player_status` fica defasado até o próximo trigger natural.
|
|
648
|
+
- Status response: `201 Created` com `{ total, registered, ignored: 0, content: [...] }`. O campo `ignored` é sempre 0 — o código não tem nenhuma lógica de skip.
|
|
649
|
+
- Sem rollback em caso de falha parcial.
|
|
650
|
+
|
|
651
|
+
### 4.6 `DELETE /v3/achievement/{id}`
|
|
652
|
+
|
|
653
|
+
**Excluir achievement.**
|
|
654
|
+
|
|
655
|
+
| Aspecto | Detalhe |
|
|
656
|
+
|---|---|
|
|
657
|
+
| Finalidade | Remover um achievement individual e recalcular player_status |
|
|
658
|
+
| Autenticação | Bearer token |
|
|
659
|
+
|
|
660
|
+
**Comportamento real:**
|
|
661
|
+
|
|
662
|
+
```
|
|
663
|
+
[1] a = findAchievement(id)
|
|
664
|
+
[2] se a != null:
|
|
665
|
+
deleteAchievement(id)
|
|
666
|
+
updateUserStatus(a.player)
|
|
667
|
+
[3] retorna 204 No Content (sempre, mesmo se não existir)
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
- Resposta 204 mesmo quando o id não existe → cliente não consegue distinguir "deletei" de "não havia nada".
|
|
671
|
+
- Recalcula `player_status` após delete — saldos voltam a refletir o estado corrente.
|
|
672
|
+
- Não dispara triggers `before_delete`/`after_delete` sobre `achievement` (não há entry para essa entidade no fluxo).
|
|
673
|
+
|
|
674
|
+
### 4.7 `GET /v3/achievement/options/{type}`
|
|
675
|
+
|
|
676
|
+
**Listar items disponíveis para um tipo.**
|
|
677
|
+
|
|
678
|
+
Retorna a lista de items que podem ser usados no campo `item` para o `type` informado:
|
|
679
|
+
|
|
680
|
+
| `type` | Origem da lista |
|
|
681
|
+
|---|---|
|
|
682
|
+
| `0` POINT | `PointManager.findAll()` |
|
|
683
|
+
| `1` CHALLENGE | `ChallengeManager.findAll()` |
|
|
684
|
+
| `2` CATALOG_ITEM | `CatalogManager.findAllItems()` |
|
|
685
|
+
| `3` LEVEL | `LevelManager.findAll()` |
|
|
686
|
+
| `4` CROWN | `CrowningManager.findAll()` |
|
|
687
|
+
| `5` LOTTERY | `LotteryManager.findAll()` |
|
|
688
|
+
| `6` MYSTERY_BOX | `MysteryBoxManager.findAll()` |
|
|
689
|
+
| `7` CHARACTER_STAR_STAT | `CharacterStarManager.findAllLevels()` |
|
|
690
|
+
| `8` BONUS | `BonusManager.findAll()` |
|
|
691
|
+
| `50` LOTTERY_TICKET | `LotteryManager.findAll()` (mesma lista que 5) |
|
|
692
|
+
|
|
693
|
+
Tipo desconhecido (incluindo `9 COMPETITION` e `99 CUSTOM`) → retorna lista vazia.
|
|
694
|
+
|
|
695
|
+
### 4.8 `GET /v3/achievement/totalPlayersByAchievement`
|
|
696
|
+
|
|
697
|
+
Retorna `GameStatus` global (cache lazy — vide 3.6).
|
|
698
|
+
|
|
699
|
+
### 4.9 `POST /v3/achievement/evaluate/requirement`
|
|
700
|
+
|
|
701
|
+
**Avaliar se o jogador atende a uma lista de Requirements.**
|
|
702
|
+
|
|
703
|
+
| Aspecto | Detalhe |
|
|
704
|
+
|---|---|
|
|
705
|
+
| Finalidade | Verificar se um jogador tem os recursos exigidos por uma lista de Requirements (pontos, items, etc.) |
|
|
706
|
+
| Body | `List<Requirement>` |
|
|
707
|
+
| Query | `?player=<id>` |
|
|
708
|
+
|
|
709
|
+
**Comportamento real:** chama `evaluateRequirements(Requirement[], player)` — para cada requirement compara `sumTotalRewards(player, type, item, period)` com `r.total`. Se algum falha, retorna `complete: false`.
|
|
17
710
|
|
|
18
711
|
```json
|
|
19
712
|
{
|
|
20
|
-
"
|
|
21
|
-
"player": "
|
|
22
|
-
|
|
713
|
+
"complete": true,
|
|
714
|
+
"player": "joao@empresa.com"
|
|
715
|
+
}
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
### 4.10 Endpoints legados `/2.0.0/achievement`
|
|
719
|
+
|
|
720
|
+
| Endpoint | Comportamento |
|
|
721
|
+
|---|---|
|
|
722
|
+
| `GET /2.0.0/achievement` | Versão sem paginação, com auth via query params `?api_key=...` |
|
|
723
|
+
| `POST /2.0.0/achievement` | Idêntico ao v3 mas exige `app_secret` válido em `securityManager.isAppAllowed(app_secret)`; sem isso retorna 401 |
|
|
724
|
+
|
|
725
|
+
Estes endpoints continuam disponíveis mas usam o caminho legado (sem `Bearer token`). Não recomendados para novas integrações.
|
|
726
|
+
|
|
727
|
+
---
|
|
728
|
+
|
|
729
|
+
## 5. Regras de Negócio
|
|
730
|
+
|
|
731
|
+
Regras presentes no código mas **não no schema** das entidades:
|
|
732
|
+
|
|
733
|
+
### 5.1 `total: 0` é tratado como no-op em vários pontos
|
|
734
|
+
|
|
735
|
+
```
|
|
736
|
+
if (totalPoints != 0) { ... addAchievement(...) ... }
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
Nas etapas de cálculo de pontos via RewardPoint, o lançamento é silenciosamente pulado quando o cálculo resulta em zero. Em `addAchievement(Achievement)` direto, `total=0` **é persistido** sem aviso — o que pode poluir queries de leitura.
|
|
740
|
+
|
|
741
|
+
### 5.2 `LOTTERY_TICKET (50)` nunca vira documento em `achievement`
|
|
742
|
+
|
|
743
|
+
Mesmo quando aparece em `Requirement.type` (recompensa) o código cria documentos em `lottery_ticket` e não em `achievement`. Consultas em `achievement?type=50` sempre retornam vazio em sistemas que não receberam inserts manuais com esse tipo.
|
|
744
|
+
|
|
745
|
+
### 5.3 Propagação de origem é **opt-in** por challenge
|
|
746
|
+
|
|
747
|
+
`challenge.propagateOrigin == true` faz com que **todos** os achievements derivados (pontos, items, deduções) recebam `extra.origin = <id_do_achievement_de_challenge>`. Sem esse flag, achievements descendentes ficam sem rastreabilidade.
|
|
748
|
+
|
|
749
|
+
### 5.4 Recursão time-after-player em `fireAction`
|
|
750
|
+
|
|
751
|
+
Toda `ActionLog` de um player gera uma segunda passagem por `fireAction` por cada time em que ele está. Consequências:
|
|
752
|
+
|
|
753
|
+
- Se um team challenge bater junto com um player challenge, **dois achievements de challenge** são gerados (um para o player, outro para o team).
|
|
754
|
+
- Custo de CPU/IO escala linearmente com o número de teams do jogador.
|
|
755
|
+
- A recursão só acontece **uma vez** — `fireAction` para `team` não recursa novamente (porque `principal.isPlayer()` é falso).
|
|
756
|
+
|
|
757
|
+
### 5.5 Avaliação de level-up roda em **loop**
|
|
758
|
+
|
|
759
|
+
```java
|
|
760
|
+
while (evaluateLevelUp(player, jongo, achievements));
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
Um único `updatePlayerStatus` pode gerar **múltiplos** achievements de TYPE_LEVEL sequencialmente — o jogador "salta" todos os levels que seus pontos cobrem em uma única chamada. Cada level-up dispara triggers e notifica.
|
|
764
|
+
|
|
765
|
+
### 5.6 `evaluateRequirements` usa `Requirement.period` (keyword)
|
|
766
|
+
|
|
767
|
+
Quando o Requirement tem `period` (ex: `-30d-` ou `-1y-`), `sumTotalRewards` é restrito a esse intervalo. Sem `period`, soma desde sempre. Isto permite requirements como "ganhou X pontos nos últimos 30 dias".
|
|
768
|
+
|
|
769
|
+
### 5.7 `evaluateRequirements(multiplier)` para compras de múltiplas unidades
|
|
770
|
+
|
|
771
|
+
Versão com `multiplier` multiplica `r.total` pelo número de items sendo comprados (chamada por `PurchaseManager`).
|
|
772
|
+
|
|
773
|
+
### 5.8 Limites por tipo (`LIMIT_PER_TEAM` é igual a `LIMIT_PER_PLAYER`)
|
|
774
|
+
|
|
775
|
+
```java
|
|
776
|
+
else if(challenge.getLimitPerType() == Challenge.LIMIT_PER_TEAM) {
|
|
777
|
+
total = countChallengeAchievementsByPlayer(action.getUserId(), ...);
|
|
778
|
+
}
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
A query usada para `LIMIT_PER_TEAM` é **idêntica** à de `LIMIT_PER_PLAYER` — usa `action.getUserId()` (que para principal=Team é o teamId, já que `fireAction` chama recursivamente com teamId), portanto funciona por accident: o limite é por team somente porque o usuário no `Achievement` é o teamId nesse caminho. Em código de chamada manual onde se passa player diretamente, o resultado seria errado.
|
|
782
|
+
|
|
783
|
+
### 5.9 Filtros de regra (`buildJsonFilter`) — geo / tipos de atributo
|
|
784
|
+
|
|
785
|
+
Tipos numéricos e booleanos são convertidos para o valor literal (sem aspas) no JSON do filtro **somente** se conseguirem fazer o parse — senão ficam como string. Geolocation usa `$nearSphere` e cria o índice `2dsphere` na coleção `action_log` na primeira utilização (linhas 2103-2105).
|
|
786
|
+
|
|
787
|
+
Operator `NOT_EXIST` gera `$or: [{attributes.X: false, attributes.X: {$exists:false}}]` — atenção que isto **inclui** documentos com o atributo `false`.
|
|
788
|
+
|
|
789
|
+
### 5.10 ChallengeRule.prepared / entity / aggregate (avaliação customizada)
|
|
790
|
+
|
|
791
|
+
Quando uma rule define `prepared` (id de `ChallengeRulePrepared`) ou `entity` + `aggregate` diretamente, a contagem da rule **não** consulta `action_log` — executa o pipeline arbitrário sobre a coleção `entity` informada (pode ser qualquer coleção do tenant). Suporta tokens:
|
|
792
|
+
|
|
793
|
+
- `FUNIFIER_PLAYER_IDS` → JSON array dos ids
|
|
794
|
+
- `FUNIFIER_TRIGGER_TIME` → epoch ms do trigger
|
|
795
|
+
- `FUNIFIER_TRIGGER_ACTION_ID`
|
|
796
|
+
- `FUNIFIER_RULE_ACTION_ID`
|
|
797
|
+
- `FUNIFIER_PERIOD_START_TIME` / `FUNIFIER_PERIOD_END_TIME` (default: últimos 10 anos se não houver período)
|
|
798
|
+
|
|
799
|
+
Pipeline customizado retorna `{ total, max }` — `max` é usado para ordenação em `RANGE_ALL_IN_ORDER`.
|
|
800
|
+
|
|
801
|
+
### 5.11 Multi-tenant
|
|
802
|
+
|
|
803
|
+
Não há campo `apiKey` no documento `achievement`. O isolamento é **inteiramente** dado pelo banco de dados — cada tenant tem sua própria connection ao Jongo (`manager.getJongoConnection()`). Documentos cross-tenant são logicamente impossíveis enquanto o roteamento de connection estiver correto.
|
|
804
|
+
|
|
805
|
+
### 5.12 Ordem temporal em `RANGE_ALL_IN_ORDER`
|
|
806
|
+
|
|
807
|
+
Para considerar o desafio concluído, é preciso que `orderTime[0] <= orderTime[1] <= ... <= orderTime[n-1]`. O algoritmo (linhas 437-451) **não** trata empate (`==`) explicitamente — usa `.after()` que é estrito. Duas actions com o mesmo timestamp podem invalidar a ordem ou não dependendo da ordem de iteração das rules.
|
|
808
|
+
|
|
809
|
+
---
|
|
810
|
+
|
|
811
|
+
## 6. Comportamentos Automáticos
|
|
812
|
+
|
|
813
|
+
| Comportamento | Trigger | Impacto | Persistência |
|
|
814
|
+
|---|---|---|---|
|
|
815
|
+
| Atribuição de `_id` (ShortGuid) | `addAchievement` se `_id` é null | Cria id curto interno | Sim, no doc inserido |
|
|
816
|
+
| Atribuição de `time = new Date()` | `addAchievement(Achievement)` (1 arg) se time é null | Override silencioso | Sim |
|
|
817
|
+
| `evaluateLevelUp` em loop | `updatePlayerStatus` | Múltiplos achievements TYPE_LEVEL em cascata | Sim, um doc por level |
|
|
818
|
+
| Recálculo de `player_status` | Após `POST /v3/achievement`, `DELETE`, fim de `fireAction` (se evaluated), purchase, mystery box | Substitui doc em `player_status` (remove+save) | Sim |
|
|
819
|
+
| Recursão fireAction → team | Quando principal é player e tem times | Avaliação adicional de teamChallenges | Sim (achievements para team) |
|
|
820
|
+
| Trigger `before_win`/`after_win` | `addAchievement` dentro de `fireAction` | Executa scripts customizados | Side effects livres |
|
|
821
|
+
| Webhook `achievement_created` | Fim de `fireAction` com `result.size() > 0` | Notifica consumidores externos | Async (depende do WebhookManager) |
|
|
822
|
+
| Notification `EVENT_WIN` | Action win, Challenge win, Level win | Cria documents em `notification` | Sim |
|
|
823
|
+
| `deleteProgress` antes de salvar progresso | A cada avaliação de challenge | Apaga progresso anterior do par (player, challenge) | Sim |
|
|
824
|
+
| Deduções de requirements (`OPERATION_DEDUCT`) | Challenge completado com `requirements` | Achievements com `total` negativo no MESMO type | Sim |
|
|
825
|
+
| Update `player_status` chamado por challenge.points / challenge.rewards | Dentro do loop de winners em fireAction | Pode ser chamado **N vezes** por challenge (1 por winner por reward), gerando overhead | Sim |
|
|
826
|
+
| Update `game_status` | **Não acontece automaticamente** (chamadas comentadas) | `game_status` fica defasado | Stale by design |
|
|
827
|
+
| Criação de índice `2dsphere` em `action_log` | Primeiro filtro `MAX_DISTANCE_IN_METERS` em qualquer challenge | `action_log` ganha índice global | Sim, persistente |
|
|
828
|
+
|
|
829
|
+
### Fluxo de cascata após challenge completado
|
|
830
|
+
|
|
831
|
+
```mermaid
|
|
832
|
+
flowchart TB
|
|
833
|
+
Done[Challenge completed]
|
|
834
|
+
Done --> P[deleteProgress player+challenge]
|
|
835
|
+
P --> J{join.required?}
|
|
836
|
+
J -- sim --> Jc[completeAll JoinLog]
|
|
837
|
+
J -- não --> Sk[skip join]
|
|
838
|
+
Jc --> A[addAchievement TYPE_CHALLENGE]
|
|
839
|
+
Sk --> A
|
|
840
|
+
A --> Pts[Loop challenge.points → achievement TYPE_POINT por winner]
|
|
841
|
+
Pts --> Rw[Loop challenge.rewards → achievement por type+winner]
|
|
842
|
+
Rw --> Lt[Loop lottery_ticket → insertTicket por unidade]
|
|
843
|
+
Lt --> Nt[Notifications EVENT_WIN]
|
|
844
|
+
Nt --> Dd[deductRequirements → achievements negativos]
|
|
845
|
+
Dd --> Up[updatePlayerStatus por winner]
|
|
846
|
+
Up --> Lev[Loop evaluateLevelUp → achievements TYPE_LEVEL extra]
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
---
|
|
850
|
+
|
|
851
|
+
## 7. Suportado vs NÃO Suportado
|
|
852
|
+
|
|
853
|
+
### ✅ Suportado
|
|
854
|
+
|
|
855
|
+
- CRUD básico via `/v3/achievement` (GET list, POST single, POST bulk, DELETE by id)
|
|
856
|
+
- Aggregation customizado via `POST /v3/achievement/aggregate`
|
|
857
|
+
- Frequência temporal via `GET /v3/achievement/frequency`
|
|
858
|
+
- Filtro por player com valor especial `me` (auto-resolve via token)
|
|
859
|
+
- Filtros de data com keywords (`-1d`, `-30m`, etc.)
|
|
860
|
+
- Tipos enumerados 0..9 + 99 (custom)
|
|
861
|
+
- Recompensas paralelas para todos os membros de um time (`perPlayer: true`)
|
|
862
|
+
- Recompensas aleatórias em catálogo (`item: "FUNIFIER_RANDOM"`) e em loteria
|
|
863
|
+
- Avaliação de challenge com aggregation customizada (`rule.entity` + `rule.aggregate` + tokens FUNIFIER_*)
|
|
864
|
+
- Progressão parcial de challenge (`challenge_progress`)
|
|
865
|
+
- Level-up automático em cascata (múltiplos levels em uma ação)
|
|
866
|
+
- Propagação de origem entre achievements (`challenge.propagateOrigin`)
|
|
867
|
+
- Estorno via lançamento negativo (mesmo type/item, total negativo)
|
|
868
|
+
- Multi-tenant via roteamento de connection por `api_key`
|
|
869
|
+
- Webhook `achievement_created` em todas as gerações dentro de `fireAction`
|
|
870
|
+
- Triggers `before_win` / `after_win` em pontos críticos da avaliação
|
|
871
|
+
|
|
872
|
+
### ❌ NÃO Suportado
|
|
873
|
+
|
|
874
|
+
- **Filtro por `type=0` (POINT) em `GET /v3/achievement`** — o código aplica filtro apenas se `type > 0` (linhas 1246, 1305). Para listar pontos é preciso passar `type=0` via `q` (ex: `q=type:0`).
|
|
875
|
+
- **Atualização (`PUT`)** — não há endpoint de update. Para "corrigir" um achievement é preciso DELETE + POST.
|
|
876
|
+
- **Triggers em `POST /v3/achievement` direto** — apenas o caminho via `fireAction` dispara triggers. Insert manual não.
|
|
877
|
+
- **Webhook em `POST /v3/achievement` direto** — idem.
|
|
878
|
+
- **Atualização incremental de `player_status`** — sempre é DELETE + INSERT no `player_status`.
|
|
879
|
+
- **Atualização automática de `game_status`** — as chamadas a `updateGameStatus`/`deleteGameStatus` em `fireAction` estão comentadas. Stale by design.
|
|
880
|
+
- **Cálculo de `team_status` persistente** — `calculateTeamStatus` retorna em memória, não persiste. Não há materialização equivalente a `player_status` para times.
|
|
881
|
+
- **Atomicidade entre lançamentos relacionados** — `addAchievement` + `updatePlayerStatus` não são transacionais. Crash entre eles deixa estado inconsistente até o próximo `updateUserStatus`.
|
|
882
|
+
- **Rollback de bulk insert** — `POST /v3/achievement/bulk` insere item por item; falha parcial deixa dados parciais persistidos.
|
|
883
|
+
- **`updateUserStatus` no bulk** — chamada está comentada (linha 294); `player_status` defasa após bulk até próximo evento.
|
|
884
|
+
- **Validação de `type`** — qualquer `int` é aceito, inclusive valores fora do enum oficial.
|
|
885
|
+
- **Validação de `item` existir** — não há verificação. `item: "naoexiste"` é persistido como string opaca.
|
|
886
|
+
- **Validação de `player` existir** — não há verificação. Achievements para player inexistente são aceitos e nunca contabilizados em status (porque `updateUserStatus` lê via `findById` que retorna null).
|
|
887
|
+
- **`TYPE_ACTION = 100`** — constante comentada no código fonte (linha 31 de `Achievement.java`). Não existe em runtime.
|
|
888
|
+
- **Triggers `before_create`/`after_create`** sobre a entidade `achievement` — só existe `before_win`/`after_win` para os items relacionados (point_category, challenge, catalog_item, level, etc.).
|
|
889
|
+
- **`TYPE_COMPETITION (9)` em `findOptionsByType`** — apesar do enum existir, o switch não tem case para `TYPE_COMPETITION` — retorna lista vazia.
|
|
890
|
+
- **Filtros geo (`MAX_DISTANCE_IN_METERS`) sobre `achievement`** — só sobre `action_log` via `buildJsonFilter` de challenges.
|
|
891
|
+
- **Distinguir 200 vs 404 em DELETE** — sempre retorna 204.
|
|
892
|
+
- **`evaluateRequirements` levando em conta período no helper público** — apenas a sobrecarga interna usa `Requirement.period`; o público `evaluateRequirements(Requirement[], player)` usa o período igualmente (chama a versão com `Jongo`).
|
|
893
|
+
- **Histórico de `challenge_progress`** — cada avaliação apaga o progresso anterior. Não há trilha de versões.
|
|
894
|
+
- **`game_status` por tenant múltiplo** — `findOne()` sem filtro: assume 1 documento por banco.
|
|
895
|
+
- **Bulk endpoint não suporta async** — diferente do `/v3/database/bulk`, este é sempre síncrono.
|
|
896
|
+
- **`TYPE_LOTTERY_TICKET` registrado como achievement** — apesar da constante existir (50), nunca produz documento em `achievement` (vide 5.2).
|
|
897
|
+
|
|
898
|
+
### Comportamentos confusos / legado relevante
|
|
899
|
+
|
|
900
|
+
- `findValidChallengesOLD` (linhas 2248-2343): query MongoDB gigante deprecada com `when.*` aninhado. **Não é chamada por nenhum caminho ativo**. Mantida para referência.
|
|
901
|
+
- `buildJsonFilterOLD` (linhas 2131-2192): versão anterior do filtro, ainda no arquivo. Não invocada.
|
|
902
|
+
- `checkActionLogsFrequencyRegisteredBetweenDatesOLD` (linha 1701): versão antiga sem `time_zone` e sem `sum_field`. Não chamada.
|
|
903
|
+
- Constantes commentadas indicando refactor incompleto: `COLLECTION_REWARD`, `COLLECTION_PRINCIPAL`, `COLLECTION_ACHIEVEMENT` duplicada (linhas 83-89).
|
|
904
|
+
- `total` é declarado como `double` em `Achievement.total`, mas vários métodos retornam `long` (`sumTotalRewards`, `countChallengeAchievementsByPlayer`) — **perda de precisão decimal** em totais agregados. Apenas `sumTotalPoints` usa double.
|
|
905
|
+
- Vários `e.printStackTrace()` com comentários "//e.printStackTrace();" — erros silenciosamente engolidos (parsing de RewardPoint, conversão de timezone, geolocation index).
|
|
906
|
+
|
|
907
|
+
---
|
|
908
|
+
|
|
909
|
+
## 8. Segurança e Permissões
|
|
910
|
+
|
|
911
|
+
### Autenticação
|
|
912
|
+
|
|
913
|
+
- Endpoints v3 exigem `Authorization: Bearer <token>` resolvido em `AuthBean`.
|
|
914
|
+
- Endpoint legado `/2.0.0/achievement POST` exige `?api_key=<id>&app_secret=<secret>` validado por `securityManager.isAppAllowed(app_secret)`.
|
|
915
|
+
- Não há separação de scopes/permissions no `AchievementRest` v3 — qualquer token autenticado no tenant pode listar, criar, ou deletar achievements.
|
|
916
|
+
|
|
917
|
+
### Isolamento multi-tenant
|
|
918
|
+
|
|
919
|
+
Cada tenant tem sua conexão MongoDB própria via `FrontController.getInstance(apiKey).getManagerFactory().getJongoConnection()`. Não há chave de tenant no documento `achievement` — a separação é puramente física.
|
|
920
|
+
|
|
921
|
+
### Superfícies de injeção MongoDB
|
|
922
|
+
|
|
923
|
+
Múltiplos endpoints concatenam strings diretamente em queries Jongo:
|
|
924
|
+
|
|
925
|
+
| Endpoint / método | Parâmetro vulnerável | Trecho do código |
|
|
926
|
+
|---|---|---|
|
|
927
|
+
| `GET /v3/achievement` (`q`) | `query.append(", " + q);` | linha 1248 / 1307 |
|
|
928
|
+
| `GET /v3/achievement/frequency` (`q`) | idem | linha 3188 |
|
|
929
|
+
| `POST /v3/achievement/aggregate` (`q` + body `aggregations`) | `query.append(", " + q);` e `a.and(aggregations.get(i))` | linhas 2995 / 3023 |
|
|
930
|
+
| `findAllPlayerStatus` (`q`) | idem | linha 884 |
|
|
931
|
+
| `buildJsonFilter` | atributos da rule concatenados com `+ value` | linhas 2043-2113 |
|
|
932
|
+
|
|
933
|
+
O conteúdo de `q` e do array `aggregations` é **interpretado** pelo Jongo / driver Mongo como pipeline. Um cliente autenticado pode:
|
|
934
|
+
|
|
935
|
+
- Injetar `$where` (NoSQL injection clássico) → execução JavaScript no servidor Mongo (se permitido pelo cluster).
|
|
936
|
+
- Disparar `$lookup` para outras coleções → ler dados não relacionados a achievements.
|
|
937
|
+
- Forçar `$out` para criar coleções arbitrárias (mitigado pelo método ser `aggregate` sem cursor de saída, mas dependente da versão do driver).
|
|
938
|
+
|
|
939
|
+
**Não há sanitização ou whitelist** — o sistema de segurança assume que tokens autenticados são confiáveis.
|
|
940
|
+
|
|
941
|
+
### Outras superfícies
|
|
942
|
+
|
|
943
|
+
- **Aggregations em `rule.aggregate`** (challenges customizados): mesmo modelo de risco — usuários com permissão de criar/editar challenges podem cravar pipelines maliciosos que rodam em todas as ações de jogadores.
|
|
944
|
+
- **Reflexivo em `MustacheUtils.parse` + `ExpressionBuilder.evaluate`** (linhas 277-284): o `rule.total` quando String é avaliado como expressão matemática com Mustache. `ExpressionBuilder` só executa aritmética (exp4j), mas Mustache permite acessar propriedades do `player`/`team`/`rule`/`challenge` que vão pro template — risco baixo de injeção arbitrária, alto de DoS via expressões grandes.
|
|
945
|
+
- **`Operator.MAX_DISTANCE_IN_METERS`** cria índice `2dsphere` automaticamente em `action_log` na primeira execução — usuários com permissão de criar challenges podem forçar criação de índices arbitrários.
|
|
946
|
+
|
|
947
|
+
---
|
|
948
|
+
|
|
949
|
+
## 9. Observabilidade e Troubleshooting
|
|
950
|
+
|
|
951
|
+
### Diagnóstico básico
|
|
952
|
+
|
|
953
|
+
| Sintoma | Verificar |
|
|
954
|
+
|---|---|
|
|
955
|
+
| Player ganhou pontos mas saldo errado em `player_status` | `db.achievement.aggregate([{$match:{player:X, type:0}}, {$group:{_id:'$item', total:{$sum:'$total'}}}])` vs `db.player_status.findOne({_id:X}).point_categories` |
|
|
956
|
+
| Challenge não está sendo completado | `db.challenge.findOne({_id:X}).active`, regras + `when`, `team_challenge` vs principal, `requirements`, `join.required` |
|
|
957
|
+
| Level não está subindo | `db.level_config.findOne({_id:"global"}).pointCategory` → `sumTotalRewards(player, 0, <category>)` >= `nextLevel.minPoints` E todos os `requirements` cumpridos? |
|
|
958
|
+
| Progresso não aparece | `db.challenge_progress.find({player:X, challenge:Y})` — só persiste se `percent_completed > 0` OU `trackFullProgress=true` |
|
|
959
|
+
| `game_status` desatualizado | Sempre. `updateGameStatus` está comentado no `fireAction`. Force `db.game_status.drop()` e leia novamente |
|
|
960
|
+
| `player_status` defasado depois de bulk | Esperado. Insira um achievement vazio (total=0) ou chame `GET /v3/player/{id}/status` (rota que dispara updateUserStatus) |
|
|
961
|
+
| Achievements duplicados | Verificar se `deleteProgress` foi chamado; se há recursão team→player com mesmo challenge |
|
|
962
|
+
|
|
963
|
+
### Queries úteis (mongosh)
|
|
964
|
+
|
|
965
|
+
```js
|
|
966
|
+
// Saldo de pontos por categoria do jogador X
|
|
967
|
+
db.achievement.aggregate([
|
|
968
|
+
{ $match: { player: "X", type: 0 } },
|
|
969
|
+
{ $group: { _id: "$item", total: { $sum: "$total" } } }
|
|
970
|
+
])
|
|
971
|
+
|
|
972
|
+
// Lista de achievements de um jogador, mais recentes primeiro
|
|
973
|
+
db.achievement.find({ player: "X" }).sort({ time: -1 }).limit(20)
|
|
974
|
+
|
|
975
|
+
// Total de challenges completos por player no último mês
|
|
976
|
+
db.achievement.aggregate([
|
|
977
|
+
{ $match: { type: 1, time: { $gte: new Date(Date.now()-30*24*3600*1000) } } },
|
|
978
|
+
{ $group: { _id: "$player", total: { $sum: 1 } } },
|
|
979
|
+
{ $sort: { total: -1 } }
|
|
980
|
+
])
|
|
981
|
+
|
|
982
|
+
// Achievements derivados de um challenge específico (via propagateOrigin)
|
|
983
|
+
db.achievement.find({ "extra.origin": "<id_do_achievement_de_challenge>" })
|
|
984
|
+
|
|
985
|
+
// Estornos (totais negativos)
|
|
986
|
+
db.achievement.find({ total: { $lt: 0 } })
|
|
987
|
+
|
|
988
|
+
// ChallengeProgress órfãos (challenge inexistente)
|
|
989
|
+
const cids = db.challenge.distinct("_id");
|
|
990
|
+
db.challenge_progress.find({ challenge: { $nin: cids } })
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
### Comandos HTTP úteis
|
|
994
|
+
|
|
995
|
+
```
|
|
996
|
+
GET /v3/achievement?player=joao@empresa.com&type=1&max_results=50
|
|
997
|
+
GET /v3/achievement/frequency?player=joao@empresa.com&every=1d&published_min=-30d
|
|
998
|
+
POST /v3/achievement/aggregate?published_min=-7d
|
|
999
|
+
Body: ["{$group: {_id: '$player', total: {$sum:'$total'}}}", "{$sort: {total:-1}}", "{$limit: 10}"]
|
|
1000
|
+
GET /v3/achievement/options/3 → lista de Levels
|
|
1001
|
+
GET /v3/achievement/totalPlayersByAchievement
|
|
1002
|
+
DELETE /v3/achievement/<id>
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
### Erros comuns
|
|
1006
|
+
|
|
1007
|
+
| Erro / sintoma | Causa provável |
|
|
1008
|
+
|---|---|
|
|
1009
|
+
| `POST /v3/achievement` retorna 200 mas saldo não muda | `total: 0` foi enviado; ou `player` não existe em `player` collection (achievement é persistido mas `updateUserStatus` no-ops) |
|
|
1010
|
+
| `GET /v3/achievement?type=0` retorna lista cheia (não filtra) | Bug documentado em 4.1 — `type=0` é ignorado. Use `q=type:0` |
|
|
1011
|
+
| `DELETE` retorna 204 mas achievement continua | id errado (case-sensitive); ou foi deletado mas `player_status` ainda tem snapshot antigo se nenhum recálculo foi feito |
|
|
1012
|
+
| `findValidChallenges` lento | Filtro `active:true` + `rules.actionId` é a única parte indexada; `when` é filtrado em memória — challenges com muitos `when` complexos não escalam |
|
|
1013
|
+
| `evaluateLevelUp` loop infinito | Improvável (cada iter aumenta o level corrente); mas se `nextLevel.minPoints == currentLevel.minPoints` e requirements cumpridos, pode laçar |
|
|
1014
|
+
| Pipeline `rule.aggregate` falha | Erro engolido por `try/catch` no for de challenges (linhas 794-797); apenas `e.printStackTrace()` no log da JVM |
|
|
1015
|
+
|
|
1016
|
+
### Logs gerados pelo módulo
|
|
1017
|
+
|
|
1018
|
+
- `System.out.println("Falha ao avaliar desafio " + challenge.getChallenge())` + stack trace (linhas 795-796) — único log explícito em `fireAction`.
|
|
1019
|
+
- `e.printStackTrace()` em conversão de timezone (linha 2222), índice geo (2107, 2181), conversão de pontos (silenciado em alguns casos).
|
|
1020
|
+
- Não há logs estruturados nem instrumentação de métricas no manager.
|
|
1021
|
+
|
|
1022
|
+
---
|
|
1023
|
+
|
|
1024
|
+
## 10. Exemplos Práticos
|
|
1025
|
+
|
|
1026
|
+
### 10.1 Exemplo mínimo — registrar manualmente pontos para um jogador
|
|
1027
|
+
|
|
1028
|
+
```http
|
|
1029
|
+
POST /v3/achievement HTTP/1.1
|
|
1030
|
+
Authorization: Bearer eyJhbGc...
|
|
1031
|
+
Content-Type: application/json
|
|
1032
|
+
|
|
1033
|
+
{
|
|
1034
|
+
"player": "maria@empresa.com",
|
|
1035
|
+
"total": 50,
|
|
23
1036
|
"type": 0,
|
|
24
|
-
"item": "xp"
|
|
25
|
-
|
|
1037
|
+
"item": "xp"
|
|
1038
|
+
}
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
Resultado: insere achievement; `updateUserStatus(maria@empresa.com)` recalcula `player_status`; se pontos somados cruzarem `minPoints` de um Level, dispara `evaluateLevelUp` em loop.
|
|
1042
|
+
|
|
1043
|
+
### 10.2 Exemplo avançado — estorno com rastreio de origem
|
|
1044
|
+
|
|
1045
|
+
Cenário: corrigir cobrança duplicada de catalog_item para um jogador. Estornar mantendo a relação com o item original.
|
|
1046
|
+
|
|
1047
|
+
```http
|
|
1048
|
+
POST /v3/achievement HTTP/1.1
|
|
1049
|
+
Authorization: Bearer eyJhbGc...
|
|
1050
|
+
Content-Type: application/json
|
|
1051
|
+
|
|
1052
|
+
{
|
|
1053
|
+
"player": "maria@empresa.com",
|
|
1054
|
+
"total": -1,
|
|
1055
|
+
"type": 2,
|
|
1056
|
+
"item": "vg_camisa_polo",
|
|
1057
|
+
"time": 1685011053000,
|
|
1058
|
+
"extra": {
|
|
1059
|
+
"origin": "achievement_id_da_compra_original",
|
|
1060
|
+
"reason": "ajuste_manual_atendimento",
|
|
1061
|
+
"operator": "joao@suporte.empresa.com"
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
Comentários sobre o exemplo:
|
|
1067
|
+
|
|
1068
|
+
- `total: -1` decrementa o saldo de catalog items.
|
|
1069
|
+
- `time` explícito mantém a ordenação histórica adequada.
|
|
1070
|
+
- `extra.origin` permite reconstruir a árvore via `db.achievement.find({"extra.origin":"..."})`.
|
|
1071
|
+
- Trigger `before_win`/`after_win` **não dispara** neste fluxo (POST direto).
|
|
1072
|
+
|
|
1073
|
+
### 10.3 Exemplo avançado — bulk import com posterior reconciliação de status
|
|
1074
|
+
|
|
1075
|
+
```http
|
|
1076
|
+
POST /v3/achievement/bulk HTTP/1.1
|
|
1077
|
+
Authorization: Bearer eyJhbGc...
|
|
1078
|
+
Content-Type: application/json
|
|
1079
|
+
|
|
1080
|
+
[
|
|
1081
|
+
{"player":"u1","total":10,"type":0,"item":"xp","time":1685010000000},
|
|
1082
|
+
{"player":"u2","total":20,"type":0,"item":"xp","time":1685010100000},
|
|
1083
|
+
{"player":"u1","total":5,"type":0,"item":"xp","time":1685010200000}
|
|
1084
|
+
]
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
Após o bulk, `player_status` **não** é recalculado. Para forçar:
|
|
1088
|
+
|
|
1089
|
+
```http
|
|
1090
|
+
GET /v3/player/u1/status
|
|
1091
|
+
GET /v3/player/u2/status
|
|
1092
|
+
```
|
|
1093
|
+
|
|
1094
|
+
(esses endpoints invocam `updateUserStatus` internamente).
|
|
1095
|
+
|
|
1096
|
+
### 10.4 Anti-pattern — usar `POST /v3/achievement` para "simular" uma action
|
|
1097
|
+
|
|
1098
|
+
```http
|
|
1099
|
+
POST /v3/achievement HTTP/1.1
|
|
1100
|
+
{
|
|
1101
|
+
"player": "maria@empresa.com",
|
|
1102
|
+
"total": 1,
|
|
1103
|
+
"type": 1,
|
|
1104
|
+
"item": "challenge_pioneirismo"
|
|
26
1105
|
}
|
|
27
1106
|
```
|
|
28
1107
|
|
|
29
|
-
**
|
|
30
|
-
|
|
31
|
-
- `
|
|
32
|
-
-
|
|
33
|
-
-
|
|
1108
|
+
**Por que é errado:**
|
|
1109
|
+
|
|
1110
|
+
- Não dispara `fireAction`. As recompensas (`points`, `rewards`, `notifications`) do challenge **não acontecem**.
|
|
1111
|
+
- Não deduz requirements do challenge.
|
|
1112
|
+
- Não dispara webhook `achievement_created`.
|
|
1113
|
+
- Triggers `before_win`/`after_win` do challenge não rodam.
|
|
1114
|
+
- `progress` órfão pode permanecer (não há `deleteProgress` chamado).
|
|
1115
|
+
|
|
1116
|
+
**Correto:** registrar a action via `POST /v3/action/log` com `actionId` que dispara o challenge. Se o objetivo é forçar a conquista sem ação registrada, use o caminho oficial de "force complete" (não existe atualmente — é uma lacuna documentada). Como workaround, registrar a `action_log` correspondente.
|
|
1117
|
+
|
|
1118
|
+
### 10.5 Anti-pattern — confiar em `game_status` para métricas em tempo real
|
|
1119
|
+
|
|
1120
|
+
```http
|
|
1121
|
+
GET /v3/achievement/totalPlayersByAchievement
|
|
1122
|
+
```
|
|
34
1123
|
|
|
35
|
-
|
|
1124
|
+
**Por que é errado:** `updateGameStatus` está comentado em `fireAction`. O resultado retornado é o último snapshot persistido, que pode ser de horas/dias atrás. Para métricas live, rode aggregations diretas em `achievement` via `POST /v3/achievement/aggregate`.
|
|
36
1125
|
|
|
37
|
-
|
|
38
|
-
**Método:** GET
|
|
39
|
-
**Endpoint:** `/v3/achievement`
|
|
1126
|
+
---
|
|
40
1127
|
|
|
41
|
-
##
|
|
1128
|
+
## Checklist de Configuração
|
|
42
1129
|
|
|
43
|
-
- [ ]
|
|
44
|
-
- [ ]
|
|
45
|
-
- [ ]
|
|
46
|
-
- [ ]
|
|
1130
|
+
- [ ] `point_category` do tenant existe **antes** de gerar achievements TYPE_POINT (`findOptionsByType(0)` retorna a categoria desejada)
|
|
1131
|
+
- [ ] `level_config` `{_id:"global"}` define `pointCategory` correta — caso contrário `evaluateLevelUp` soma **todos** os pontos sem filtro de categoria
|
|
1132
|
+
- [ ] Levels têm `position` e `minPoints` configurados — sem `position` o `findLatestLevelAchieved → nextLevel` quebra
|
|
1133
|
+
- [ ] Challenges com `RANGE_ALL_IN_ORDER` consideram timestamps com resolução suficiente — actions com mesmo `time` podem invalidar a ordem (vide 5.12)
|
|
1134
|
+
- [ ] Challenges com `join.required=true` exigem que o jogador tenha um `JoinLog` aberto **antes** da action — caso contrário `fireAction` ignora (continue) silenciosamente
|
|
1135
|
+
- [ ] Challenges com `rule.entity` + `rule.aggregate` foram revisados — pipelines mal formados são engolidos por try/catch e o desafio nunca completa
|
|
1136
|
+
- [ ] Para teams, configurar `Challenge.teamChallenge=true` E `Challenge.principals` com os times — sem `teamChallenge=true` a recursão player→team avalia mas falha em `evaluatePrincipal`
|
|
1137
|
+
- [ ] **Armadilha**: `GET /v3/achievement?type=0` **não filtra** — passe `q=type:0` para conseguir filtrar achievements de pontos
|
|
1138
|
+
- [ ] **Armadilha**: `POST /v3/achievement/bulk` não atualiza `player_status` — chame `GET /v3/player/{id}/status` ou faça um `POST /v3/achievement` com `total: 0` para forçar
|
|
1139
|
+
- [ ] **Armadilha**: `POST /v3/achievement` direto não dispara triggers nem webhook — se o consumo downstream depende disso, registre via action ou chame manualmente o trigger
|
|
1140
|
+
- [ ] **Armadilha**: `game_status` está stale by design — não usar para painéis em tempo real
|
|
1141
|
+
- [ ] **Armadilha**: campo `extra.origin` só é setado quando `challenge.propagateOrigin=true` — para rastreabilidade, marcar `propagateOrigin=true` desde o cadastro
|
|
1142
|
+
- [ ] Considerar que `total` é declarado como `double` em `Achievement` mas vários cálculos retornam `long` — para items fracionários (créditos em decimais), validar caminho de cada agregação
|
|
1143
|
+
- [ ] **Armadilha**: `Achievement.id` é ShortGuid (não ObjectId) — strings curtas, suficientes apenas para isolamento por tenant; não usar para correlação global multi-tenant
|
|
1144
|
+
- [ ] Removidos em batch (`DELETE` em massa): **não existe endpoint**. Para apagar muitos achievements use `/v3/database` direto com `collection=achievement` (e perde-se `updateUserStatus` automático)
|