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