funifier-mcp 0.2.26 → 0.2.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/.cursor/rules/funifier.mdc +38 -41
  2. package/.github/copilot-instructions.md +38 -41
  3. package/AGENTS.md +56 -49
  4. package/README.md +40 -22
  5. package/datasource-funifier-docs/.coverage.json +326 -0
  6. package/datasource-funifier-docs/.validation.json +593 -0
  7. package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
  8. package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
  9. package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
  10. package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
  11. package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
  12. package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
  13. package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
  14. package/datasource-funifier-docs/knowledge/index.md +4 -1
  15. package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
  16. package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
  17. package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
  18. package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
  19. package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
  20. package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
  21. package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
  22. package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
  23. package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
  24. package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
  25. package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
  26. package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
  27. package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
  28. package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
  29. package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
  30. package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
  31. package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
  32. package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
  33. package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
  34. package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
  35. package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
  36. package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
  37. package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
  38. package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
  39. package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
  40. package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
  41. package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
  42. package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
  43. package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
  44. package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
  45. package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
  46. package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
  47. package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
  48. package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
  49. package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
  50. package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
  51. package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
  52. package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
  53. package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
  54. package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
  55. package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
  56. package/dist/cli/init.d.ts.map +1 -1
  57. package/dist/cli/init.js +42 -1
  58. package/dist/cli/init.js.map +1 -1
  59. package/dist/cli/init.test.js +74 -3
  60. package/dist/cli/init.test.js.map +1 -1
  61. package/dist/cli/persona.d.ts +3 -0
  62. package/dist/cli/persona.d.ts.map +1 -0
  63. package/dist/cli/persona.js +25 -0
  64. package/dist/cli/persona.js.map +1 -0
  65. package/dist/mcp/bundle.js +119 -93
  66. package/dist/mcp/check-update.d.ts +5 -0
  67. package/dist/mcp/check-update.d.ts.map +1 -1
  68. package/dist/mcp/check-update.js +21 -10
  69. package/dist/mcp/check-update.js.map +1 -1
  70. package/dist/mcp/check-update.test.d.ts +2 -0
  71. package/dist/mcp/check-update.test.d.ts.map +1 -0
  72. package/dist/mcp/check-update.test.js +33 -0
  73. package/dist/mcp/check-update.test.js.map +1 -0
  74. package/dist/mcp/index.js +2 -2
  75. package/dist/mcp/index.js.map +1 -1
  76. package/dist/mcp/prompts/templates.d.ts.map +1 -1
  77. package/dist/mcp/prompts/templates.js +35 -0
  78. package/dist/mcp/prompts/templates.js.map +1 -1
  79. package/dist/mcp/resources/documentation.d.ts +1 -1
  80. package/dist/mcp/resources/documentation.d.ts.map +1 -1
  81. package/dist/mcp/resources/documentation.js +39 -3
  82. package/dist/mcp/resources/documentation.js.map +1 -1
  83. package/dist/mcp/tools/connect.d.ts.map +1 -1
  84. package/dist/mcp/tools/connect.js +18 -8
  85. package/dist/mcp/tools/connect.js.map +1 -1
  86. package/dist/mcp/tools/database.d.ts.map +1 -1
  87. package/dist/mcp/tools/database.js +59 -47
  88. package/dist/mcp/tools/database.js.map +1 -1
  89. package/dist/mcp/tools/database.test.js +2 -2
  90. package/dist/mcp/tools/database.test.js.map +1 -1
  91. package/dist/mcp/tools/delete.d.ts.map +1 -1
  92. package/dist/mcp/tools/delete.js +13 -3
  93. package/dist/mcp/tools/delete.js.map +1 -1
  94. package/dist/mcp/tools/execute.d.ts.map +1 -1
  95. package/dist/mcp/tools/execute.js +20 -9
  96. package/dist/mcp/tools/execute.js.map +1 -1
  97. package/dist/mcp/tools/folder.d.ts.map +1 -1
  98. package/dist/mcp/tools/folder.js +22 -12
  99. package/dist/mcp/tools/folder.js.map +1 -1
  100. package/dist/mcp/tools/get.d.ts.map +1 -1
  101. package/dist/mcp/tools/get.js +16 -6
  102. package/dist/mcp/tools/get.js.map +1 -1
  103. package/dist/mcp/tools/index.d.ts +1 -1
  104. package/dist/mcp/tools/index.d.ts.map +1 -1
  105. package/dist/mcp/tools/index.js +28 -1
  106. package/dist/mcp/tools/index.js.map +1 -1
  107. package/dist/mcp/tools/list.d.ts.map +1 -1
  108. package/dist/mcp/tools/list.js +38 -14
  109. package/dist/mcp/tools/list.js.map +1 -1
  110. package/dist/mcp/tools/logs.d.ts.map +1 -1
  111. package/dist/mcp/tools/logs.js +15 -5
  112. package/dist/mcp/tools/logs.js.map +1 -1
  113. package/dist/mcp/tools/save.d.ts.map +1 -1
  114. package/dist/mcp/tools/save.js +14 -4
  115. package/dist/mcp/tools/save.js.map +1 -1
  116. package/dist/mcp/tools/save.test.js +3 -3
  117. package/dist/mcp/tools/save.test.js.map +1 -1
  118. package/dist/mcp/tools/search-docs.d.ts +3 -0
  119. package/dist/mcp/tools/search-docs.d.ts.map +1 -0
  120. package/dist/mcp/tools/search-docs.js +102 -0
  121. package/dist/mcp/tools/search-docs.js.map +1 -0
  122. package/package.json +6 -2
  123. package/skills/acquire-funifier-knowledge/SKILL.md +155 -0
  124. package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
  125. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
  126. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
  127. package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
  128. package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
  129. package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
  130. package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +86 -0
  131. package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
  132. package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
  133. package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
  134. package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
  135. package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
  136. package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
  137. package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
  138. package/skills/funifier/SKILL.md +88 -0
  139. package/skills/funifier/references/configure-security.md +96 -0
  140. package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
  141. package/skills/funifier/references/create-aggregate.md +144 -0
  142. package/skills/funifier/references/create-challenge.md +116 -0
  143. package/skills/funifier/references/create-competition.md +98 -0
  144. package/skills/funifier/references/create-crossword.md +574 -0
  145. package/skills/funifier/references/create-custom-object.md +91 -0
  146. package/skills/funifier/references/create-custom-page.md +135 -0
  147. package/skills/funifier/references/create-folder.md +104 -0
  148. package/skills/funifier/references/create-lastmile.md +643 -0
  149. package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
  150. package/skills/funifier/references/create-level.md +94 -0
  151. package/skills/funifier/references/create-lottery.md +913 -0
  152. package/skills/funifier/references/create-mystery.md +769 -0
  153. package/skills/funifier/references/create-notification.md +75 -0
  154. package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
  155. package/skills/funifier/references/create-quiz.md +98 -0
  156. package/skills/funifier/references/create-scheduler.md +141 -0
  157. package/skills/funifier/references/create-story.md +636 -0
  158. package/skills/funifier/references/create-swap.md +95 -0
  159. package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
  160. package/skills/funifier/references/create-virtual-good.md +96 -0
  161. package/skills/funifier/references/create-webhook.md +72 -0
  162. package/skills/funifier/references/create-websocket.md +71 -0
  163. package/skills/funifier/references/create-widget.md +76 -0
  164. package/skills/funifier/references/debug.md +87 -0
  165. package/skills/funifier/references/help.md +81 -0
  166. package/skills/funifier/references/implement-frontend.md +106 -0
  167. package/skills/funifier/references/import-csv.md +75 -0
  168. package/skills/funifier/references/manage-player.md +82 -0
  169. package/skills/funifier/references/manage-team.md +76 -0
  170. package/skills/funifier/references/upload-file.md +91 -0
  171. package/skills/funifier-create-aggregate/SKILL.md +0 -127
  172. package/skills/funifier-create-challenge/SKILL.md +0 -88
  173. package/skills/funifier-create-custom-page/SKILL.md +0 -127
  174. package/skills/funifier-create-level/SKILL.md +0 -87
  175. package/skills/funifier-create-quiz/SKILL.md +0 -87
  176. package/skills/funifier-create-scheduler/SKILL.md +0 -127
  177. package/skills/funifier-create-virtual-good/SKILL.md +0 -87
  178. package/skills/funifier-debug/SKILL.md +0 -92
  179. package/skills/funifier-help/SKILL.md +0 -86
  180. package/skills/funifier-implement-frontend/SKILL.md +0 -90
  181. package/skills/funifier-index/SKILL.md +0 -58
