funifier-mcp 0.2.25 → 0.2.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/funifier.mdc +38 -41
- package/.github/copilot-instructions.md +38 -41
- package/AGENTS.md +56 -49
- package/README.md +40 -22
- package/datasource-funifier-docs/.coverage.json +326 -0
- package/datasource-funifier-docs/.validation.json +593 -0
- package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
- package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
- package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
- package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
- package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
- package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
- package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
- package/datasource-funifier-docs/knowledge/index.md +5 -2
- 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 +1011 -77
- 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/core/api-client.d.ts +21 -1
- package/dist/core/api-client.d.ts.map +1 -1
- package/dist/core/api-client.js +154 -1
- package/dist/core/api-client.js.map +1 -1
- package/dist/core/constants.d.ts +14 -0
- package/dist/core/constants.d.ts.map +1 -1
- package/dist/core/constants.js +14 -0
- package/dist/core/constants.js.map +1 -1
- package/dist/core/types/Folder.d.ts +16 -0
- package/dist/core/types/Folder.d.ts.map +1 -0
- package/dist/core/types/Folder.js +3 -0
- package/dist/core/types/Folder.js.map +1 -0
- package/dist/core/types/FolderContent.d.ts +10 -0
- package/dist/core/types/FolderContent.d.ts.map +1 -0
- package/dist/core/types/FolderContent.js +3 -0
- package/dist/core/types/FolderContent.js.map +1 -0
- package/dist/core/types/FolderContentType.d.ts +10 -0
- package/dist/core/types/FolderContentType.d.ts.map +1 -0
- package/dist/core/types/FolderContentType.js +3 -0
- package/dist/core/types/FolderContentType.js.map +1 -0
- package/dist/core/types/FolderLog.d.ts +11 -0
- package/dist/core/types/FolderLog.d.ts.map +1 -0
- package/dist/core/types/FolderLog.js +3 -0
- package/dist/core/types/FolderLog.js.map +1 -0
- package/dist/core/types/index.d.ts +4 -0
- package/dist/core/types/index.d.ts.map +1 -1
- package/dist/core/types/index.js +4 -0
- package/dist/core/types/index.js.map +1 -1
- package/dist/mcp/bundle.js +121 -87
- package/dist/mcp/check-update.d.ts +2 -0
- package/dist/mcp/check-update.d.ts.map +1 -0
- package/dist/mcp/check-update.js +44 -0
- package/dist/mcp/check-update.js.map +1 -0
- package/dist/mcp/index.js +5 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/resources/documentation.d.ts +1 -1
- package/dist/mcp/resources/documentation.d.ts.map +1 -1
- package/dist/mcp/resources/documentation.js +39 -3
- package/dist/mcp/resources/documentation.js.map +1 -1
- package/dist/mcp/tools/_char-guard.js +1 -1
- package/dist/mcp/tools/_char-guard.js.map +1 -1
- package/dist/mcp/tools/_fetch-current.d.ts +1 -1
- package/dist/mcp/tools/_fetch-current.d.ts.map +1 -1
- package/dist/mcp/tools/_fetch-current.js +12 -0
- package/dist/mcp/tools/_fetch-current.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 +33 -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 +4 -0
- package/dist/mcp/tools/folder.d.ts.map +1 -0
- package/dist/mcp/tools/folder.js +68 -0
- package/dist/mcp/tools/folder.js.map +1 -0
- 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 +5 -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 +26 -4
- package/dist/mcp/tools/save.js.map +1 -1
- package/dist/mcp/tools/save.test.js +192 -1
- package/dist/mcp/tools/save.test.js.map +1 -1
- package/dist/mcp/tools/search-docs.d.ts +3 -0
- package/dist/mcp/tools/search-docs.d.ts.map +1 -0
- package/dist/mcp/tools/search-docs.js +102 -0
- package/dist/mcp/tools/search-docs.js.map +1 -0
- package/package.json +6 -2
- package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
- package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
- package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
- package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
- package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
- package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
- package/skills/funifier/SKILL.md +88 -0
- package/skills/funifier/references/configure-security.md +96 -0
- package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
- package/skills/funifier/references/create-aggregate.md +144 -0
- package/skills/funifier/references/create-challenge.md +116 -0
- package/skills/funifier/references/create-competition.md +98 -0
- package/skills/funifier/references/create-crossword.md +574 -0
- package/skills/funifier/references/create-custom-object.md +91 -0
- package/skills/funifier/references/create-custom-page.md +135 -0
- package/skills/funifier/references/create-folder.md +104 -0
- package/skills/funifier/references/create-lastmile.md +643 -0
- package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
- package/skills/funifier/references/create-level.md +94 -0
- package/skills/funifier/references/create-lottery.md +913 -0
- package/skills/funifier/references/create-mystery.md +769 -0
- package/skills/funifier/references/create-notification.md +75 -0
- package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
- package/skills/funifier/references/create-quiz.md +98 -0
- package/skills/funifier/references/create-scheduler.md +141 -0
- package/skills/funifier/references/create-story.md +636 -0
- package/skills/funifier/references/create-swap.md +95 -0
- package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
- package/skills/funifier/references/create-virtual-good.md +96 -0
- package/skills/funifier/references/create-webhook.md +72 -0
- package/skills/funifier/references/create-websocket.md +71 -0
- package/skills/funifier/references/create-widget.md +76 -0
- package/skills/funifier/references/debug.md +87 -0
- package/skills/funifier/references/help.md +81 -0
- package/skills/funifier/references/implement-frontend.md +106 -0
- package/skills/funifier/references/import-csv.md +75 -0
- package/skills/funifier/references/manage-player.md +82 -0
- package/skills/funifier/references/manage-team.md +76 -0
- package/skills/funifier/references/upload-file.md +91 -0
- package/datasource-funifier-docs/.search-index.json +0 -17318
- package/datasource-funifier-docs/.skills-map.json +0 -73
- 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,328 +1,1156 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `challenge`
|
|
2
2
|
|
|
3
3
|
**Acesso Studio:** `/studio/challenge`
|
|
4
4
|
**API Endpoint:** `/v3/challenge`
|
|
5
|
+
**Coleção MongoDB:** `challenge`
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
> Documentação produzida por engenharia reversa direta do código-fonte em `funifier-service` (pacote `com.funifier.engine.challenge` e `com.funifier.engine.achievement.AchievementManager`). Onde a documentação contradiz o schema histórico, o **código tem prioridade**.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
---
|
|
9
10
|
|
|
10
|
-
##
|
|
11
|
+
## 1. Visão Geral
|
|
11
12
|
|
|
12
|
-
-
|
|
13
|
-
- Para definir missões e objetivos para jogadores
|
|
14
|
-
- Para vincular ações a recompensas (pontos, itens, etc.)
|
|
15
|
-
- Para criar progressão e senso de conquista
|
|
13
|
+
`challenge` é o módulo de **desafios/missões** da gamificação. Cada documento descreve um objetivo composto por uma ou mais regras (`rules`), cada regra ligada a uma `Action` do sistema. Quando uma `Action` é registrada via `fireAction` (ver `AchievementManager.fireAction`), todos os challenges válidos para aquela action são re-avaliados em tempo real e, ao serem concluídos, geram:
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
- Um `Achievement` do tipo `TYPE_CHALLENGE` (id da conquista guardado em `achievement.item = challenge._id`);
|
|
16
|
+
- Pontuações configuradas em `points`;
|
|
17
|
+
- Recompensas materializadas em `rewards` (catalog items, lottery tickets, challenges, pontos);
|
|
18
|
+
- Notificações em `notifications`;
|
|
19
|
+
- Deduções automáticas via `requirements` com `operation = DEDUCT`.
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
- **Action**: ações devem existir antes de criar challenges que as referenciam
|
|
21
|
+
Papel arquitetural:
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
- O Challenge é a **engrenagem central** da regra de pontuação não-trivial. `Action` registra direto apenas os pontos definidos em `Action.points`; tudo que dependa de contagens, filtros, janelas temporais ou metas só existe em Challenge.
|
|
24
|
+
- Não tem scheduler / worker / job dedicado. A avaliação é **100% reativa**, disparada pelo método `AchievementManager.fireAction` (chamado por `ActionManager.trackSynchonous`).
|
|
25
|
+
- Compartilha o documento de progresso (`challenge_progress`) com o módulo `achievement`.
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
Relação direta com outros módulos:
|
|
28
|
+
|
|
29
|
+
| Módulo | Onde | Como |
|
|
25
30
|
|---|---|---|
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
37
|
-
| `
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
31
|
+
| `action` | `ChallengeRule.actionId` | identifica qual `Action` dispara a avaliação |
|
|
32
|
+
| `achievement` | `fireAction`, `Achievement.TYPE_CHALLENGE` | conquista é registrada na coleção `achievement` |
|
|
33
|
+
| `point` | `RewardPoint.category` | pontuação ao concluir |
|
|
34
|
+
| `catalog` | `Requirement.type = TYPE_CATALOG_ITEM` | recompensas/requisitos de virtual goods |
|
|
35
|
+
| `lottery` | `Requirement.type = TYPE_LOTTERY_TICKET` | tickets de loteria como recompensa |
|
|
36
|
+
| `level` | `Requirement.type = TYPE_LEVEL` | apenas requisito (não é dedutível) |
|
|
37
|
+
| `join` (`join_log`) | `Challenge.join`, `JoinManager.insert` | controla aceite obrigatório do jogador |
|
|
38
|
+
| `trigger` | `EVENT_BEFORE_WIN` / `EVENT_AFTER_WIN` | hooks Groovy disparados ao conceder challenge |
|
|
39
|
+
| `webhook` | `Webhook.EVENT_ACHIEVEMENT_CREATED` | notificação externa de conquistas |
|
|
40
|
+
| `notification` | `NotificationDefinition.EVENT_WIN` | mensagens privadas/newsfeed ao concluir |
|
|
41
|
+
| `principal`/`team` | `Challenge.principals`, `teamChallenge` | filtragem de elegibilidade |
|
|
42
|
+
| `technique` | `GameTechniqueManager` | challenge sem techniques recebe automaticamente `GT35` |
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 2. Arquitetura e Fluxos
|
|
47
|
+
|
|
48
|
+
### 2.1 Classes envolvidas
|
|
49
|
+
|
|
50
|
+
| Classe | Papel |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `com.funifier.rest.v3.rest.ChallengeRest` | Controller REST `/v3/challenge` |
|
|
53
|
+
| `com.funifier.engine.challenge.ChallengeManager` | Orquestra CRUD, validações, geração de descrição |
|
|
54
|
+
| `com.funifier.engine.challenge.ChallengeDaoMongo` | Persistência direta em MongoDB (collection `challenge`) |
|
|
55
|
+
| `com.funifier.engine.challenge.Challenge` | Entidade raiz (documento `challenge`) |
|
|
56
|
+
| `com.funifier.engine.challenge.ChallengeRule` | Regra individual de avaliação |
|
|
57
|
+
| `com.funifier.engine.challenge.ChallengeRuleFilter` | Filtro sobre atributos da action |
|
|
58
|
+
| `com.funifier.engine.challenge.ChallengeRulePrepared` | Aggregation salva (collection `challenge_rule_prepared`) |
|
|
59
|
+
| `com.funifier.engine.challenge.RewardPoint` | Pontos concedidos ao concluir |
|
|
60
|
+
| `com.funifier.engine.challenge.Requirement` | Requisito de entrada **e** recompensa de catálogo/loteria |
|
|
61
|
+
| `com.funifier.engine.achievement.ChallengeProgress` | Snapshot de progresso (collection `challenge_progress`) |
|
|
62
|
+
| `com.funifier.engine.achievement.ChallengeRuleProgress` | Snapshot por regra dentro do progresso |
|
|
63
|
+
| `com.funifier.engine.achievement.AchievementManager` | **Avaliador real** — `fireAction`, `findValidChallenges`, `evaluatePrincipal`, `evaluateLimit`, `isChallengeAvailable` |
|
|
64
|
+
| `com.funifier.engine.challenge.LinkPrincipal` | **Deprecated**, todo uso comentado |
|
|
65
|
+
| `com.funifier.engine.challenge.ChallengeReward` | **Deprecated**, modelo antigo desconectado |
|
|
66
|
+
|
|
67
|
+
### 2.2 Pipeline principal — `fireAction` (avaliação de challenges)
|
|
68
|
+
|
|
69
|
+
Ponto de entrada: `AchievementManager.fireAction(ActionLog trigger)` — invocado por `ActionManager.trackSynchonous`.
|
|
51
70
|
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
```
|
|
72
|
+
[1] findPrincipalByUserOrTeamId(trigger.userId)
|
|
73
|
+
└── se principal é null → retorna lista vazia (sem erro, sem log)
|
|
74
|
+
|
|
75
|
+
[2] ACTION POINTS — apenas se principal.isPlayer():
|
|
76
|
+
├── Lê Action.points[]
|
|
77
|
+
├── Para cada RewardPoint:
|
|
78
|
+
│ OPERATION_NONE → totalPoints = p.total
|
|
79
|
+
│ MULTIPLY_BY_ATTRIBUTE → totalPoints = p.total * trigger.attributes[p.value]
|
|
80
|
+
│ DIVIDE_BY_ATTRIBUTE → totalPoints = p.total / trigger.attributes[p.value]
|
|
81
|
+
├── Cria Achievement(TYPE_POINT, p.category)
|
|
82
|
+
├── TriggerManager.execute(EVENT_BEFORE_WIN) → addAchievement → EVENT_AFTER_WIN
|
|
83
|
+
└── Dispara Notifications da Action (EVENT_WIN)
|
|
84
|
+
|
|
85
|
+
[3] Resolve playerIds:
|
|
86
|
+
├── Se isTeam: ids dos membros + o próprio teamId
|
|
87
|
+
└── Se isPlayer: [trigger.userId]
|
|
88
|
+
|
|
89
|
+
[4] findValidChallenges(trigger, jongo, timeZone):
|
|
90
|
+
├── Query Mongo: { active:true, rules:{$elemMatch:{actionId: trigger.actionId}} }
|
|
91
|
+
├── Itera resultado em memória aplicando challenge.when.isDateAllowed(...)
|
|
92
|
+
└── (timezone usado: Security.timeZone — NÃO o do jogador)
|
|
93
|
+
|
|
94
|
+
[5] evaluatePrincipal(trigger, challenges, principal, jongo):
|
|
95
|
+
Para cada challenge:
|
|
96
|
+
├── XOR(principal.isTeam, challenge.teamChallenge) → reprova
|
|
97
|
+
├── evaluateLimit (conta achievements anteriores deste challenge)
|
|
98
|
+
├── isUserChallengePrincipal (challenge.principals contém player ou um time do player)
|
|
99
|
+
├── evaluateRequirements (sumTotalRewards >= r.total, por período opcional)
|
|
100
|
+
└── evaluateJoin (se join.required=true exige JoinLog não-completado dentro do timeframe)
|
|
101
|
+
|
|
102
|
+
[6] Para cada challenge sobrevivente, avalia REGRAS:
|
|
103
|
+
├── getDefaultJoinTimeFrame(challenge) — janela vinda do JoinLog.start/finish
|
|
104
|
+
├── inOrder = (range==RANGE_ALL_IN_ORDER && rules.length > 1)
|
|
105
|
+
├── Para cada rule[i]:
|
|
106
|
+
│ ├── rule_total = parseTotal(rule.total)
|
|
107
|
+
│ │ - null → 1
|
|
108
|
+
│ │ - Number → intValue
|
|
109
|
+
│ │ - String → ExpressionBuilder(Mustache(rule.total, {player, team, rule, challenge}))
|
|
110
|
+
│ │
|
|
111
|
+
│ ├── filter = buildJsonFilter(rule, trigger, jongo)
|
|
112
|
+
│ ├── type = getRuleType(rule, rule_total)
|
|
113
|
+
│ │ - everyAmount>0 && everyScale!=NONE && timeAmount>0 && timeScale!=NONE → EVERY
|
|
114
|
+
│ │ - rule_total>1 && everyScale==NONE → SEVERAL
|
|
115
|
+
│ │ - default → ONE
|
|
116
|
+
│ ├── between = getRuleTime(rule, trigger.time, timeframe)
|
|
117
|
+
│ │ - timeAmount==0 || timeScale==NONE → timeframe || [null,null]
|
|
118
|
+
│ │ - outOfTime=true → DateUtil.betweenOverThan(...)
|
|
119
|
+
│ │ - outOfTime=false → DateUtil.betweenLessThan(...)
|
|
120
|
+
│ │
|
|
121
|
+
│ ├── Se rule.prepared definido → carrega ChallengeRulePrepared e injeta entity+aggregate
|
|
122
|
+
│ ├── Se rule.entity && rule.aggregate definidos:
|
|
123
|
+
│ │ countWithAggregateRegisteredBetweenDatesV3(...)
|
|
124
|
+
│ │ operator EQUAL/GTE/MOD → ruleDone
|
|
125
|
+
│ ├── senão se ONE:
|
|
126
|
+
│ │ sem filtros, sem attribute → done direto se actionId == trigger.actionId
|
|
127
|
+
│ │ senão countActionLogsRegisteredBetweenDatesV3 (usa trigger.time se única rule da action)
|
|
128
|
+
│ ├── senão se SEVERAL:
|
|
129
|
+
│ │ countActionLogsRegisteredBetweenDatesV3 em [between[0], between[1]]
|
|
130
|
+
│ │ operator EQUAL/GTE/MOD → ruleDone
|
|
131
|
+
│ └── senão se EVERY:
|
|
132
|
+
│ checkActionLogsFrequencyRegisteredBetweenDates
|
|
133
|
+
│ frequency.size() < totalUnitsInPeriod → false
|
|
134
|
+
│ qualquer bucket com total < rule_total → false
|
|
135
|
+
|
|
136
|
+
[7] Avalia conclusão do challenge:
|
|
137
|
+
├── RANGE_ALL → _true == rules.length
|
|
138
|
+
├── RANGE_ALL_IN_ORDER → _true == rules.length && orderTime[i] <= orderTime[i+1] ∀ i
|
|
139
|
+
└── RANGE_ANY → _true > 0
|
|
140
|
+
Se in-order falha, marca rules subsequentes como setUncompletedBecauseOfTime().
|
|
141
|
+
|
|
142
|
+
[8] Se concluído:
|
|
143
|
+
├── deleteProgress(player, challengeId)
|
|
144
|
+
├── Se challenge.join.required:
|
|
145
|
+
│ ├── findNotCompleted → se null, ABORTA este challenge (continue)
|
|
146
|
+
│ └── senão completeAll dos JoinLog abertos
|
|
147
|
+
├── Cria Achievement TYPE_CHALLENGE
|
|
148
|
+
├── TriggerManager.execute(CHALLENGE, EVENT_BEFORE_WIN) → addAchievement → EVENT_AFTER_WIN
|
|
149
|
+
├── REGISTER POINTS (challenge.points):
|
|
150
|
+
│ - mesmo cálculo MULTIPLY/DIVIDE da etapa [2]
|
|
151
|
+
│ - se teamChallenge && p.perPlayer=true → loop por cada membro
|
|
152
|
+
│ - addAchievement TYPE_POINT, updateUserStatus
|
|
153
|
+
│ - se challenge.propagateOrigin → achievement.extra.origin = challengeAchievement.id
|
|
154
|
+
├── REGISTER REWARDS (challenge.rewards):
|
|
155
|
+
│ - Apenas tipos POINT, CATALOG_ITEM, CHALLENGE, LOTTERY_TICKET
|
|
156
|
+
│ - r.item == "FUNIFIER_RANDOM" + CATALOG_ITEM → CatalogManager.findRandom (ou findValidRandom se restrict)
|
|
157
|
+
│ - r.item == "FUNIFIER_RANDOM" + LOTTERY_TICKET → LotteryManager.findRandom + cria N tickets
|
|
158
|
+
│ - LOTTERY_TICKET com r.item específico → cria N LotteryTickets, dispara triggers LOTTERY_TICKET
|
|
159
|
+
│ - perPlayer + teamChallenge → loop por membros
|
|
160
|
+
├── REGISTER NOTIFICATIONS (challenge.notifications EVENT_WIN)
|
|
161
|
+
└── deductRequirements (apenas operation=DEDUCT, tipos POINT/CATALOG_ITEM/CHALLENGE — total negativado)
|
|
162
|
+
|
|
163
|
+
[9] Se NÃO concluído mas (única rule || _true > 0 || trackFullProgress=true):
|
|
164
|
+
├── deleteProgress
|
|
165
|
+
├── progress.calculatePercentCompleted()
|
|
166
|
+
└── Se percent_completed > 0 || trackFullProgress → addProgress
|
|
167
|
+
|
|
168
|
+
[10] updatePlayerStatus(userId) — apenas para principal.isPlayer
|
|
169
|
+
(TODO no código: não há update para team status)
|
|
170
|
+
|
|
171
|
+
[11] Recursão TEAM: se principal.isPlayer e tem teams, chama fireAction novamente
|
|
172
|
+
com ActionLog clonado mas userId = teamId (uma vez por time)
|
|
173
|
+
|
|
174
|
+
[12] WebhookManager.execute(EVENT_ACHIEVEMENT_CREATED, result)
|
|
58
175
|
```
|
|
59
176
|
|
|
60
|
-
|
|
177
|
+
#### Diagrama — Fluxo de avaliação
|
|
178
|
+
|
|
179
|
+
```mermaid
|
|
180
|
+
flowchart LR
|
|
181
|
+
A[ActionLog trigger] --> B[fireAction]
|
|
182
|
+
B --> C{principal == null?}
|
|
183
|
+
C -- sim --> Z[retorna lista vazia]
|
|
184
|
+
C -- não --> D[registra Action.points]
|
|
185
|
+
D --> E[findValidChallenges<br/>active+actionId+when]
|
|
186
|
+
E --> F[evaluatePrincipal<br/>teamChallenge+limit+principals+requirements+join]
|
|
187
|
+
F --> G[loop por rules]
|
|
188
|
+
G --> H{tipo da rule}
|
|
189
|
+
H -- ONE --> H1[countActionLogs]
|
|
190
|
+
H -- SEVERAL --> H2[count + operator]
|
|
191
|
+
H -- EVERY --> H3[checkFrequency]
|
|
192
|
+
H -- aggregate --> H4[countWithAggregate]
|
|
193
|
+
H1 & H2 & H3 & H4 --> I{range satisfeito?}
|
|
194
|
+
I -- não --> J[grava ChallengeProgress se aplicável]
|
|
195
|
+
I -- sim --> K[Achievement TYPE_CHALLENGE]
|
|
196
|
+
K --> L[points + rewards + notifications]
|
|
197
|
+
L --> M[deductRequirements]
|
|
198
|
+
M --> N[updatePlayerStatus]
|
|
199
|
+
N --> O[recursão para teams]
|
|
200
|
+
O --> P[WebhookManager EVENT_ACHIEVEMENT_CREATED]
|
|
201
|
+
```
|
|
61
202
|
|
|
62
|
-
|
|
203
|
+
#### Diagrama — Interação `fireAction` com outros managers
|
|
204
|
+
|
|
205
|
+
```mermaid
|
|
206
|
+
sequenceDiagram
|
|
207
|
+
participant AL as ActionManager
|
|
208
|
+
participant AM as AchievementManager
|
|
209
|
+
participant CM as ChallengeManager
|
|
210
|
+
participant JM as JoinManager
|
|
211
|
+
participant TM as TriggerManager
|
|
212
|
+
participant PM as PlayerManager
|
|
213
|
+
participant WM as WebhookManager
|
|
214
|
+
|
|
215
|
+
AL->>AM: fireAction(trigger)
|
|
216
|
+
AM->>AM: findValidChallenges (Mongo)
|
|
217
|
+
AM->>AM: evaluatePrincipal
|
|
218
|
+
AM->>JM: findNotCompleted (se join.required)
|
|
219
|
+
JM-->>AM: JoinLog ou null
|
|
220
|
+
AM->>AM: avalia rules
|
|
221
|
+
alt Challenge concluído
|
|
222
|
+
AM->>JM: completeAll
|
|
223
|
+
AM->>TM: execute EVENT_BEFORE_WIN
|
|
224
|
+
AM->>AM: addAchievement (TYPE_CHALLENGE)
|
|
225
|
+
AM->>TM: execute EVENT_AFTER_WIN
|
|
226
|
+
AM->>AM: registra points + rewards
|
|
227
|
+
AM->>PM: updateUserStatus
|
|
228
|
+
end
|
|
229
|
+
AM->>WM: execute EVENT_ACHIEVEMENT_CREATED
|
|
230
|
+
AM-->>AL: List<Achievement>
|
|
231
|
+
```
|
|
63
232
|
|
|
64
|
-
|
|
233
|
+
### 2.3 Pipeline — CRUD via `/v3/challenge`
|
|
65
234
|
|
|
66
|
-
|
|
235
|
+
| Pipeline | Etapas |
|
|
236
|
+
|---|---|
|
|
237
|
+
| **POST** (`insert`) | `ChallengeManager.add` → set `created` (se null) → set `updated = now` → `when.compile()` (no-op atualmente) → `mongo.add` (ver 2.4) |
|
|
238
|
+
| **GET por id** | `mongo.findById` → `find({_id:#})` direto |
|
|
239
|
+
| **GET lista** | `manager.findAll(filters)` aggregation `$match` + `$project` + `$sort` + `$limit`; `to=me` substitui pelo token do jogador; se `available=true` + `player`, popula `challenge.available` em memória |
|
|
240
|
+
| **DELETE** | `mongo.remove({_id:#})` — **não** remove achievements/progress associados |
|
|
241
|
+
| **POST `/describe`** | `ChallengeManager.describe` gera texto pt-BR/en em string única |
|
|
242
|
+
| **GET `/evaluate/{id}`** | `evaluatePrincipalAnalyze` (analisa principals, limit, requirements, join — **não conta logs de regras**) |
|
|
243
|
+
| **GET `/prepared`** | lê `challenge_rule_prepared` direto (sem aggregation) |
|
|
67
244
|
|
|
68
|
-
|
|
245
|
+
Não existe método `PUT`. Atualização é feita reusando `POST` com `_id` preenchido (Jongo `save` é upsert).
|
|
69
246
|
|
|
70
|
-
|
|
71
|
-
|
|
247
|
+
### 2.4 Pipeline de persistência (`ChallengeDaoMongo.add`)
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
[1] Se id null ou vazio → Guid.shortTimeMillis()
|
|
251
|
+
└── ⚠ NÃO é UUID — id baseado em timestamp curto
|
|
252
|
+
[2] Se challenge.principals != null && size > 0:
|
|
253
|
+
└── reescreve principals = jongo("principal").distinct("_id").query({_id:{$in:principals}})
|
|
254
|
+
→ silenciosamente REMOVE qualquer id de principal que não exista
|
|
255
|
+
[3] Se rules != null:
|
|
256
|
+
└── remove silenciosamente toda rule sem actionId
|
|
257
|
+
[4] Se requirements != null:
|
|
258
|
+
└── remove silenciosamente toda requirement sem item
|
|
259
|
+
[5] Se rewards != null:
|
|
260
|
+
└── remove silenciosamente toda reward sem item
|
|
261
|
+
[6] jongo("challenge").save(challenge) → upsert por _id
|
|
72
262
|
```
|
|
73
263
|
|
|
74
|
-
|
|
264
|
+
`ChallengeDaoMongo.update`:
|
|
75
265
|
|
|
76
|
-
|
|
266
|
+
```
|
|
267
|
+
[1] jongo("challenge").remove({_id: challenge.id})
|
|
268
|
+
[2] jongo("challenge").save(challenge)
|
|
269
|
+
```
|
|
77
270
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
271
|
+
⚠ **NÃO é atômico**: existe uma janela de tempo em que o documento não está na coleção. Reads concorrentes podem retornar `null`. Esta operação **não é chamada pelo REST** (apenas pelo `manager.update`, que por sua vez não é exposto). Em prática, todo update real passa por `add` → `save` (upsert), que é atômico.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## 3. Estrutura dos Objetos
|
|
276
|
+
|
|
277
|
+
### 3.1 `Challenge` — documento raiz (`challenge`)
|
|
278
|
+
|
|
279
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
280
|
+
|---|---|---|---|---|
|
|
281
|
+
| `_id` | String | `Guid.shortTimeMillis()` | — | id do desafio. Time-based, **não** UUID |
|
|
282
|
+
| `challenge` | String | — | sim (na prática) | nome legível |
|
|
283
|
+
| `description` | String | — | não | descrição livre |
|
|
284
|
+
| `range` | int | `0` | não | `0`=ALL, `1`=ANY, `2`=ALL_IN_ORDER |
|
|
285
|
+
| `active` | boolean | `true` | não | só challenges com `active=true` entram em `findValidChallenges` |
|
|
286
|
+
| `when` | `TimeRestriction` | null | não | janela de validade temporal |
|
|
287
|
+
| `rules` | `ChallengeRule[]` | `[]` | sim | regras de avaliação |
|
|
288
|
+
| `teamChallenge` | boolean | `false` | não | se `true`, só dispara quando o `principal` é Team |
|
|
289
|
+
| `limitTotal` | int | `0` | não | máximo de conquistas (0 = ilimitado) |
|
|
290
|
+
| `limitPerType` | int | `0` | não | `0`=PER_PLAYER, `1`=PER_TEAM, `2`=PER_GAME |
|
|
291
|
+
| `limitTimeAmount` | int | `0` | não | tamanho da janela de limite |
|
|
292
|
+
| `limitTimeScale` | int | `0` | não | unidade da janela (ver Enum TimeScale) |
|
|
293
|
+
| `techniques` | List<String> | `null` | não | códigos GT (auto-preenchido com `GT35`) |
|
|
294
|
+
| `join` | `Join` | null | não | controle de aceite obrigatório |
|
|
295
|
+
| `trackFullProgress` | Boolean | null | não | se `true`, grava progress mesmo com `_true=0` |
|
|
296
|
+
| `propagateOrigin` | Boolean | null | não | se `true`, propaga `origin` em achievements derivados |
|
|
297
|
+
| `badge` | `Image` | null | não | imagem com `small`, `medium`, `original` |
|
|
298
|
+
| `hideUntilEarned` | boolean | `false` | não | (campo só lido — não há filtro automático na listagem) |
|
|
299
|
+
| `points` | `RewardPoint[]` | null | não | pontos concedidos ao concluir |
|
|
300
|
+
| `notifications` | `NotificationDefinition[]` | null | não | notificações por evento |
|
|
301
|
+
| `requirements` | `Requirement[]` | null | não | pré-requisitos + deduções |
|
|
302
|
+
| `tags` | String[] | null | não | tags livres (filtráveis em `findAll`) |
|
|
303
|
+
| `principals` | List<String> | null | não | players/teams elegíveis. **null/empty = todos** |
|
|
304
|
+
| `extra` | Map | `{}` | não | campos livres |
|
|
305
|
+
| `rewards` | List<`Requirement`> | `[]` | não | recompensas materializadas ao concluir |
|
|
306
|
+
| `i18n` | Map<String, Map<String, String>> | `{}` | não | traduções (consumido externamente) |
|
|
307
|
+
| `created` | Date | `now()` no insert | — | preenchido em `add` se null |
|
|
308
|
+
| `updated` | Date | `now()` em todo `add` | — | sobrescrito em todo save |
|
|
309
|
+
|
|
310
|
+
**Campos computados em runtime (não persistidos):**
|
|
311
|
+
|
|
312
|
+
- `available` (Boolean): populado em memória por `AchievementManager.isChallengesAvailable` quando o `GET /v3/challenge` recebe `available=true` + `player=<id>`.
|
|
313
|
+
|
|
314
|
+
**Campos removidos silenciosamente no save:**
|
|
315
|
+
|
|
316
|
+
- Itens de `rules` sem `actionId`
|
|
317
|
+
- Itens de `requirements` sem `item`
|
|
318
|
+
- Itens de `rewards` sem `item`
|
|
319
|
+
- Ids de `principals` que não existam na coleção `principal`
|
|
320
|
+
|
|
321
|
+
**Campos deprecated/legados aceitos mas ignorados (estão comentados na entidade):**
|
|
322
|
+
|
|
323
|
+
- `triggerUrl` — webhook por challenge, removido em 26-jul-2017 (pedido da Wiz)
|
|
324
|
+
- `order` — ordem visual, removida da UI
|
|
325
|
+
- `items` — recompensa de items de catálogo, substituído por `rewards`
|
|
326
|
+
- `levelId` — substituído por `requirements`
|
|
327
|
+
- `pointCategoryId`, `pointAward` — substituído por `points` (v2.0)
|
|
328
|
+
- `notify`, `notifyMessage`, `notifyUsersNow`, `notifyUsersNowMessage` — substituído por `notifications`
|
|
329
|
+
- `badgeIcon` — substituído por `badge` (v2.0)
|
|
330
|
+
- `repeatable`, `activeStartDate`, `activeEndDate` — substituídos por `limitTotal`+`when`
|
|
331
|
+
|
|
332
|
+
Estes campos serão **persistidos** se o cliente enviá-los (Mongo aceita campos extras), mas serão ignorados em qualquer leitura — `Challenge` não tem getters/setters para eles.
|
|
333
|
+
|
|
334
|
+
#### Enums (`Challenge.*`)
|
|
335
|
+
|
|
336
|
+
| Constante | Valor | Significado |
|
|
337
|
+
|---|---|---|
|
|
338
|
+
| `RANGE_ALL` | `0` | Conclui se todas as regras estão completas |
|
|
339
|
+
| `RANGE_ANY` | `1` | Conclui se ao menos uma regra está completa |
|
|
340
|
+
| `RANGE_ALL_IN_ORDER` | `2` | Todas as regras + na ordem cronológica |
|
|
341
|
+
| `LIMIT_PER_PLAYER` | `0` | Limite por jogador |
|
|
342
|
+
| `LIMIT_PER_TEAM` | `1` | Limite por time — **mesmo comportamento que PER_PLAYER** (ver §5) |
|
|
343
|
+
| `LIMIT_PER_GAME` | `2` | Limite global da gamificação |
|
|
344
|
+
|
|
345
|
+
#### Ciclo de vida do progresso
|
|
346
|
+
|
|
347
|
+
```mermaid
|
|
348
|
+
stateDiagram-v2
|
|
349
|
+
[*] --> SemAvaliacao: challenge ativo, jogador elegível
|
|
350
|
+
SemAvaliacao --> EmProgresso: fireAction com _true >= 1\n(ou trackFullProgress=true)
|
|
351
|
+
EmProgresso --> EmProgresso: novo fireAction\n(deleteProgress + addProgress)
|
|
352
|
+
EmProgresso --> Concluido: range satisfeito
|
|
353
|
+
SemAvaliacao --> Concluido: única rule completa
|
|
354
|
+
Concluido --> [*]: Achievement TYPE_CHALLENGE registrado\nChallengeProgress deletado
|
|
355
|
+
Concluido --> SemAvaliacao: limitTotal permite repetir
|
|
356
|
+
```
|
|
83
357
|
|
|
84
|
-
|
|
358
|
+
### 3.2 `ChallengeRule` — regra (`Challenge.rules[]`)
|
|
359
|
+
|
|
360
|
+
| Campo | Tipo | Padrão | Descrição |
|
|
361
|
+
|---|---|---|---|
|
|
362
|
+
| `_id` | String | — | id local da regra (manual ou em cliente) |
|
|
363
|
+
| `actionId` | String | — | **obrigatório** — sem ele a rule é descartada no save |
|
|
364
|
+
| `position` | int | `0` | posição na ordem (relevante p/ `RANGE_ALL_IN_ORDER`) |
|
|
365
|
+
| `operator` | int | `0` (NONE) | comparador para `SEVERAL`/aggregate (`EQUAL`, `GTE`, `MOD`) |
|
|
366
|
+
| `timeAmount` | int | `0` | tamanho da janela temporal |
|
|
367
|
+
| `timeScale` | int | `0` (NONE) | unidade temporal (ver TimeScale) |
|
|
368
|
+
| `outOfTime` | boolean | `false` | `false=TIME_LESS_THAN`, `true=TIME_OVER_THAN` |
|
|
369
|
+
| `everyAmount` | int | `0` | tamanho do bucket de frequência (RULE_TYPE_EVERY) |
|
|
370
|
+
| `everyScale` | int | `0` | unidade do bucket |
|
|
371
|
+
| `filters` | List<`ChallengeRuleFilter`> | null | filtros sobre `action_log.attributes.*` |
|
|
372
|
+
| `attribute` | String | null | atributo para `sum_attributes` (soma valores em vez de contar) |
|
|
373
|
+
| `total` | Object | null | aceita Number ou String com expressão Mustache (v4) |
|
|
374
|
+
| `entity` | String | null | (v5) coleção arbitrária para avaliação via aggregate |
|
|
375
|
+
| `aggregate` | String | null | (v5) pipeline aggregate inline (retorna `{_id, total, max}`) |
|
|
376
|
+
| `prepared` | String | null | (v5) id de `ChallengeRulePrepared` — substitui `entity`+`aggregate` |
|
|
377
|
+
|
|
378
|
+
**Tipos de rule (calculados em runtime — não persistem):**
|
|
379
|
+
|
|
380
|
+
| Tipo | Detecção | Avaliação |
|
|
381
|
+
|---|---|---|
|
|
382
|
+
| `RULE_TYPE_ONE` (0) | Default | Conta logs no intervalo; se sem filtros/attribute e action == trigger, conclui imediatamente |
|
|
383
|
+
| `RULE_TYPE_SEVERAL` (1) | `rule_total > 1` e `everyScale == NONE` | Conta logs; aplica operator EQUAL/GTE/MOD contra `rule_total` |
|
|
384
|
+
| `RULE_TYPE_EVERY` (2) | `everyAmount > 0` + `everyScale != NONE` + `timeAmount > 0` + `timeScale != NONE` | Verifica frequência: cada bucket no período precisa ter ≥ `rule_total` registros |
|
|
85
385
|
|
|
86
|
-
|
|
386
|
+
**Sobre `rule.total`:**
|
|
87
387
|
|
|
88
|
-
|
|
388
|
+
- `null` ou string `"null"` → 1
|
|
389
|
+
- `Number` → `intValue()` (perda de decimais)
|
|
390
|
+
- `String` não vazio → parseado por `MustacheUtils.parse(rule.total, {player, team, rule, challenge})` e avaliado por `ExpressionBuilder` (exemplo: `"{{player.extra.monthly_sales_goal}} + 10"`)
|
|
391
|
+
- ⚠ Se o parse falhar, `ExpressionBuilder` lança RuntimeException — **a exceção é capturada no `try` externo do `fireAction`** e o challenge é silenciosamente ignorado naquele dispatch.
|
|
392
|
+
|
|
393
|
+
### 3.3 `ChallengeRuleFilter` — filtro sobre `action_log.attributes`
|
|
394
|
+
|
|
395
|
+
| Campo | Tipo | Descrição |
|
|
89
396
|
|---|---|---|
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
397
|
+
| `value` | String | valor a comparar. `"{{trigger}}"` é substituído pelo valor do attribute no próprio trigger |
|
|
398
|
+
| `operator` | int | ver enum Operator |
|
|
399
|
+
| `param` | String | nome do atributo em `action_log.attributes.<param>` |
|
|
400
|
+
| `location` | DBObject | usado apenas com `MAX_DISTANCE_IN_METERS` (GeoJSON Point) |
|
|
93
401
|
|
|
94
|
-
|
|
402
|
+
Conversão automática em `buildJsonFilter` baseada em `Action.attribute.type`:
|
|
95
403
|
|
|
96
|
-
|
|
404
|
+
- `TYPE_NUMBER` → valor não-aspeado (numérico)
|
|
405
|
+
- `TYPE_BOOLEAN` → valor não-aspeado
|
|
406
|
+
- demais tipos → string aspeada
|
|
97
407
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
|
101
|
-
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
108
|
-
| `
|
|
109
|
-
| `
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
|
408
|
+
#### Enum Operator
|
|
409
|
+
|
|
410
|
+
| Constante | Valor | Operador Mongo gerado |
|
|
411
|
+
|---|---|---|
|
|
412
|
+
| `NONE` | 0 | (sem filtro) |
|
|
413
|
+
| `EQUAL` | 1 | `<val>` |
|
|
414
|
+
| `LIKE` | 2 | (não implementado em `buildJsonFilter`) |
|
|
415
|
+
| `REGEX` | 3 | `{$regex:<val>}` |
|
|
416
|
+
| `GREATER_THAN` | 4 | `{$gt:<val>}` |
|
|
417
|
+
| `GREATER_THAN_OR_EQUAL` | 5 | `{$gte:<val>}` |
|
|
418
|
+
| `LESS_THAN` | 6 | `{$lt:<val>}` |
|
|
419
|
+
| `LESS_THAN_OR_EQUAL` | 7 | `{$lte:<val>}` |
|
|
420
|
+
| `NOT_EQUAL` | 8 | `{$ne:<val>}` |
|
|
421
|
+
| `EXIST` | 9 | `{$exists:true, $ne:null}` |
|
|
422
|
+
| `NOT_EXIST` | 10 | `$or:[{attr:false, attr:{$exists:false}}]` |
|
|
423
|
+
| `MAX_DISTANCE_IN_METERS` | 20 | `{$nearSphere:{$geometry:<loc>, $maxDistance:<val>}}` + cria índice `2dsphere` |
|
|
424
|
+
| `MOD` | 40 | comparação em código: `total % rule_total == 0` (apenas SEVERAL/aggregate) |
|
|
425
|
+
|
|
426
|
+
⚠ `LIKE` declarado mas não tratado em `buildJsonFilter` — comportamento equivale a "ignorar o filtro" (filtro vazio).
|
|
427
|
+
|
|
428
|
+
### 3.4 `ChallengeRulePrepared` — coleção `challenge_rule_prepared`
|
|
429
|
+
|
|
430
|
+
| Campo | Tipo | Descrição |
|
|
114
431
|
|---|---|---|
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
117
|
-
| `
|
|
432
|
+
| `_id` | String | id manual |
|
|
433
|
+
| `title` | String | rótulo no Studio |
|
|
434
|
+
| `description` | String | descrição livre |
|
|
435
|
+
| `action` | String | qual `Action` esta aggregation se aplica (informativo) |
|
|
436
|
+
| `entity` | String | coleção destino do aggregate (ex: `action_log`) |
|
|
437
|
+
| `aggregate` | String | pipeline JSON que retorna `{_id, total, max}` |
|
|
438
|
+
|
|
439
|
+
Carregado on-demand em `fireAction` quando `rule.prepared` está preenchido. Sobrescreve `rule.entity` e `rule.aggregate` em memória (não persiste).
|
|
440
|
+
|
|
441
|
+
### 3.5 `RewardPoint` — `challenge.points[]`
|
|
442
|
+
|
|
443
|
+
| Campo | Tipo | Padrão | Descrição |
|
|
444
|
+
|---|---|---|---|
|
|
445
|
+
| `total` | double | — | quantidade base de pontos |
|
|
446
|
+
| `category` | String | — | id de `point_category` |
|
|
447
|
+
| `operation` | int | `0` (NONE) | NONE, MULTIPLY_BY_ATTRIBUTE, DIVIDE_BY_ATTRIBUTE |
|
|
448
|
+
| `value` | String | — | nome do atributo da action (usado em MULTIPLY/DIVIDE) |
|
|
449
|
+
| `perPlayer` | boolean | `false` | em teamChallenge, true = distribui para cada membro |
|
|
450
|
+
|
|
451
|
+
| Constante | Valor | Cálculo |
|
|
452
|
+
|---|---|---|
|
|
453
|
+
| `OPERATION_NONE` | 0 | `total` |
|
|
454
|
+
| `OPERATION_MULTIPLY_BY_ATTRIBUTE` | 1 | `total * trigger.attributes[value]` |
|
|
455
|
+
| `OPERATION_DIVIDE_BY_ATTRIBUTE` | 2 | `total / trigger.attributes[value]` |
|
|
118
456
|
|
|
119
|
-
|
|
457
|
+
⚠ Comentário no código: "ATENCAO opcao valida apenas para challenge com 1 rule de 1 action" — porém **não é validado**. Em challenges multi-rule, a operação usa silenciosamente o atributo do `trigger` que fechou o challenge, independentemente de qual rule disparou.
|
|
120
458
|
|
|
121
|
-
### `
|
|
459
|
+
### 3.6 `Requirement` — `challenge.requirements[]` e `challenge.rewards[]`
|
|
122
460
|
|
|
123
|
-
|
|
|
124
|
-
|
|
125
|
-
| `
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
461
|
+
| Campo | Tipo | Descrição |
|
|
462
|
+
|---|---|---|
|
|
463
|
+
| `total` | int | quantidade requisitada ou concedida |
|
|
464
|
+
| `type` | int | tipo do item (ver enum abaixo) |
|
|
465
|
+
| `item` | String | id do item ou `"FUNIFIER_RANDOM"` |
|
|
466
|
+
| `operation` | int | `0`=VERIFY, `1`=DEDUCT (afeta apenas `requirements`; em `rewards` é ignorado) |
|
|
467
|
+
| `extra` | Map | campo livre (usado por `competition`) |
|
|
468
|
+
| `period` | String | expressão de período (ex: `-5h;-0h`) — usado em `evaluateRequirements` |
|
|
469
|
+
| `folder` | String | filtra item aleatório por catálogo/folder de loteria |
|
|
470
|
+
| `restrict` | boolean | se `true`, em FUNIFIER_RANDOM respeita interval/requirements do destino |
|
|
471
|
+
| `perPlayer` | boolean | em teamChallenge, true = distribui para cada membro |
|
|
472
|
+
|
|
473
|
+
#### Enum Requirement.TYPE_*
|
|
474
|
+
|
|
475
|
+
| Constante | Valor | Significado |
|
|
476
|
+
|---|---|---|
|
|
477
|
+
| `TYPE_POINT` | 0 | ponto |
|
|
478
|
+
| `TYPE_CHALLENGE` | 1 | outro challenge (achievement) |
|
|
479
|
+
| `TYPE_CATALOG_ITEM` | 2 | item de catálogo |
|
|
480
|
+
| `TYPE_LEVEL` | 3 | nível (apenas como requisito — não pode ser recompensa nem dedutível) |
|
|
481
|
+
| `TYPE_CROWN` | 4 | crown (apenas como requisito) |
|
|
482
|
+
| `TYPE_LOTTERY` | 5 | sorteio (apenas como requisito) |
|
|
483
|
+
| `TYPE_MYSTERY_BOX` | 6 | mystery box (apenas como requisito) |
|
|
484
|
+
| `TYPE_CHARACTER_STAR_STAT` | 7 | character star stat (apenas como requisito) |
|
|
485
|
+
| `TYPE_LOTTERY_TICKET` | 50 | ticket de loteria (recompensa especial) |
|
|
486
|
+
|
|
487
|
+
**Comportamento real em `rewards`:**
|
|
488
|
+
|
|
489
|
+
- Apenas `POINT`, `CATALOG_ITEM`, `CHALLENGE` e `LOTTERY_TICKET` são processados — outros tipos são **silenciosamente ignorados**.
|
|
490
|
+
- `item == "FUNIFIER_RANDOM"`:
|
|
491
|
+
- `CATALOG_ITEM` → `CatalogManager.findRandom(query)` (ou `findValidRandom` se `restrict=true`)
|
|
492
|
+
- `LOTTERY_TICKET` → `LotteryManager.findRandom(query)` + cria `total` `LotteryTicket`s
|
|
493
|
+
- `total < 0` é **convertido em positivo** internamente (`(r.total < 0) ? -r.total : r.total`).
|
|
494
|
+
|
|
495
|
+
**Comportamento real em `requirements`:**
|
|
496
|
+
|
|
497
|
+
- `evaluateRequirements`: para cada r, calcula `sumTotalRewards(player, type, item, period)`. Se algum < `total`, retorna `false`.
|
|
498
|
+
- `deductRequirements`: aplica apenas se `operation == DEDUCT` E `type ∈ {POINT, CATALOG_ITEM, CHALLENGE}`. Cria Achievement com `total` **negativado**.
|
|
499
|
+
- `period` aceita expressões como `"-5h;-0h"` ou `"month"` (interpretado por `DateUtil.fromKeywordPeriodExpression`).
|
|
500
|
+
|
|
501
|
+
### 3.7 `ChallengeProgress` — coleção `challenge_progress`
|
|
502
|
+
|
|
503
|
+
| Campo | Tipo | Descrição |
|
|
504
|
+
|---|---|---|
|
|
505
|
+
| `_id` | String | `Guid.newShortGuid()` |
|
|
506
|
+
| `player` | String | userId (não teamId) |
|
|
507
|
+
| `challenge` | String | challenge id |
|
|
508
|
+
| `name` | String | snapshot do challenge name |
|
|
509
|
+
| `image` | String | snapshot do `challenge.badge.small.url` |
|
|
510
|
+
| `rules_completed` | int | quantas rules concluídas |
|
|
511
|
+
| `rules_total` | int | total de rules da snapshot |
|
|
512
|
+
| `percent_completed` | double | calculado em `calculatePercentCompleted` |
|
|
513
|
+
| `time` | Date | momento do progresso (= `trigger.time`) |
|
|
514
|
+
| `rules` | List<`ChallengeRuleProgress`> | snapshot por rule |
|
|
515
|
+
|
|
516
|
+
**Algoritmo `calculatePercentCompleted`:**
|
|
517
|
+
|
|
518
|
+
```
|
|
519
|
+
se rules_total == 1 && rules_completed == 0:
|
|
520
|
+
percent_completed = rules[0].percent_completed
|
|
521
|
+
senão:
|
|
522
|
+
percent_completed = min(rules_completed / rules_total, 1) * 100
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
⚠ Operação é **delete+insert** a cada fireAction parcial. Não há update incremental.
|
|
526
|
+
|
|
527
|
+
### 3.8 `ChallengeRuleProgress`
|
|
528
|
+
|
|
529
|
+
| Campo | Tipo | Descrição |
|
|
530
|
+
|---|---|---|
|
|
531
|
+
| `rule` | String | id da rule (rule._id) |
|
|
532
|
+
| `completed` | boolean | rule concluiu? |
|
|
533
|
+
| `times_completed` | long | contagem real |
|
|
534
|
+
| `times_required` | long | meta (rule_total) |
|
|
535
|
+
| `percent_completed` | double | `min(times_completed/times_required, 1)*100`; NaN → 0 |
|
|
536
|
+
|
|
537
|
+
`setUncompletedBecauseOfTime()` zera `completed`, `times_completed` e `percent_completed` — chamado quando `RANGE_ALL_IN_ORDER` falha na ordenação cronológica.
|
|
538
|
+
|
|
539
|
+
### 3.9 `Join` — `challenge.join`
|
|
540
|
+
|
|
541
|
+
| Campo | Tipo | Descrição |
|
|
542
|
+
|---|---|---|
|
|
543
|
+
| `required` | Boolean | se `true`, exige `JoinLog` ativo para o challenge contar |
|
|
544
|
+
| `timeframe` | String | janela temporal aplicada ao join (ex: `-0h;+1h`, `tomorrow`) |
|
|
128
545
|
|
|
129
|
-
|
|
546
|
+
`JoinManager.insert` consome este timeframe ao registrar o aceite.
|
|
130
547
|
|
|
131
|
-
|
|
548
|
+
### 3.10 `NotificationDefinition` — `challenge.notifications[]`
|
|
549
|
+
|
|
550
|
+
Apenas notificações com `event == EVENT_WIN (0)` E `scope != SCOPE_CUSTOM (99)` são disparadas automaticamente ao concluir.
|
|
551
|
+
|
|
552
|
+
| Campo | Tipo | Valores |
|
|
553
|
+
|---|---|---|
|
|
554
|
+
| `event` | int | `0`=WIN, `1`=CREATE, `2`=CHANGE, `3`=LOSE, `4`=REMOVE |
|
|
555
|
+
| `type` | int | `0`=TEXT, `1`=VIDEO |
|
|
556
|
+
| `scope` | int | `0`=PRIVATE, `1`=NEWSFEED, `99`=CUSTOM (não dispara) |
|
|
557
|
+
| `content` | String | conteúdo da notificação |
|
|
558
|
+
| `tag` | String | tag opcional |
|
|
559
|
+
|
|
560
|
+
### 3.11 Técnicas de jogo (`techniques`)
|
|
561
|
+
|
|
562
|
+
Códigos GT (Game Technique) presentes em `GameTechniqueManager`:
|
|
563
|
+
|
|
564
|
+
| Código | Significado | Onde aplica |
|
|
565
|
+
|---|---|---|
|
|
566
|
+
| `GT01` | Points | `point_category` |
|
|
567
|
+
| `GT03` | Leaderboard | `leaderboard` |
|
|
568
|
+
| `GT05` | Blank Fills | `level` (sem techniques) |
|
|
569
|
+
| `GT08` | Virtual Goods | `catalog_item` |
|
|
570
|
+
| `GT26` | Elitism | `level` |
|
|
571
|
+
| **`GT35`** | **Quests / Challenges** | **default para `challenge` sem `techniques`** |
|
|
572
|
+
| `GT53` | Last Mile | `lastmile` |
|
|
573
|
+
| `GT74` | Lottery | `lottery` |
|
|
574
|
+
| `GT85` | Level | `level` |
|
|
575
|
+
|
|
576
|
+
`autoConfigureMissingTechniqueFields()` adiciona `GT35` a todo challenge cujo array `techniques` esteja vazio ou ausente.
|
|
577
|
+
|
|
578
|
+
---
|
|
579
|
+
|
|
580
|
+
## 4. Endpoints
|
|
581
|
+
|
|
582
|
+
Todos exigem `Authorization: Bearer <token>` (escopos validados via `AuthBean`).
|
|
583
|
+
|
|
584
|
+
### `GET /v3/challenge`
|
|
585
|
+
|
|
586
|
+
| Aspecto | Detalhe |
|
|
132
587
|
|---|---|
|
|
133
|
-
|
|
|
134
|
-
| `
|
|
135
|
-
| `
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
|
588
|
+
| Finalidade | Listar / filtrar challenges |
|
|
589
|
+
| Implementação | `ChallengeRest.findAll` → `ChallengeManager.findAll` (Mongo aggregation) |
|
|
590
|
+
| Response | Array de `Challenge` (campos `null` removidos via `JsonUtil.toJsonRemoveNullFields`) |
|
|
591
|
+
|
|
592
|
+
**Query params:**
|
|
593
|
+
|
|
594
|
+
| Param | Tipo | Descrição / Comportamento real |
|
|
595
|
+
|---|---|---|
|
|
596
|
+
| `id` | String | `_id == #` |
|
|
597
|
+
| `challenge` | String | `challenge: { $regex: #, $options: 'i' }` |
|
|
598
|
+
| `tags` | String | csv. Aplica `tags: { $all: [tag1, tag2] }` |
|
|
599
|
+
| `to` | String | filtra elegibilidade para player/team. `"me"` → token. Expande: inclui também os teams do player. Aplica `$or: [{principals:null}, {principals:{$exists:false}}, {principals:[]}, {principals:{$in:[...]}}]` |
|
|
600
|
+
| `player` | String | usado junto com `available` para popular `challenge.available`. `"me"` → token |
|
|
601
|
+
| `available` | String | se igual à string `"true"` E `player` definido → executa `AchievementManager.isChallengesAvailable` em memória |
|
|
602
|
+
| `q` | String | bloco JSON Mongo concatenado direto na query (`, ` + valor literal). **Risco de injeção** (ver §8) |
|
|
603
|
+
| `fields` | String | csv → `$project` MongoDB |
|
|
604
|
+
| `orderby` | String | nome de campo → `$sort` MongoDB |
|
|
605
|
+
| `reverse` | String | `"true"` → ordem desc (`-1`) |
|
|
606
|
+
| `max_results` | String | inteiro; default e fallback `100` |
|
|
607
|
+
|
|
608
|
+
**Comportamento silencioso:**
|
|
609
|
+
|
|
610
|
+
- `max_results <= 0` ou inválido → `100`
|
|
611
|
+
- `reverse` inválido → `false`
|
|
612
|
+
- `q` é inserido **literalmente** no `$match` sem sanitização — ver §8
|
|
613
|
+
|
|
614
|
+
### `GET /v3/challenge/{id}`
|
|
615
|
+
|
|
616
|
+
Retorna `Challenge` ou `null` (HTTP 200) — sem 404 quando não encontrado.
|
|
617
|
+
|
|
618
|
+
### `POST /v3/challenge`
|
|
619
|
+
|
|
620
|
+
| Aspecto | Detalhe |
|
|
145
621
|
|---|---|
|
|
146
|
-
|
|
|
147
|
-
|
|
|
148
|
-
|
|
|
622
|
+
| Finalidade | Cria **ou atualiza** (Jongo `save` é upsert por `_id`) |
|
|
623
|
+
| Status | 201 com o objeto enviado |
|
|
624
|
+
| Sanitização | ver §2.4 — principals inexistentes, rules sem actionId, requirements/rewards sem item são removidos |
|
|
149
625
|
|
|
150
|
-
|
|
626
|
+
⚠ Não há PUT explícito. Para atualizar, envie POST com `_id` preenchido.
|
|
151
627
|
|
|
152
|
-
|
|
628
|
+
**Request mínimo:**
|
|
629
|
+
|
|
630
|
+
```json
|
|
631
|
+
{
|
|
632
|
+
"challenge": "Faça 3 vendas hoje",
|
|
633
|
+
"rules": [{ "actionId": "venda", "total": 3, "operator": 5 }],
|
|
634
|
+
"active": true
|
|
635
|
+
}
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
### `DELETE /v3/challenge/{id}`
|
|
639
|
+
|
|
640
|
+
Status 204. Remove apenas o documento de `challenge` — **não remove** `achievement` (TYPE_CHALLENGE) nem `challenge_progress` associados.
|
|
641
|
+
|
|
642
|
+
### `POST /v3/challenge/describe?language=pt|en`
|
|
643
|
+
|
|
644
|
+
| Aspecto | Detalhe |
|
|
153
645
|
|---|---|
|
|
154
|
-
|
|
|
155
|
-
|
|
|
156
|
-
|
|
|
646
|
+
| Finalidade | Gera descrição humanamente legível do challenge |
|
|
647
|
+
| Body | Objeto `Challenge` (não precisa estar salvo) |
|
|
648
|
+
| Response | `{"description": "..."}` |
|
|
157
649
|
|
|
158
|
-
|
|
650
|
+
⚠ Pelo código (`ChallengeManager.describe`), há um `System.out.println("Language : " + lang)` no caminho — leak de debug em produção.
|
|
651
|
+
⚠ Strings sem tradução cadastrada são impressas em stdout (`System.out.println(novo)`) — comportamento de "harvest" de strings não traduzidas.
|
|
652
|
+
⚠ Erro de digitação: a tradução pt-BR de "semana" mapeia para "mês" (`pt.put("semana", "mês")`). Para `lang=pt*` a palavra "semana" será descrita como "mês".
|
|
159
653
|
|
|
160
|
-
|
|
654
|
+
### `GET /v3/challenge/evaluate/{id}?player=&time=`
|
|
655
|
+
|
|
656
|
+
| Aspecto | Detalhe |
|
|
161
657
|
|---|---|
|
|
162
|
-
|
|
|
163
|
-
|
|
|
164
|
-
| `
|
|
165
|
-
| `
|
|
166
|
-
|
|
|
167
|
-
|
|
168
|
-
|
|
658
|
+
| Finalidade | Diagnóstico: explica por que um player está/não está autorizado para um challenge naquele instante |
|
|
659
|
+
| Não avalia | rules / contagem de logs |
|
|
660
|
+
| Avalia | `time_restriction`, `who` (teamChallenge + principals), `amount_restrictions` (limit), `requirements`, `join` |
|
|
661
|
+
| Param `time` | aceita milliseconds (long), zoned date ISO-8601, keyword (`today`, `yesterday`...). Default = agora |
|
|
662
|
+
| Response | Objeto JSON com diagnóstico estruturado e métricas `*_ms` |
|
|
663
|
+
|
|
664
|
+
**Comportamento real:**
|
|
169
665
|
|
|
170
|
-
|
|
666
|
+
- `principal == null` → retorna `{"status":"UNAUTHORIZED"}`
|
|
667
|
+
- `challenge.when` é avaliado via `isDateAllowedAnalyze` (retorna detalhes do match)
|
|
668
|
+
- Não conta logs de action — não diz "este desafio está progredindo"
|
|
171
669
|
|
|
172
|
-
###
|
|
173
|
-
**Método:** GET
|
|
174
|
-
**Endpoint:** `/v3/challenge`
|
|
670
|
+
### `GET /v3/challenge/prepared`
|
|
175
671
|
|
|
176
|
-
|
|
177
|
-
**Método:** GET
|
|
178
|
-
**Endpoint:** `/v3/challenge/:id`
|
|
672
|
+
Retorna toda a coleção `challenge_rule_prepared` sem filtros.
|
|
179
673
|
|
|
180
|
-
|
|
181
|
-
**Método:** POST
|
|
182
|
-
**Endpoint:** `/v3/challenge`
|
|
674
|
+
---
|
|
183
675
|
|
|
184
|
-
|
|
676
|
+
## 5. Regras de Negócio
|
|
185
677
|
|
|
186
|
-
###
|
|
187
|
-
**Método:** DELETE
|
|
188
|
-
**Endpoint:** `/v3/challenge/:id`
|
|
678
|
+
### 5.1 Validações implícitas (no `add`)
|
|
189
679
|
|
|
190
|
-
|
|
680
|
+
- IDs de `principals` inexistentes na coleção `principal` são **removidos silenciosamente**.
|
|
681
|
+
- Rules sem `actionId`, requirements sem `item`, rewards sem `item` são **removidos silenciosamente**.
|
|
682
|
+
- `_id` é gerado por `Guid.shortTimeMillis()` — **time-based**, não criptograficamente aleatório (id pode colidir em batch). Outros módulos usam `Guid.newShortGuid()` (UUID).
|
|
191
683
|
|
|
192
|
-
###
|
|
684
|
+
### 5.2 Elegibilidade do principal (`isUserChallengePrincipal`)
|
|
685
|
+
|
|
686
|
+
```
|
|
687
|
+
se challenge.principals == null || empty:
|
|
688
|
+
AUTORIZADO (todos)
|
|
689
|
+
senão se principals contém principal.id:
|
|
690
|
+
AUTORIZADO
|
|
691
|
+
senão para cada p em principals:
|
|
692
|
+
se p é Team E principal.userId é membro de p.team:
|
|
693
|
+
AUTORIZADO
|
|
694
|
+
senão:
|
|
695
|
+
NEGADO
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
⚠ A expansão team→members só é feita em **uma direção**: se principals tiver um team, players membros do team passam. Mas se principals tiver um player, players do mesmo team **não** passam.
|
|
699
|
+
|
|
700
|
+
### 5.3 Limite (`evaluateLimit`)
|
|
701
|
+
|
|
702
|
+
```
|
|
703
|
+
se challenge.limitTotal == 0:
|
|
704
|
+
OK
|
|
705
|
+
senão:
|
|
706
|
+
se limitTimeAmount > 0 && limitTimeScale != NONE:
|
|
707
|
+
between = DateUtil.betweenLessThan(now, limitTimeScale, limitTimeAmount)
|
|
708
|
+
senão:
|
|
709
|
+
between = [null, null] ← janela INFINITA (foi removida a janela default de 50 anos)
|
|
710
|
+
|
|
711
|
+
se LIMIT_PER_PLAYER || LIMIT_PER_TEAM:
|
|
712
|
+
total = count Achievement {player: userId, type: CHALLENGE, item: challengeId, time: between}
|
|
713
|
+
senão LIMIT_PER_GAME:
|
|
714
|
+
total = count Achievement {type: CHALLENGE, item: challengeId, time: between}
|
|
715
|
+
|
|
716
|
+
return total < limitTotal
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
⚠ **`LIMIT_PER_TEAM` é idêntico a `LIMIT_PER_PLAYER`** — ambos contam achievements pelo `userId` (não pelo teamId). Bug conhecido herdado.
|
|
720
|
+
|
|
721
|
+
### 5.4 Range e ordem cronológica
|
|
722
|
+
|
|
723
|
+
- `RANGE_ALL_IN_ORDER` só engata se `rules.length > 1`.
|
|
724
|
+
- `orderTime[i]` recebe a maior data do match (para ONE: `trigger.time` se própria action, senão `findLatestActionLogDateRegisteredBetweenDates`; para SEVERAL/EVERY: `findLatestActionLogDateRegisteredBetweenDates`; para aggregate: campo `max` retornado pelo pipeline).
|
|
725
|
+
- Se a ordem falhar, `progress.rules_completed` é truncado em `i` e todas as rules a partir de `i` são marcadas como `setUncompletedBecauseOfTime`.
|
|
726
|
+
|
|
727
|
+
### 5.5 Join obrigatório
|
|
728
|
+
|
|
729
|
+
- `challenge.join.required == true` E sem `JoinLog` aberto dentro do timeframe → `continue` no loop de challenges (challenge é silenciosamente **pulado** sem registrar progress).
|
|
730
|
+
- Quando o challenge conclui com join obrigatório, `JoinLog.completed` é preenchido e o id é guardado em `achievement.extra.join`.
|
|
731
|
+
|
|
732
|
+
### 5.6 Propagação de origem (`propagateOrigin`)
|
|
733
|
+
|
|
734
|
+
- Se `true`, todos os achievements derivados (points, rewards, deductions) recebem `extra.origin = challengeAchievement.id`.
|
|
735
|
+
- LotteryTickets também recebem `extra.origin`.
|
|
736
|
+
|
|
737
|
+
### 5.7 trackFullProgress
|
|
738
|
+
|
|
739
|
+
- Por default, só registra `ChallengeProgress` se ao menos uma rule passou (`_true > 0`) ou se há uma única rule.
|
|
740
|
+
- `trackFullProgress = true` força a gravar progress mesmo com `_true = 0` (útil para dashboards de progresso completo).
|
|
741
|
+
|
|
742
|
+
### 5.8 teamChallenge — fluxo de pontos e recompensas
|
|
743
|
+
|
|
744
|
+
- Pontos/rewards com `perPlayer=false` → registrados **uma vez** no `teamId`.
|
|
745
|
+
- Pontos/rewards com `perPlayer=true` + `teamChallenge=true` → loop por cada membro do time, cria N achievements (um por membro).
|
|
746
|
+
- `updateUserStatus` é chamado para cada `winner` individualmente.
|
|
747
|
+
|
|
748
|
+
### 5.9 Multi-tenant
|
|
749
|
+
|
|
750
|
+
- `ManagerFactory` é resolvido por `authBean.getApiKey()`. Cada tenant tem sua própria connection Mongo via `FrontController.getInstance(apiKey).getManagerFactory()`.
|
|
751
|
+
- Não há cross-tenant em nenhuma query — toda operação roda no banco do tenant.
|
|
752
|
+
|
|
753
|
+
### 5.10 Consistência
|
|
754
|
+
|
|
755
|
+
- `ChallengeDaoMongo.update` é **delete+save** (NÃO atômico). Não é exposto via REST mas é chamado por `ChallengeManager.update`. Em produção, todo update real entra via POST/save (atômico).
|
|
756
|
+
- `addProgress` é precedido por `deleteProgress` — pode haver corrida em fireActions concorrentes do mesmo player (último write wins).
|
|
757
|
+
|
|
758
|
+
---
|
|
759
|
+
|
|
760
|
+
## 6. Comportamentos Automáticos
|
|
761
|
+
|
|
762
|
+
| Comportamento | Trigger | Impacto | Persistência |
|
|
763
|
+
|---|---|---|---|
|
|
764
|
+
| Geração de `_id` | POST sem `_id` | `Guid.shortTimeMillis()` (time-based) | `challenge._id` |
|
|
765
|
+
| Set `created` | POST sem `created` | `new Date()` | `challenge.created` |
|
|
766
|
+
| Set `updated` | Todo POST | `new Date()` | `challenge.updated` (sobrescrito sempre) |
|
|
767
|
+
| Sanitização de principals | POST | Remove ids inexistentes | `challenge.principals` |
|
|
768
|
+
| Sanitização de rules/requirements/rewards | POST | Remove itens com `actionId`/`item` ausente | arrays do challenge |
|
|
769
|
+
| Auto-tagging técnica `GT35` | `GameTechniqueManager.autoConfigureMissingTechniqueFields()` | Adiciona `GT35` a challenges sem techniques | `challenge.techniques` |
|
|
770
|
+
| Avaliação de challenges | `ActionManager.trackSynchonous` → `fireAction` | Avalia todos challenges com `actionId` correspondente | `achievement`, `challenge_progress` |
|
|
771
|
+
| Reset de progresso ao concluir | Challenge concluído | `deleteProgress(player, challengeId)` | remove de `challenge_progress` |
|
|
772
|
+
| Reset de progresso a cada fireAction parcial | Antes de gravar novo progresso | `deleteProgress` + `addProgress` | sobrescreve `challenge_progress` |
|
|
773
|
+
| Conclusão de JoinLog | Challenge concluído com `join.required` | `JoinLog.completed = trigger.time` para todos os joins abertos | `join_log` |
|
|
774
|
+
| Trigger Groovy BEFORE_WIN/AFTER_WIN | Challenge concluído | Executa scripts cadastrados no módulo `trigger` | side-effects definidos no trigger |
|
|
775
|
+
| Notification dispatch | Challenge concluído com `notifications[].event=WIN` | Envia para `notification` (private/newsfeed) | `notification` |
|
|
776
|
+
| Webhook dispatch | Ao final de `fireAction` com `result.size() > 0` | Dispara `EVENT_ACHIEVEMENT_CREATED` | externo |
|
|
777
|
+
| Recursão para teams | `principal.isPlayer` com teams | Re-chama `fireAction` com `userId = teamId` | challenge avaliações em cascata |
|
|
778
|
+
| Deduction silenciosa | Challenge concluído com `requirements[].operation=DEDUCT` | Cria Achievement negativo (apenas POINT/CATALOG_ITEM/CHALLENGE) | `achievement` |
|
|
779
|
+
|
|
780
|
+
### Diagrama — Cascata de side-effects ao concluir
|
|
781
|
+
|
|
782
|
+
```mermaid
|
|
783
|
+
flowchart TD
|
|
784
|
+
A[Challenge concluído] --> B[deleteProgress]
|
|
785
|
+
B --> C{join.required?}
|
|
786
|
+
C -- sim --> D[completeAll JoinLog]
|
|
787
|
+
C -- não --> E[Achievement TYPE_CHALLENGE]
|
|
788
|
+
D --> E
|
|
789
|
+
E --> F[Trigger BEFORE_WIN/AFTER_WIN]
|
|
790
|
+
F --> G[Loop RewardPoints]
|
|
791
|
+
G --> H{teamChallenge && perPlayer?}
|
|
792
|
+
H -- sim --> H1[Achievement TYPE_POINT por membro]
|
|
793
|
+
H -- não --> H2[Achievement TYPE_POINT no userId]
|
|
794
|
+
H1 & H2 --> I[Loop Rewards]
|
|
795
|
+
I --> J{type?}
|
|
796
|
+
J -- POINT --> J1[Achievement TYPE_POINT]
|
|
797
|
+
J -- CATALOG_ITEM --> J2[FUNIFIER_RANDOM? findRandom : Achievement]
|
|
798
|
+
J -- LOTTERY_TICKET --> J3[N LotteryTickets + triggers]
|
|
799
|
+
J -- CHALLENGE --> J4[Achievement TYPE_CHALLENGE]
|
|
800
|
+
J1 & J2 & J3 & J4 --> K[Notifications EVENT_WIN]
|
|
801
|
+
K --> L[deductRequirements]
|
|
802
|
+
L --> M[updatePlayerStatus]
|
|
803
|
+
M --> N[Recursão fireAction para teams]
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
---
|
|
807
|
+
|
|
808
|
+
## 7. Suportado vs NÃO Suportado
|
|
809
|
+
|
|
810
|
+
### ✅ Suportado
|
|
811
|
+
|
|
812
|
+
- CRUD via REST (`GET`, `POST`, `DELETE`).
|
|
813
|
+
- Filtragem por id, nome (regex case-insensitive), tags (AND), elegibilidade por player/team, Mongo query livre (`q`), projeção, ordenação, paginação.
|
|
814
|
+
- Avaliação reativa a partir de `Action` (síncrona, dentro do `fireAction`).
|
|
815
|
+
- Range `ALL`, `ANY`, `ALL_IN_ORDER`.
|
|
816
|
+
- 3 tipos de rule: `ONE`, `SEVERAL`, `EVERY` + aggregate inline/`prepared`.
|
|
817
|
+
- 11 operadores em filtros (NONE/EQUAL/REGEX/GT/GTE/LT/LTE/NE/EXIST/NOT_EXIST/MAX_DISTANCE/MOD).
|
|
818
|
+
- Pontuação com `MULTIPLY_BY_ATTRIBUTE` / `DIVIDE_BY_ATTRIBUTE`.
|
|
819
|
+
- Recompensas: pontos, catalog items, lottery tickets, challenges em cascata.
|
|
820
|
+
- `FUNIFIER_RANDOM` em catalog items e lottery tickets, com filtro por `folder`.
|
|
821
|
+
- Limite por player / game (per-team é alias de per-player).
|
|
822
|
+
- TimeRestriction com `repeat`, `frequency` (weekly/monthly), `allDay`, `weeklyRepeat`, `startDate/endDate/startTime/endTime/startDateTime/endDateTime`.
|
|
823
|
+
- Join obrigatório com `timeframe` (incluindo expressões como `-0h;+1h`).
|
|
824
|
+
- Multi-rule com ordenação cronológica.
|
|
825
|
+
- Mustache + ExpressionBuilder em `rule.total` (`{{player.extra.field}} * 2`).
|
|
826
|
+
- Internacionalização em `i18n` (campo livre — consumido por clientes).
|
|
827
|
+
- TeamChallenge com expansão para membros.
|
|
828
|
+
- Avaliação diagnóstica via `/evaluate/{id}` (sem contar logs).
|
|
829
|
+
- Geração de descrição multilíngue via `/describe`.
|
|
830
|
+
- Triggers `before_win` / `after_win`.
|
|
831
|
+
- Webhooks via `EVENT_ACHIEVEMENT_CREATED`.
|
|
832
|
+
|
|
833
|
+
### ❌ NÃO Suportado
|
|
834
|
+
|
|
835
|
+
- **Sem endpoint PUT explícito**. Updates devem reusar POST com `_id` (a UI Studio faz isto). Sem versionamento ou control de concorrência.
|
|
836
|
+
- **Sem cleanup em cascata**: `DELETE /v3/challenge/{id}` deixa `achievement` (`TYPE_CHALLENGE`) e `challenge_progress` órfãos.
|
|
837
|
+
- **`LIMIT_PER_TEAM` é alias de `LIMIT_PER_PLAYER`** — sempre conta pelo `userId`. Bug não corrigido.
|
|
838
|
+
- **`Operator.LIKE` (2)** declarado mas não tratado em `buildJsonFilter` — filtro é silenciosamente vazio.
|
|
839
|
+
- **Tradução pt-BR de "semana" mapeia para "mês"** — erro em `ChallengeManager.describe`.
|
|
840
|
+
- **`describe` imprime stdout** em produção (`System.out.println("Language : " + lang)` e linhas com strings sem tradução).
|
|
841
|
+
- **`MULTIPLY_BY_ATTRIBUTE` / `DIVIDE_BY_ATTRIBUTE` não são validados**: comentário diz "apenas para challenge com 1 rule de 1 action", mas qualquer combinação é aceita; em multi-rule usa o `trigger` que fechou (não-determinístico).
|
|
842
|
+
- **`ChallengeReward`** declarado e completo, mas **desconectado**: nenhum CRUD/manager o referencia.
|
|
843
|
+
- **`LinkPrincipal`** deprecated — substituído por `Challenge.principals`. Métodos relacionados estão comentados.
|
|
844
|
+
- **Campos schema-only** (não persistem getters/setters): `triggerUrl`, `order`, `items`, `levelId`, `pointCategoryId`, `pointAward`, `notify`, `notifyMessage`, `notifyUsersNow`, `notifyUsersNowMessage`, `badgeIcon`, `repeatable`, `activeStartDate`, `activeEndDate`. Se enviados via POST, são gravados no Mongo mas ignorados em qualquer leitura tipada.
|
|
845
|
+
- **`TimeRestriction.compile()`** é no-op (corpo comentado) — chamado por `ChallengeManager.add` por compatibilidade histórica.
|
|
846
|
+
- **`evaluatePrincipalAnalyze` não simula a avaliação de rules** — `/evaluate/{id}` não pode ser usado para prever se uma action específica concluiria o desafio.
|
|
847
|
+
- **`evaluateLimit` com `limitTimeAmount==0`** usa janela `[null, null]` — significa **contagem global histórica**, sem corte temporal.
|
|
848
|
+
- **Notificações com `scope == SCOPE_CUSTOM (99)`** são silenciosamente filtradas em `getNotificationsByEvent`.
|
|
849
|
+
- **Sem job/scheduler para expirar challenges**: `challenge.when.endDateTime` no passado simplesmente faz `findValidChallenges` retornar o array vazio — não há limpeza de progress órfão.
|
|
850
|
+
- **`recursão para teams` cria 1 fireAction por team a cada action do player**: se o player está em N times, são N+1 avaliações por dispatch (custo O(N) silencioso).
|
|
851
|
+
- **`hideUntilEarned`** é lido (`isHideUntilEarned`) mas **nenhum endpoint REST aplica esse filtro**. Cliente precisa filtrar localmente.
|
|
852
|
+
- **`Operator.MOD`** funciona apenas em `RULE_TYPE_SEVERAL` e em ramos com aggregate. Em ONE e EVERY o operador é ignorado.
|
|
853
|
+
- **Sem agregação de progress entre rules** quando `RANGE_ANY`: a primeira rule completa fecha o challenge; demais rules não são avaliadas para fins de progresso.
|
|
854
|
+
|
|
855
|
+
---
|
|
856
|
+
|
|
857
|
+
## 8. Segurança e Permissões
|
|
858
|
+
|
|
859
|
+
### 8.1 Autenticação
|
|
860
|
+
|
|
861
|
+
- Todo endpoint depende de `@BeanParam AuthBean` (`Authorization: Bearer <jwt>`).
|
|
862
|
+
- O `apiKey` extraído do bean resolve o `ManagerFactory` correto → **isolamento por tenant garantido no nível de conexão Mongo**.
|
|
863
|
+
|
|
864
|
+
### 8.2 Autorização
|
|
865
|
+
|
|
866
|
+
- Não há escopo específico para `challenge` — qualquer usuário com token válido pode CRUD challenges. A separação esperada é via roles no nível do gateway/frontend (Studio).
|
|
867
|
+
|
|
868
|
+
### 8.3 Injeção de Mongo via `q`
|
|
869
|
+
|
|
870
|
+
```java
|
|
871
|
+
if(q != null && q.trim().length() > 0) { query.append(", " + q); }
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
O parâmetro `q` é **concatenado literalmente** dentro do `$match` da aggregation, sem sanitização nem parsing. Embora rode no contexto do tenant (`apiKey`), permite:
|
|
875
|
+
|
|
876
|
+
- Acesso a campos que o cliente não deveria conhecer (ex: filtrar por `extra.internal_flag`).
|
|
877
|
+
- Construção de operadores `$where` com JavaScript arbitrário (Mongo legacy aceita) — caminho de execução remota em servidores Mongo não-hardenizados.
|
|
878
|
+
- DoS via aggregations pesadas (`$elemMatch` aninhados).
|
|
879
|
+
|
|
880
|
+
**Mitigação esperada**: o gateway/Studio deve restringir uso de `q` a roles administrativas.
|
|
881
|
+
|
|
882
|
+
### 8.4 Isolamento por organização
|
|
883
|
+
|
|
884
|
+
- Tudo opera no `Jongo` do tenant. Sem chamadas cross-tenant em nenhum método.
|
|
885
|
+
- IDs de challenge são únicos apenas dentro do tenant — o mesmo `_id` pode existir em tenants diferentes.
|
|
886
|
+
|
|
887
|
+
### 8.5 Outras superfícies
|
|
888
|
+
|
|
889
|
+
- `Action.attributes` é embutida no filtro Mongo via `buildJsonFilter`. Atributos do tipo `STRING` são aspeados; `NUMBER`/`BOOLEAN` são embutidos crus — se um atributo for declarado como `NUMBER` mas o valor for malformado, o build pode produzir JSON inválido (queda silenciosa do challenge no try/catch externo).
|
|
890
|
+
- `rule.total` aceita string Mustache + ExpressionBuilder. ExpressionBuilder não executa código arbitrário (apenas operadores matemáticos), mas Mustache resolve referências em `player.*`, `team.*`, `rule.*`, `challenge.*` — qualquer campo destes objetos é acessível.
|
|
891
|
+
|
|
892
|
+
### 8.6 Comportamentos inseguros documentados
|
|
893
|
+
|
|
894
|
+
- `q` sem sanitização (§8.3).
|
|
895
|
+
- `update()` não-atômico no DAO (race condition).
|
|
896
|
+
- `_id` time-based (`Guid.shortTimeMillis`) — não criptograficamente aleatório.
|
|
897
|
+
- `describe` imprime no stdout em produção.
|
|
898
|
+
|
|
899
|
+
---
|
|
900
|
+
|
|
901
|
+
## 9. Observabilidade e Troubleshooting
|
|
902
|
+
|
|
903
|
+
### 9.1 Diagnóstico de challenge
|
|
904
|
+
|
|
905
|
+
```
|
|
906
|
+
GET /v3/challenge/<id>
|
|
907
|
+
GET /v3/challenge/evaluate/<id>?player=<userId>&time=<iso8601_or_keyword_or_millis>
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
O endpoint `/evaluate` retorna métricas `*_ms` por etapa e o motivo de UNAUTHORIZED para cada bloco (`who`, `amount_restrictions`, `requirements`, `join`).
|
|
911
|
+
|
|
912
|
+
### 9.2 Inspeção de progresso
|
|
913
|
+
|
|
914
|
+
```js
|
|
915
|
+
// MongoDB
|
|
916
|
+
db.challenge_progress.find({ player: "<userId>" })
|
|
917
|
+
db.challenge_progress.find({ challenge: "<challengeId>" })
|
|
918
|
+
db.achievement.find({ player: "<userId>", type: 1, item: "<challengeId>" }) // TYPE_CHALLENGE = 1
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
### 9.3 Verificar quais challenges entram para uma action
|
|
922
|
+
|
|
923
|
+
```js
|
|
924
|
+
db.challenge.find(
|
|
925
|
+
{ active: true, rules: { $elemMatch: { actionId: "<actionId>" } } },
|
|
926
|
+
{ _id: 1, challenge: 1 }
|
|
927
|
+
)
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
(esta é exatamente a query usada em `findValidChallenges` antes do filtro de `when` em memória)
|
|
931
|
+
|
|
932
|
+
### 9.4 Verificar JoinLogs abertos
|
|
933
|
+
|
|
934
|
+
```js
|
|
935
|
+
db.join_log.find({
|
|
936
|
+
player: "<userId>",
|
|
937
|
+
entity: "challenge",
|
|
938
|
+
item: "<challengeId>",
|
|
939
|
+
completed: { $exists: false }
|
|
940
|
+
})
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
### 9.5 Erros comuns e causas
|
|
944
|
+
|
|
945
|
+
| Sintoma | Causa provável |
|
|
946
|
+
|---|---|
|
|
947
|
+
| Challenge nunca conclui | Verificar `active`, `when.isDateAllowed`, `teamChallenge` vs principal, `limitTotal` já atingido, `join.required` sem JoinLog ativo |
|
|
948
|
+
| Conclui mas sem pontos | `RewardPoint.total == 0` ou attribute usado em MULTIPLY/DIVIDE inválido (catch silencioso) |
|
|
949
|
+
| `LIMIT_PER_TEAM` parece quebrado | Bug: conta pelo `userId`, não `teamId` |
|
|
950
|
+
| `RANGE_ALL_IN_ORDER` falha mesmo com rules feitas | Verificar `orderTime` no progress: o trigger atual define `trigger.time` para a rule da action corrente; outras rules pegam o LATEST log no `between` |
|
|
951
|
+
| Pontos não creditados em teamChallenge | `RewardPoint.perPlayer` precisa ser `true` para distribuir aos membros |
|
|
952
|
+
| Reward `FUNIFIER_RANDOM` não credita item | `restrict=true` E nenhum item disponível para o player → silenciosamente nada acontece |
|
|
953
|
+
| Rule com aggregate retorna 0 sempre | Pipeline precisa retornar campos `_id`, `total` e opcionalmente `max`. Verifique `$group:{_id:..., total:{$sum:...}}` |
|
|
954
|
+
| String `"FUNIFIER_RANDOM"` aparece literal em achievement | reward foi processado mas o `findRandom` retornou null E `r.item` era explicitamente `"FUNIFIER_RANDOM"` |
|
|
955
|
+
| Descrição em pt-BR diz "mês" onde devia dizer "semana" | Bug em `describe` (`pt.put("semana", "mês")`) |
|
|
956
|
+
| Challenge "desapareceu" entre 2 reads consecutivos | `manager.update()` foi chamado (delete+save não-atômico). Verifique se algum código fora do REST está chamando isso. |
|
|
957
|
+
| Memory leak / logs ruidosos | `describe` imprime no stdout |
|
|
958
|
+
|
|
959
|
+
### 9.6 Logs de avaliação
|
|
960
|
+
|
|
961
|
+
`AchievementManager.fireAction` captura `Exception` por challenge num try externo e imprime `Falha ao avaliar desafio <name>` + stacktrace no stdout. Se um challenge falha em runtime (parse de Mustache, divisão por zero, etc.), o **dispatch continua** e os outros challenges seguem normalmente.
|
|
962
|
+
|
|
963
|
+
---
|
|
964
|
+
|
|
965
|
+
## 10. Exemplos Práticos
|
|
966
|
+
|
|
967
|
+
### 10.1 Exemplo mínimo — 1 venda
|
|
193
968
|
|
|
194
969
|
```json
|
|
195
970
|
{
|
|
196
|
-
"
|
|
197
|
-
"
|
|
198
|
-
"
|
|
199
|
-
"rules": [
|
|
200
|
-
|
|
201
|
-
],
|
|
202
|
-
"
|
|
203
|
-
{ "total": 10, "category": "xp", "operation": 0 }
|
|
204
|
-
]
|
|
971
|
+
"_id": "primeira_venda",
|
|
972
|
+
"challenge": "Primeira venda",
|
|
973
|
+
"active": true,
|
|
974
|
+
"rules": [{ "actionId": "venda" }],
|
|
975
|
+
"range": 0,
|
|
976
|
+
"points": [{ "total": 10, "category": "moedas", "operation": 0 }],
|
|
977
|
+
"techniques": ["GT35", "GT05"]
|
|
205
978
|
}
|
|
206
979
|
```
|
|
207
980
|
|
|
208
|
-
|
|
981
|
+
Comportamento: cada `action_log` com `actionId=venda` registra +10 moedas. Como `limitTotal=0` (ilimitado), repete infinitamente.
|
|
982
|
+
|
|
983
|
+
### 10.2 Exemplo intermediário — 3 vendas por dia
|
|
209
984
|
|
|
210
985
|
```json
|
|
211
986
|
{
|
|
212
|
-
"challenge": "
|
|
213
|
-
"
|
|
214
|
-
"techniques": ["GT35"],
|
|
987
|
+
"challenge": "Vendedor diário",
|
|
988
|
+
"active": true,
|
|
215
989
|
"rules": [
|
|
216
990
|
{
|
|
217
|
-
"actionId": "
|
|
991
|
+
"actionId": "venda",
|
|
992
|
+
"total": 3,
|
|
218
993
|
"operator": 5,
|
|
219
|
-
"
|
|
220
|
-
"
|
|
221
|
-
{ "param": "product", "operator": 1, "value": "book" }
|
|
222
|
-
]
|
|
994
|
+
"timeAmount": 1,
|
|
995
|
+
"timeScale": 5
|
|
223
996
|
}
|
|
224
997
|
],
|
|
225
|
-
"
|
|
226
|
-
|
|
227
|
-
|
|
998
|
+
"range": 0,
|
|
999
|
+
"limitTotal": 1,
|
|
1000
|
+
"limitPerType": 0,
|
|
1001
|
+
"limitTimeAmount": 1,
|
|
1002
|
+
"limitTimeScale": 5,
|
|
1003
|
+
"points": [{ "total": 100, "category": "moedas" }]
|
|
228
1004
|
}
|
|
229
1005
|
```
|
|
230
1006
|
|
|
231
|
-
|
|
1007
|
+
- `operator: 5` (GREATER_THAN_OR_EQUAL), `total: 3`
|
|
1008
|
+
- `timeScale: 5` (DAY) com `timeAmount: 1` → janela das últimas 24h
|
|
1009
|
+
- `limitTotal: 1` + `limitPerType: 0` (PER_PLAYER) + `limitTimeAmount/Scale: 1/DAY` → 1 conquista por dia por jogador
|
|
1010
|
+
|
|
1011
|
+
### 10.3 Exemplo avançado — multi-rule com join e rewards
|
|
232
1012
|
|
|
233
1013
|
```json
|
|
234
1014
|
{
|
|
235
|
-
"challenge": "
|
|
236
|
-
"
|
|
237
|
-
"range":
|
|
1015
|
+
"challenge": "Onboarding completo",
|
|
1016
|
+
"active": true,
|
|
1017
|
+
"range": 2,
|
|
1018
|
+
"join": { "required": true, "timeframe": "+7d" },
|
|
1019
|
+
"trackFullProgress": true,
|
|
238
1020
|
"rules": [
|
|
239
|
-
{ "actionId": "
|
|
240
|
-
{ "actionId": "
|
|
1021
|
+
{ "actionId": "criar_perfil" },
|
|
1022
|
+
{ "actionId": "completar_tutorial" },
|
|
1023
|
+
{ "actionId": "primeira_compra" }
|
|
241
1024
|
],
|
|
242
|
-
"
|
|
243
|
-
{ "total":
|
|
244
|
-
]
|
|
1025
|
+
"requirements": [
|
|
1026
|
+
{ "total": 1, "type": 3, "item": "novato", "operation": 0 }
|
|
1027
|
+
],
|
|
1028
|
+
"rewards": [
|
|
1029
|
+
{ "total": 1, "type": 2, "item": "FUNIFIER_RANDOM", "folder": "catalogo_bonus", "restrict": true },
|
|
1030
|
+
{ "total": 50, "type": 0, "item": "xp" }
|
|
1031
|
+
],
|
|
1032
|
+
"points": [{ "total": 500, "category": "xp" }],
|
|
1033
|
+
"notifications": [
|
|
1034
|
+
{ "event": 0, "type": 0, "scope": 1, "content": "Bem-vindo! Você completou o onboarding 🎉" }
|
|
1035
|
+
],
|
|
1036
|
+
"principals": [],
|
|
1037
|
+
"propagateOrigin": true,
|
|
1038
|
+
"tags": ["onboarding", "novato"]
|
|
245
1039
|
}
|
|
246
1040
|
```
|
|
247
1041
|
|
|
248
|
-
|
|
1042
|
+
- `range: 2` (ALL_IN_ORDER) — as três actions precisam acontecer **nessa sequência cronológica**.
|
|
1043
|
+
- `join.required=true` + `timeframe: "+7d"` → o jogador precisa aceitar via `JoinLog`; tem 7 dias para concluir.
|
|
1044
|
+
- `requirements`: precisa já possuir o nível `novato` (TYPE_LEVEL=3) — não é dedutível.
|
|
1045
|
+
- `rewards`: 1 catalog item aleatório do `catalogo_bonus` (respeitando interval/requirements do item via `restrict=true`) + 50 pontos xp.
|
|
1046
|
+
- `trackFullProgress: true` → grava `challenge_progress` mesmo se zero rules completas.
|
|
1047
|
+
- `propagateOrigin: true` → todos os achievements derivados terão `extra.origin = <id-do-achievement-do-challenge>`.
|
|
1048
|
+
|
|
1049
|
+
### 10.4 Exemplo com `techniques` — categorização explícita por mecânica de jogo
|
|
249
1050
|
|
|
250
1051
|
```json
|
|
251
1052
|
{
|
|
252
|
-
"
|
|
253
|
-
"
|
|
1053
|
+
"_id": "explorador_semanal",
|
|
1054
|
+
"challenge": "Explorador Semanal",
|
|
1055
|
+
"description": "Visite 5 lugares diferentes em uma semana",
|
|
1056
|
+
"active": true,
|
|
1057
|
+
"techniques": ["GT35", "GT05"],
|
|
254
1058
|
"rules": [
|
|
255
|
-
{
|
|
1059
|
+
{
|
|
1060
|
+
"actionId": "checkin",
|
|
1061
|
+
"total": 5,
|
|
1062
|
+
"operator": 5,
|
|
1063
|
+
"timeAmount": 1,
|
|
1064
|
+
"timeScale": 6,
|
|
1065
|
+
"filters": [
|
|
1066
|
+
{ "param": "lugar", "operator": 9, "value": "" }
|
|
1067
|
+
]
|
|
1068
|
+
}
|
|
256
1069
|
],
|
|
257
|
-
"
|
|
1070
|
+
"range": 0,
|
|
258
1071
|
"limitTotal": 1,
|
|
259
1072
|
"limitPerType": 0,
|
|
260
1073
|
"limitTimeAmount": 1,
|
|
261
|
-
"limitTimeScale":
|
|
1074
|
+
"limitTimeScale": 6,
|
|
1075
|
+
"points": [{ "total": 200, "category": "xp" }],
|
|
1076
|
+
"tags": ["exploracao", "semanal"]
|
|
262
1077
|
}
|
|
263
1078
|
```
|
|
264
1079
|
|
|
265
|
-
|
|
1080
|
+
**Comportamento da propriedade `techniques`:**
|
|
266
1081
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
```
|
|
1082
|
+
- `techniques` é um `List<String>` com códigos GT (Game Technique). É **persistido literalmente** — o backend não valida o conteúdo do array.
|
|
1083
|
+
- Se o cliente **não enviar** `techniques` (campo ausente, `null` ou array vazio), o método `GameTechniqueManager.autoConfigureMissingTechniqueFields()` adiciona automaticamente `["GT35"]` na próxima execução desse job. ⚠ Isso **não acontece no save** — só quando o job auto-config roda.
|
|
1084
|
+
- Se o cliente enviar `techniques` com qualquer valor (mesmo códigos inexistentes como `["GTX99"]`), o array é gravado **as-is** e o auto-config **não sobrescreve**.
|
|
1085
|
+
- Códigos comuns para challenges:
|
|
1086
|
+
- `GT35` — Quests / Challenges (técnica principal, default)
|
|
1087
|
+
- `GT05` — Blank Fills (preenchimento/coleção)
|
|
1088
|
+
- `GT26` — Elitism (desafios exclusivos)
|
|
1089
|
+
- Os códigos GT são consumidos pelo módulo `technique` (coleção `technique`) e usados em relatórios / dashboards de framework de gamificação. **Não afetam a avaliação do challenge** (não entram em `fireAction`).
|
|
1090
|
+
|
|
1091
|
+
**Detalhes deste exemplo:**
|
|
1092
|
+
|
|
1093
|
+
- `techniques: ["GT35", "GT05"]` declara explicitamente que este challenge implementa Quests + Blank Fills (cumular 5 lugares diferentes).
|
|
1094
|
+
- `filters[0]`: `operator: 9` (EXIST) sobre `lugar` — exige que o atributo exista no `action_log.attributes.lugar`. (Não há operador "DISTINCT" nativo — para "5 lugares diferentes" o controle de unicidade depende de regras do filtro ou aggregate customizado.)
|
|
1095
|
+
- `timeScale: 6` (WEEK) com `timeAmount: 1` → janela das últimas 7 dias.
|
|
1096
|
+
- `limitTimeScale: 6` (WEEK) + `limitTotal: 1` → 1 conquista por semana por jogador.
|
|
283
1097
|
|
|
284
|
-
###
|
|
1098
|
+
### 10.5 Anti-pattern — usar `MULTIPLY_BY_ATTRIBUTE` em challenge multi-rule
|
|
285
1099
|
|
|
286
1100
|
```json
|
|
287
1101
|
{
|
|
288
|
-
"challenge": "
|
|
289
|
-
"
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
1102
|
+
"challenge": "Comissão sobre venda",
|
|
1103
|
+
"rules": [
|
|
1104
|
+
{ "actionId": "venda" },
|
|
1105
|
+
{ "actionId": "fechamento_mes" }
|
|
1106
|
+
],
|
|
1107
|
+
"range": 0,
|
|
1108
|
+
"points": [
|
|
1109
|
+
{ "total": 1, "category": "comissao", "operation": 1, "value": "valor" }
|
|
294
1110
|
]
|
|
295
1111
|
}
|
|
296
1112
|
```
|
|
297
1113
|
|
|
298
|
-
|
|
1114
|
+
**Por que não fazer:**
|
|
299
1115
|
|
|
300
|
-
|
|
1116
|
+
- O comentário no código diz claramente: "ATENCAO opcao valida apenas para challenge com 1 rule de 1 action".
|
|
1117
|
+
- Quando o challenge fechar, `trigger.attributes["valor"]` será o do **trigger que disparou o fechamento** (`fechamento_mes`), não o do `venda`. Resultado: comissão de zero (se `fechamento_mes` não tem `valor`) ou comissão calculada sobre atributo errado.
|
|
1118
|
+
- Sem warning, sem log, sem erro — apenas zero pontos ou pontos arbitrários.
|
|
301
1119
|
|
|
302
|
-
|
|
303
|
-
funifier_get type=challenge id=<id_retornado>
|
|
304
|
-
```
|
|
1120
|
+
**Correção:** use challenge single-rule com action `venda`, ou crie uma `Action` dedicada com `points` próprios.
|
|
305
1121
|
|
|
306
|
-
|
|
1122
|
+
### 10.6 Anti-pattern — usar `q` cru do cliente
|
|
307
1123
|
|
|
308
|
-
```
|
|
309
|
-
|
|
1124
|
+
```http
|
|
1125
|
+
GET /v3/challenge?q={"extra.secret_flag":true}
|
|
310
1126
|
```
|
|
311
1127
|
|
|
312
|
-
|
|
1128
|
+
**Por que não fazer:**
|
|
313
1129
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
1130
|
+
- `q` é concatenado direto no aggregation sem parse. Cliente pode ler qualquer campo, incluindo flags internas ou de outros tenants se a config Mongo permitir.
|
|
1131
|
+
- Cliente pode forçar `$where` com JavaScript se o Mongo aceitar (deprecated mas habilitado em alguns clusters legados).
|
|
1132
|
+
|
|
1133
|
+
**Correção:** validar/sanitizar `q` no gateway; restringir ao subconjunto de campos públicos.
|
|
1134
|
+
|
|
1135
|
+
---
|
|
318
1136
|
|
|
319
1137
|
## Checklist de Configuração
|
|
320
1138
|
|
|
321
|
-
- [ ] `challenge` (nome) preenchido
|
|
322
|
-
- [ ] `
|
|
323
|
-
- [ ] `
|
|
324
|
-
- [ ] `range
|
|
325
|
-
- [ ] `
|
|
326
|
-
- [ ]
|
|
327
|
-
- [ ]
|
|
328
|
-
- [ ]
|
|
1139
|
+
- [ ] Campo `challenge` (nome) preenchido — sem ele a entidade aparece sem rótulo na UI.
|
|
1140
|
+
- [ ] Todas as `rules[].actionId` apontam para uma `Action` existente — itens sem actionId são removidos no save.
|
|
1141
|
+
- [ ] `Action.attributes` declara o tipo correto (`STRING`/`NUMBER`/`BOOLEAN`) para os atributos usados em `ChallengeRuleFilter.param`.
|
|
1142
|
+
- [ ] Se `range = ALL_IN_ORDER`, garantir que `rules` tem mais de uma entrada (com 1 só rule, o flag é ignorado).
|
|
1143
|
+
- [ ] Se `limitTotal > 0` e quer janela temporal, definir `limitTimeAmount > 0` e `limitTimeScale != NONE` (NONE = janela infinita).
|
|
1144
|
+
- [ ] Se `teamChallenge = true`, `principals` deve conter teamIds (não playerIds) ou estar vazio.
|
|
1145
|
+
- [ ] Se `MULTIPLY_BY_ATTRIBUTE` ou `DIVIDE_BY_ATTRIBUTE` em `RewardPoint`, garantir challenge single-rule + single-action.
|
|
1146
|
+
- [ ] Para `LOTTERY_TICKET`/`CATALOG_ITEM` em rewards com `FUNIFIER_RANDOM`, definir `folder` para limitar o universo.
|
|
1147
|
+
- [ ] Se `join.required = true`, registrar primeiro `JoinLog` via `JoinManager.insert` antes de esperar a conclusão.
|
|
1148
|
+
- [ ] Para `RANGE_ALL_IN_ORDER`, considerar que `orderTime` para rules de action diferente do trigger usa `findLatestActionLogDateRegisteredBetweenDates` — uma rule pode parecer "fora de ordem" se o log mais recente do tipo está fora do `between`.
|
|
1149
|
+
- [ ] Não confie em `LIMIT_PER_TEAM` — usa `userId`. Para limite real por time, modelar via outro campo ou aceitar comportamento PER_PLAYER.
|
|
1150
|
+
- [ ] Não confie em `Operator.LIKE` em filtros — equivale a filtro vazio. Use `REGEX`.
|
|
1151
|
+
- [ ] Para deduzir pontos/items em requirements: `operation = 1` (DEDUCT) e `type ∈ {POINT, CATALOG_ITEM, CHALLENGE}` — outros tipos não são dedutíveis.
|
|
1152
|
+
- [ ] Antes de deletar um challenge, considere deletar manualmente os achievements e progress associados — não há cascade.
|
|
1153
|
+
- [ ] Para diagnóstico em produção, use `/evaluate/{id}` em vez de tentar reproduzir o pipeline manualmente.
|
|
1154
|
+
- [ ] Não envie `q` vindo do cliente sem sanitização — risco de injeção MongoDB.
|
|
1155
|
+
- [ ] Atenção ao bug de tradução `pt.put("semana", "mês")` em `describe(lang="pt*")`.
|
|
1156
|
+
- [ ] Em produção com logs limpos, esteja ciente do leak de stdout em `describe`.
|