@@ -1,328 +1,1156 @@
1
- # Challenge (Desafio)
1
+ # `challenge`
2
2
 
3
3
  **Acesso Studio:** `/studio/challenge`
4
4
  **API Endpoint:** `/v3/challenge`
5
+ **Coleção MongoDB:** `challenge`
5
6
 
6
- ## O que é
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
- Configuração de desafios a serem completados por jogadores ou equipes. Permite criar desafios personalizados, definindo ações que o jogador precisa realizar e recompensas que receberá. Os desafios podem variar de simples tarefas a missões complexas compostas por múltiplas ações ou etapas.
9
+ ---
9
10
 
10
- ## Quando usar
11
+ ## 1. Visão Geral
11
12
 
12
- - Em quase todo projeto de gamificação
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
- ## Dependências
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
- - **Point**: tipos de ponto devem existir antes de criar challenges que os recompensam
20
- - **Action**: ações devem existir antes de criar challenges que as referenciam
21
+ Papel arquitetural:
21
22
 
22
- ## Campos Obrigatórios vs Opcionais
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
- | Campo | Obrigatório | Descrição |
27
+ Relação direta com outros módulos:
28
+
29
+ | Módulo | Onde | Como |
25
30
  |---|---|---|
26
- | `challenge` | sim | Nome/título do desafio |
27
- | `techniques` | sim | Array com pelo menos um código GT (ex: `["GT35"]`) |
28
- | `rules` | sim | Regras que definem o que o jogador deve fazer |
29
- | `points` | recomendado | Recompensas em pontos ao completar |
30
- | `description` | não | Descrição exibida ao jogador |
31
- | `active` | não | `true` por padrão |
32
- | `_id` | não | Gerado automaticamente se omitido |
33
- | `range` | não | Obrigatório quando múltiplas rules; padrão `0` |
34
- | `teamChallenge` | não | `true` para desafios de equipe |
35
- | `limitTotal` | não | Limite de vezes que pode ser completado |
36
- | `limitPerType` | não | A quem se aplica o limite |
37
- | `limitTimeAmount` | não | Número de períodos para o limite |
38
- | `limitTimeScale` | não | Escala de tempo do limite |
39
- | `notifications` | não | Notificações ao completar/criar/alterar |
40
- | `requirements` | não | Pré-requisitos para desbloquear |
41
- | `rewards` | não | Recompensas adicionais (itens de catálogo) |
42
- | `when` | não | Restrição de data/hora de disponibilidade |
43
- | `principals` | não | Restringe o desafio a jogadores/equipes específicos |
44
- | `trackFullProgress` | não | Rastrear progresso completo de cada rule |
45
- | `extra` | não | Campos customizados livres |
46
- | `i18n` | não | Traduções do título/descrição |
47
-
48
- ## Imagem (badge)
49
-
50
- Challenges usam o campo `badge` (não `image`) para a imagem do desafio:
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
- ```json
53
- "badge": {
54
- "small": { "url": "https://...", "size": 0, "width": 0, "height": 0, "depth": 0 },
55
- "medium": { "url": "https://...", "size": 0, "width": 0, "height": 0, "depth": 0 },
56
- "original": { "url": "https://...", "size": 0, "width": 0, "height": 0, "depth": 0 }
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
- Na prática basta preencher `url` nos três tamanhos com a mesma URL se não houver versões distintas.
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
- ## Técnicas de Jogo (techniques)
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
- `techniques` é **obrigatório**. Cada código GT representa uma mecânica de gamificação. Para challenges, os códigos válidos são:
233
+ ### 2.3 Pipeline CRUD via `/v3/challenge`
65
234
 
66
- `GT35`, `GT22`, `GT02`, `GT07`, `GT17`, `GT14`, `GT48`, `GT12`, `GT40`, `GT89`, `GT68`, `GT66`, `GT67`, `GT21`, `GT30`, `GT86`
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
- O mais comum para challenges é **`GT35` (Quest List)** exibe uma lista de tarefas/missões para o jogador cumprir.
245
+ Não existe método `PUT`. Atualização é feita reusando `POST` com `_id` preenchido (Jongo `save` é upsert).
69
246
 
70
- ```json
71
- "techniques": ["GT35"]
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
- ## Tabela de Referência dos Enums
264
+ `ChallengeDaoMongo.update`:
75
265
 
76
- ### `range` — como as rules são avaliadas
266
+ ```
267
+ [1] jongo("challenge").remove({_id: challenge.id})
268
+ [2] jongo("challenge").save(challenge)
269
+ ```
77
270
 
78
- | Valor | Label |
79
- |---|---|
80
- | `0` | Complete All (completar todas as rules) |
81
- | `1` | Complete Any (completar qualquer uma) |
82
- | `2` | Complete All in Order (completar todas em ordem) |
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&lt;String&gt; | `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&lt;String&gt; | null | não | players/teams elegíveis. **null/empty = todos** |
304
+ | `extra` | Map | `{}` | não | campos livres |
305
+ | `rewards` | List&lt;`Requirement`&gt; | `[]` | não | recompensas materializadas ao concluir |
306
+ | `i18n` | Map&lt;String, Map&lt;String, String&gt;&gt; | `{}` | 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
- Padrão: `0` (Complete All). Necessário quando há mais de uma rule.
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&lt;`ChallengeRuleFilter`&gt; | 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
- ### `rules[].operator` — critério de contagem da rule
386
+ **Sobre `rule.total`:**
87
387
 
88
- | Valor | Label | Uso |
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
- | `5` | `>=` (Greater Than or Equal) | Padrão contar N ou mais vezes |
91
- | `1` | `=` (Equal) | Exatamente N vezes |
92
- | `40` | `%` (Percentage) | Percentual |
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
- Exemplo: `operator: 5, total: 3` = executar a ação 3 ou mais vezes.
402
+ Conversão automática em `buildJsonFilter` baseada em `Action.attribute.type`:
95
403
 
96
- ### `rules[].filters[].operator` operador dos filtros de atributo
404
+ - `TYPE_NUMBER` valor não-aspeado (numérico)
405
+ - `TYPE_BOOLEAN` → valor não-aspeado
406
+ - demais tipos → string aspeada
97
407
 
98
- | Valor | Label |
99
- |---|---|
100
- | `1` | Is (igual a) |
101
- | `3` | Contains (contém) |
102
- | `4` | Greater Than (maior que) |
103
- | `5` | Greater Than or Equal To |
104
- | `6` | Less Than (menor que) |
105
- | `7` | Less Than or Equal To |
106
- | `8` | Is Not (diferente de) |
107
- | `9` | Exists (campo existe) |
108
- | `10` | Does Not Exist (campo não existe) |
109
- | `20` | Near in Meters (geolocalização) |
110
-
111
- ### `points[].operation` operação sobre atributo numérico da ação
112
-
113
- | Valor | Label | Uso |
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
- | `0` | None | Pontos fixos (padrão) |
116
- | `1` | Multiply By | Multiplica `total` pelo valor do atributo da ação |
117
- | `2` | Divide By | Divide `total` pelo valor do atributo da ação |
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
- `operation > 0` faz sentido quando a rule tem exatamente 1 action com atributo numérico. Nesse caso, `points[].value` deve conter o nome do atributo.
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
- ### `limitPerType` — a quem o limite se aplica
459
+ ### 3.6 `Requirement` — `challenge.requirements[]` e `challenge.rewards[]`
122
460
 
123
- | Valor | Label |
124
- |---|---|
125
- | `0` | Player |
126
- | `1` | Team |
127
- | `2` | Gamification (global) |
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&lt;`ChallengeRuleProgress`&gt; | 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
- ### `limitTimeScale` escala de tempo do limite
546
+ `JoinManager.insert` consome este timeframe ao registrar o aceite.
130
547
 
131
- | Valor | Label |
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
- | `0` | None (sem janela de tempo) |
134
- | `2` | Second |
135
- | `3` | Minute |
136
- | `4` | Hour |
137
- | `5` | Day |
138
- | `6` | Week |
139
- | `7` | Month |
140
- | `8` | Year |
141
-
142
- ### `notifications[].event`
143
-
144
- | Valor | Label |
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
- | `0` | On Win (ao completar o desafio) |
147
- | `1` | On Create (ao criar o progresso) |
148
- | `2` | On Change (ao avançar no progresso) |
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
- ### `notifications[].scope`
626
+ Não há PUT explícito. Para atualizar, envie POST com `_id` preenchido.
151
627
 
152
- | Valor | Label |
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
- | `0` | Private (apenas o jogador) |
155
- | `1` | Newsfeed (visível no feed) |
156
- | `99` | Custom |
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
- ### `requirements[].type` / `rewards[].type`
650
+ Pelo código (`ChallengeManager.describe`), 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
- | Valor | Label |
654
+ ### `GET /v3/challenge/evaluate/{id}?player=&time=`
655
+
656
+ | Aspecto | Detalhe |
161
657
  |---|---|
162
- | `0` | Point |
163
- | `1` | Challenge |
164
- | `2` | Catalog Item |
165
- | `3` | Level |
166
- | `5` | Lottery |
167
- | `9` | Competition |
168
- | `50` | Lottery Ticket (apenas em rewards) |
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
- ## API Endpoints
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
- ### Listar Desafios
173
- **Método:** GET
174
- **Endpoint:** `/v3/challenge`
670
+ ### `GET /v3/challenge/prepared`
175
671
 
176
- ### Buscar por ID
177
- **Método:** GET
178
- **Endpoint:** `/v3/challenge/:id`
672
+ Retorna toda a coleção `challenge_rule_prepared` sem filtros.
179
673
 
180
- ### Criar / Atualizar Desafio
181
- **Método:** POST
182
- **Endpoint:** `/v3/challenge`
674
+ ---
183
675
 
184
- Criar e atualizar usam o mesmo endpoint POST. Se `_id` for enviado e o documento existir, é atualizado.
676
+ ## 5. Regras de Negócio
185
677
 
186
- ### Excluir Desafio
187
- **Método:** DELETE
188
- **Endpoint:** `/v3/challenge/:id`
678
+ ### 5.1 Validações implícitas (no `add`)
189
679
 
190
- ## Exemplos
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
- ### Challenge simples executar uma ação N vezes
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
- "challenge": "Watch Video",
197
- "description": "Watch a video to earn 10 xp",
198
- "techniques": ["GT35"],
199
- "rules": [
200
- { "actionId": "watch_video", "operator": 5, "total": 1 }
201
- ],
202
- "points": [
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
- ### Challenge com filtro de atributo
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": "Sell 10 Books",
213
- "description": "Sell 10 books to earn 25 xp",
214
- "techniques": ["GT35"],
987
+ "challenge": "Vendedor diário",
988
+ "active": true,
215
989
  "rules": [
216
990
  {
217
- "actionId": "sell",
991
+ "actionId": "venda",
992
+ "total": 3,
218
993
  "operator": 5,
219
- "total": 10,
220
- "filters": [
221
- { "param": "product", "operator": 1, "value": "book" }
222
- ]
994
+ "timeAmount": 1,
995
+ "timeScale": 5
223
996
  }
224
997
  ],
225
- "points": [
226
- { "total": 25, "category": "xp", "operation": 0 }
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
- ### Challenge com múltiplas rules (completar todas)
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": "Daily Routine",
236
- "techniques": ["GT35"],
237
- "range": 0,
1015
+ "challenge": "Onboarding completo",
1016
+ "active": true,
1017
+ "range": 2,
1018
+ "join": { "required": true, "timeframe": "+7d" },
1019
+ "trackFullProgress": true,
238
1020
  "rules": [
239
- { "actionId": "login", "operator": 5, "total": 1 },
240
- { "actionId": "comment", "operator": 5, "total": 3 }
1021
+ { "actionId": "criar_perfil" },
1022
+ { "actionId": "completar_tutorial" },
1023
+ { "actionId": "primeira_compra" }
241
1024
  ],
242
- "points": [
243
- { "total": 50, "category": "xp", "operation": 0 }
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
- ### Challenge com limite de frequência (1x por dia por jogador)
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
- "challenge": "Daily Check-in",
253
- "techniques": ["GT35"],
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
- { "actionId": "checkin", "operator": 5, "total": 1 }
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
- "points": [{ "total": 5, "category": "xp", "operation": 0 }],
1070
+ "range": 0,
258
1071
  "limitTotal": 1,
259
1072
  "limitPerType": 0,
260
1073
  "limitTimeAmount": 1,
261
- "limitTimeScale": 5
1074
+ "limitTimeScale": 6,
1075
+ "points": [{ "total": 200, "category": "xp" }],
1076
+ "tags": ["exploracao", "semanal"]
262
1077
  }
263
1078
  ```
264
1079
 
265
- ### Challenge com notificação ao completar
1080
+ **Comportamento da propriedade `techniques`:**
266
1081
 
267
- ```json
268
- {
269
- "challenge": "First Comment",
270
- "techniques": ["GT35"],
271
- "rules": [{ "actionId": "comment", "operator": 5, "total": 1 }],
272
- "points": [{ "total": 10, "category": "xp", "operation": 0 }],
273
- "notifications": [
274
- {
275
- "event": 0,
276
- "type": 0,
277
- "scope": 0,
278
- "content": "{{player.name}} completed {{item.name}}"
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
- ### Challenge com pré-requisito (desbloqueia após completar outro desafio)
1098
+ ### 10.5 Anti-pattern usar `MULTIPLY_BY_ATTRIBUTE` em challenge multi-rule
285
1099
 
286
1100
  ```json
287
1101
  {
288
- "challenge": "Advanced Task",
289
- "techniques": ["GT35"],
290
- "rules": [{ "actionId": "upload", "operator": 5, "total": 5 }],
291
- "points": [{ "total": 100, "category": "xp", "operation": 0 }],
292
- "requirements": [
293
- { "type": 1, "total": 1, "item": "first_comment_challenge_id" }
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
- ## Como Validar se foi Criado Corretamente
1114
+ **Por que não fazer:**
299
1115
 
300
- Após salvar com `funifier_save`, confirme com `funifier_get`:
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
- Ou liste e filtre:
1122
+ ### 10.6 Anti-pattern — usar `q` cru do cliente
307
1123
 
308
- ```
309
- funifier_list type=challenge search=<nome_do_challenge>
1124
+ ```http
1125
+ GET /v3/challenge?q={"extra.secret_flag":true}
310
1126
  ```
311
1127
 
312
- Para testar se o challenge está funcionando, registre a ação e verifique o progresso do jogador:
1128
+ **Por que não fazer:**
313
1129
 
314
- ```
315
- POST /v3/action/log { "actionId": "...", "player": "player_id" }
316
- GET /v3/challenge/progress?player=player_id
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
- - [ ] `techniques` com pelo menos um código GT (ex: `["GT35"]`)
323
- - [ ] `rules` com `actionId` existente, `operator` e `total` definidos
324
- - [ ] `range` definido quando mais de uma rule
325
- - [ ] `points` com `category` existente
326
- - [ ] Ações referenciadas nas rules existem (`funifier_list type=action`)
327
- - [ ] Pontos referenciados existem (`funifier_list type=point`)
328
- - [ ] Validado com `funifier_get` após salvar
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`.