funifier-mcp 0.2.26 → 0.2.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) 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/index.js +2 -2
  67. package/dist/mcp/index.js.map +1 -1
  68. package/dist/mcp/resources/documentation.d.ts +1 -1
  69. package/dist/mcp/resources/documentation.d.ts.map +1 -1
  70. package/dist/mcp/resources/documentation.js +39 -3
  71. package/dist/mcp/resources/documentation.js.map +1 -1
  72. package/dist/mcp/tools/connect.d.ts.map +1 -1
  73. package/dist/mcp/tools/connect.js +18 -8
  74. package/dist/mcp/tools/connect.js.map +1 -1
  75. package/dist/mcp/tools/database.d.ts.map +1 -1
  76. package/dist/mcp/tools/database.js +59 -47
  77. package/dist/mcp/tools/database.js.map +1 -1
  78. package/dist/mcp/tools/database.test.js +2 -2
  79. package/dist/mcp/tools/database.test.js.map +1 -1
  80. package/dist/mcp/tools/delete.d.ts.map +1 -1
  81. package/dist/mcp/tools/delete.js +13 -3
  82. package/dist/mcp/tools/delete.js.map +1 -1
  83. package/dist/mcp/tools/execute.d.ts.map +1 -1
  84. package/dist/mcp/tools/execute.js +20 -9
  85. package/dist/mcp/tools/execute.js.map +1 -1
  86. package/dist/mcp/tools/folder.d.ts.map +1 -1
  87. package/dist/mcp/tools/folder.js +22 -12
  88. package/dist/mcp/tools/folder.js.map +1 -1
  89. package/dist/mcp/tools/get.d.ts.map +1 -1
  90. package/dist/mcp/tools/get.js +16 -6
  91. package/dist/mcp/tools/get.js.map +1 -1
  92. package/dist/mcp/tools/index.d.ts +1 -1
  93. package/dist/mcp/tools/index.d.ts.map +1 -1
  94. package/dist/mcp/tools/index.js +3 -1
  95. package/dist/mcp/tools/index.js.map +1 -1
  96. package/dist/mcp/tools/list.d.ts.map +1 -1
  97. package/dist/mcp/tools/list.js +38 -14
  98. package/dist/mcp/tools/list.js.map +1 -1
  99. package/dist/mcp/tools/logs.d.ts.map +1 -1
  100. package/dist/mcp/tools/logs.js +15 -5
  101. package/dist/mcp/tools/logs.js.map +1 -1
  102. package/dist/mcp/tools/save.d.ts.map +1 -1
  103. package/dist/mcp/tools/save.js +14 -4
  104. package/dist/mcp/tools/save.js.map +1 -1
  105. package/dist/mcp/tools/save.test.js +3 -3
  106. package/dist/mcp/tools/save.test.js.map +1 -1
  107. package/dist/mcp/tools/search-docs.d.ts +3 -0
  108. package/dist/mcp/tools/search-docs.d.ts.map +1 -0
  109. package/dist/mcp/tools/search-docs.js +102 -0
  110. package/dist/mcp/tools/search-docs.js.map +1 -0
  111. package/package.json +6 -2
  112. package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
  113. package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
  114. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
  115. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
  116. package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
  117. package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
  118. package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
  119. package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
  120. package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
  121. package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
  122. package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
  123. package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
  124. package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
  125. package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
  126. package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
  127. package/skills/funifier/SKILL.md +88 -0
  128. package/skills/funifier/references/configure-security.md +96 -0
  129. package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
  130. package/skills/funifier/references/create-aggregate.md +144 -0
  131. package/skills/funifier/references/create-challenge.md +116 -0
  132. package/skills/funifier/references/create-competition.md +98 -0
  133. package/skills/funifier/references/create-crossword.md +574 -0
  134. package/skills/funifier/references/create-custom-object.md +91 -0
  135. package/skills/funifier/references/create-custom-page.md +135 -0
  136. package/skills/funifier/references/create-folder.md +104 -0
  137. package/skills/funifier/references/create-lastmile.md +643 -0
  138. package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
  139. package/skills/funifier/references/create-level.md +94 -0
  140. package/skills/funifier/references/create-lottery.md +913 -0
  141. package/skills/funifier/references/create-mystery.md +769 -0
  142. package/skills/funifier/references/create-notification.md +75 -0
  143. package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
  144. package/skills/funifier/references/create-quiz.md +98 -0
  145. package/skills/funifier/references/create-scheduler.md +141 -0
  146. package/skills/funifier/references/create-story.md +636 -0
  147. package/skills/funifier/references/create-swap.md +95 -0
  148. package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
  149. package/skills/funifier/references/create-virtual-good.md +96 -0
  150. package/skills/funifier/references/create-webhook.md +72 -0
  151. package/skills/funifier/references/create-websocket.md +71 -0
  152. package/skills/funifier/references/create-widget.md +76 -0
  153. package/skills/funifier/references/debug.md +87 -0
  154. package/skills/funifier/references/help.md +81 -0
  155. package/skills/funifier/references/implement-frontend.md +106 -0
  156. package/skills/funifier/references/import-csv.md +75 -0
  157. package/skills/funifier/references/manage-player.md +82 -0
  158. package/skills/funifier/references/manage-team.md +76 -0
  159. package/skills/funifier/references/upload-file.md +91 -0
  160. package/skills/funifier-create-aggregate/SKILL.md +0 -127
  161. package/skills/funifier-create-challenge/SKILL.md +0 -88
  162. package/skills/funifier-create-custom-page/SKILL.md +0 -127
  163. package/skills/funifier-create-level/SKILL.md +0 -87
  164. package/skills/funifier-create-quiz/SKILL.md +0 -87
  165. package/skills/funifier-create-scheduler/SKILL.md +0 -127
  166. package/skills/funifier-create-virtual-good/SKILL.md +0 -87
  167. package/skills/funifier-debug/SKILL.md +0 -92
  168. package/skills/funifier-help/SKILL.md +0 -86
  169. package/skills/funifier-implement-frontend/SKILL.md +0 -90
  170. package/skills/funifier-index/SKILL.md +0 -58
@@ -1,82 +1,692 @@
1
- # Mystery (Mistério / Caixa Surpresa)
1
+ # `mystery`
2
2
 
3
3
  **Acesso Studio:** `/studio/mystery`
4
4
  **API Endpoint:** `/v3/mystery`
5
+ **Coleções MongoDB:** `mystery_box`, `mystery_box_log`, `mystery_folder` (as vitórias e recompensas ficam em `achievement` com `type=6`)
5
6
 
6
- ## O que é
7
+ > Documentação de engenharia reversa, produzida a partir do código-fonte real do `funifier-service` (commit `830037e`). Os exemplos da API antiga (`MysteryBoxRest.java`) contêm campos que o runtime **ignora silenciosamente** — este documento prioriza o comportamento do código sobre o que os exemplos sugerem.
7
8
 
8
- Jogos de probabilidade e recompensas aleatórias. Permite criar experiências como caixas surpresa, rodas da fortuna, raspadinhas e outros jogos baseados em sorte, definindo as chances e prêmios disponíveis.
9
+ ---
9
10
 
10
- ## Quando usar
11
+ ## 1. Visão Geral
11
12
 
12
- - Para criar rodas da fortuna
13
- - Para raspadinhas premiadas
14
- - Para jogos de cara ou coroa
15
- - Para recompensas aleatórias com probabilidades definidas
13
+ O módulo `mystery` (Mystery Box / "caça-níquel"/slot machine/raspadinha) implementa **recompensa aleatória por probabilidade**. O jogador "gira a roleta" (executa a mystery box) e o sistema sorteia, para cada coluna, uma das opções configuradas respeitando a probabilidade de cada uma. O conjunto de opções sorteadas é comparado contra a `win_chart` (tabela de combinações vencedoras); se houver casamento, o jogador recebe a recompensa associada.
16
14
 
17
- ## Checklist de Configuração no Studio
15
+ Papel arquitetural:
18
16
 
19
- - [ ] Definir título da mystery box
20
- - [ ] Definir opções com probabilidades (devem somar 1.0)
21
- - [ ] Definir número de colunas para exibição
22
- - [ ] Configurar combinações vencedoras (win_chart)
23
- - [ ] Definir recompensas para cada combinação
24
- - [ ] Configurar requisitos para jogar (requirements)
17
+ - A configuração é um documento na coleção `mystery_box` (`MysteryBox.java`). **Não existe Service nem Repository/Dao dedicados** — toda persistência é feita diretamente via `Jongo` dentro de `MysteryBoxManager` (`MysteryBoxManager.java`, L31-754).
18
+ - **Não coleção própria de "resultados"**: a jogada vencedora e cada recompensa viram documentos na coleção `achievement`, reutilizando o `AchievementManager` (totais, level-up, `player_status`, triggers, webhooks). Por isso o módulo depende fortemente de [achievement](achievement.md).
19
+ - Toda execução é registrada na coleção `mystery_box_log` (auditoria de tentativas / "attempts").
20
+ - A execução é **sempre síncrona e manual** (chamada de endpoint). Não há scheduler/job automático para mystery — diferente de [lottery](lottery.md).
25
21
 
26
- ## API Endpoints
22
+ Relação com outros módulos:
27
23
 
28
- ### Listar Mystery Boxes
29
- **Método:** GET
30
- **Endpoint:** `/v3/mystery`
24
+ - [achievement](achievement.md) a jogada vira `Achievement type=6` (somente em vitória); cada recompensa vira um achievement adicional (ponto/desafio/item).
25
+ - [trigger](../guides/triggers.md) — dispara `before_win`/`after_win`/`before_lose`/`after_lose` (entidade `mystery_box`), além do evento literal `before_win_reward` e dos eventos `before_win`/`after_win` na coleção da recompensa.
26
+ - [challenge](challenge.md) (`Requirement`) — define tanto o **custo de jogar** (`requirements`) quanto a **recompensa** de cada combinação (`win_chart[].reward`).
27
+ - `point_category`, `catalog_item` (virtual good) — tipos de recompensa suportados (além de `challenge`).
28
+ - [folder](folder.md) — agrupamento via coleção `mystery_folder`.
29
+ - `ai` — `POST /v3/ai/build/mystery` (`AIRest.buildMystery`, L2088) **gera** um JSON de mystery box via OpenAI a partir de linguagem natural, mas **não persiste** — apenas devolve o objeto desserializado.
31
30
 
32
- ### Criar Mystery Box
33
- **Método:** POST
34
- **Endpoint:** `/v3/mystery`
31
+ ---
32
+
33
+ ## 2. Arquitetura e Fluxos
34
+
35
+ ### 2.1 Classes envolvidas
36
+
37
+ | Classe | Papel |
38
+ |---|---|
39
+ | `com.funifier.engine.mystery.MysteryBox` | Entidade/POJO raiz — documento em `mystery_box` (`MysteryBox.java`, L24-89) |
40
+ | `com.funifier.engine.mystery.MysteryOption` | Uma opção sorteável (valor + probabilidade) (`MysteryOption.java`) |
41
+ | `com.funifier.engine.mystery.MysteryWin` | Linha da `win_chart` (combinação vencedora + recompensa + limites) (`MysteryWin.java`) |
42
+ | `com.funifier.engine.mystery.MysteryResult` | DTO de saída do `execute` — **não persistido** (`MysteryResult.java`) |
43
+ | `com.funifier.engine.mystery.MysteryBoxLog` | Log de tentativa — documento em `mystery_box_log` (`MysteryBoxLog.java`) |
44
+ | `com.funifier.engine.mystery.MysteryFolder` | Pasta de agrupamento — documento em `mystery_folder` |
45
+ | `com.funifier.engine.mystery.MysteryBoxFolder` | **Classe morta** — duplicata de `MysteryFolder`, sem nenhuma referência no código (ver §7) |
46
+ | `com.funifier.engine.mystery.ProbabilityUtil` | Sorteio ponderado por probabilidade (`getChoice`) (`ProbabilityUtil.java`) |
47
+ | `com.funifier.engine.mystery.MysteryBoxManager` | Manager monolítico: CRUD, execução, limites, rollback, evaluate (`MysteryBoxManager.java`, L31-754) |
48
+ | `com.funifier.rest.v3.rest.MysteryBoxRest` | Controller REST v3 (`/v3/mystery`) (`MysteryBoxRest.java`, L44-572) |
49
+
50
+ ### 2.2 Pipeline de criação — `insert(MysteryBox)` (L39-70)
51
+
52
+ `POST /v3/mystery` → `MysteryBoxManager.insert`. Comportamento de **upsert** (Jongo `c.save` grava por `_id`; reenviar o mesmo `_id` **substitui o documento inteiro**).
53
+
54
+ ```
55
+ [1] Se _id ausente/vazio → gera Guid.newShortGuid() (L42-44)
56
+ [2] Se active == null → active = true (L46-48)
57
+ [3] prepareOptions(mystery) — recalcula probabilidades das opções (L51 → L117-146)
58
+ [4] prepareWinchart(mystery) — atribui _id "o1","o2",... às combinações (L54 → L85-111)
59
+ [5] Se folder vazio → folder = "default" (L57-59)
60
+ [6] Garante que o documento da pasta exista em mystery_folder;
61
+ se não existir, cria MysteryFolder {id=folder, title=folder} (L60-67)
62
+ [7] c.save(mystery) na coleção mystery_box (L69)
63
+ ```
64
+
65
+ > **Não existe `PUT`.** Atualização = re-`POST` com o mesmo `_id`. Como é `save` (full replace), **omitir um campo o apaga** — não é patch parcial.
66
+
67
+ #### `prepareOptions` — recálculo de probabilidades (L117-146)
68
+
69
+ Apesar do comentário do método citar `type == option_probability`, ele é executado **sempre**, independentemente de qualquer `type` (que aliás é ignorado — ver §3/§7).
70
+
71
+ ```
72
+ para cada opção o:
73
+ se o.probability == null OU == 0 → vai para zeroOptions
74
+ senão se 0 < o.probability <= 1 → vai para goodOptions; goodTotal += o.probability
75
+ senão (prob > 1 OU prob < 0) → DESCARTADA silenciosamente (!)
76
+
77
+ diffTotal = 1 - goodTotal
78
+ division = diffTotal / zeroOptions.size()
79
+ para cada opção em zeroOptions:
80
+ o.probability = division (divide o "resto" igualmente entre as opções sem prob.)
81
+
82
+ mystery.options = goodOptions ++ (zeroOptions já ajustadas) → ORDEM É ALTERADA (!)
83
+ ```
84
+
85
+ Regras emergentes (não documentadas no schema):
86
+
87
+ - Opções com `probability > 1` ou `< 0` são **removidas silenciosamente** do documento salvo.
88
+ - Opções sem probabilidade (`null`/`0`) recebem partes iguais do "resto" (`1 - soma das demais`).
89
+ - Se a soma das probabilidades informadas for `< 1` **e não houver** opção de probabilidade zero, o "resto" vira a chance de **NONE** (sorteio devolve `null` — ver §2.4).
90
+ - **Foot-gun:** se `goodTotal > 1`, `diffTotal` fica **negativo** e as opções "zero" recebem probabilidade **negativa**, corrompendo `ProbabilityUtil.getChoice()` (ver §5).
91
+ - A lista é **reordenada** (boas primeiro, depois as ex-zero) — qualquer UI/Groovy que dependa da ordem de entrada verá a ordem mudar após o primeiro save.
92
+
93
+ ### 2.3 Pipeline principal — `execute(id, player[, extra])` (L173-419)
94
+
95
+ Método **`synchronized`** (lock de **instância** do `MysteryBoxManager`, que é por API key / `ManagerFactory`). Serializa execuções concorrentes **dentro da mesma JVM**; **não é lock distribuído** — múltiplos pods podem competir. Síncrono, **sem transação MongoDB** (cada `save`/`addAchievement` é independente).
96
+
97
+ A validação de `active`, `limit` e `attempts` acontece **na camada REST** (`MysteryBoxRest.execute`, L244-317) **antes** de chamar o manager. O manager re-busca o documento e executa:
98
+
99
+ ```
100
+ [1] Re-carrega MysteryBox por id; carrega Player (L178-181)
101
+ Se mystery == null OU player == null → não faz nada, retorna result vazio (L184)
102
+ [2] Cria a1 = Achievement(novoGuid, player, total=1, TYPE_MYSTERY_BOX=6, item=id, now) (L187)
103
+ [3] SALVA MysteryBoxLog(a1.id, item=id, player, time, extra) em mystery_box_log (L191-192) (!) SEMPRE
104
+ [4] Se há requirements: evaluateRequirements; se OK → deductRequirements,
105
+ senão → throw "insufficient requirements" (HTTP 400) (L195-202)
106
+ (!) O log do passo [3] já foi gravado mesmo se falhar aqui.
107
+ [5] columns = (mystery.columns > 0) ? mystery.columns : 1 (L206)
108
+ [6] [Apenas se columns == 1] Exclusão de opções com recompensa esgotada (L213-283) — ver §2.5
109
+ [7] Monta ProbabilityUtil (opções viáveis OU originais) e sorteia
110
+ `columns` valores → lista `win` (pode conter null = NONE) (L286-298)
111
+ [8] result = {mystery:id, player, time, result:win} (L301-304)
112
+ [9] SE win_chart não-vazia (L307):
113
+ status = containsWinCombination(win) ? WIN : LOSE (L313-317)
114
+ SE WIN → result.achievements.add(a1);
115
+ trigger before_win (mystery_box);
116
+ addAchievement(a1) <- a1 só persiste em VITÓRIA (!) (L320-331)
117
+ SE LOSE → trigger before_lose (mystery_box) (L333-337)
118
+ Para cada win_chart[i] que casa com `win` (L340-397):
119
+ checkRewardIsInLimit && checkInventoryIsInLimit (L345-348)
120
+ se reward != null E disponível → cria a2, dispara triggers,
121
+ addAchievement(a2) (só p/ reward type 0/1/2) (L351-390)
122
+ senão → System.out.println("MysteryWin not registered because of limit") (L393)
123
+ SE WIN → trigger after_win (mystery_box) (L400-403)
124
+ SE LOSE → trigger after_lose (mystery_box) (L407-409)
125
+ [10] updatePlayerStatus(player) — SEMPRE que mystery!=null && player!=null (L414)
126
+ ```
127
+
128
+ > **Distinção crítica entre `limit` e `attempts`:** o achievement `a1` (`type=6`) só é gravado em **vitória** (passo [9], L330). Logo:
129
+ > - **`limit`** conta documentos `achievement type=6` → é um limite de **VITÓRIAS**.
130
+ > - **`attempts`** conta documentos `mystery_box_log` → é um limite de **TENTATIVAS/jogadas** (gravado sempre, passo [3]).
131
+ >
132
+ > Uma derrota **não** incrementa `limit`, mas **incrementa** `attempts`.
133
+
134
+ > **Sem `win_chart`:** se a box não tiver `win_chart` (vazia/nula), nenhum `status` é definido, **nenhum** achievement `a1` é gravado e **nenhum** trigger dispara. O `result` sai com `result[]` preenchido e `status` ausente.
135
+
136
+ ### Fluxo de execução — `execute(id, player)`
137
+
138
+ ```mermaid
139
+ flowchart TB
140
+ A["REST: validacoes<br/>(existe? active? limit? attempts?)"] --> B["Manager.execute<br/>(synchronized)"]
141
+ B --> C{"mystery != null<br/>&& player != null?"}
142
+ C -- nao --> Zr["retorna result vazio"]
143
+ C -- sim --> D["cria a1 (Achievement type=6)"]
144
+ D --> E["salva MysteryBoxLog (SEMPRE)"]
145
+ E --> F{requirements OK?}
146
+ F -- nao --> Z["throw 400<br/>(log ja persistido)"]
147
+ F -- sim --> G["deductRequirements"]
148
+ G --> H{columns == 1?}
149
+ H -- sim --> I["exclui opcoes c/ recompensa<br/>esgotada e renormaliza"]
150
+ H -- nao --> J["usa opcoes originais"]
151
+ I --> K["sorteia colunas<br/>(ProbabilityUtil)"]
152
+ J --> K
153
+ K --> L{win_chart vazia?}
154
+ L -- sim --> P["sem status, sem trigger"]
155
+ L -- nao --> M{containsWinCombination?}
156
+ M -- sim --> N["status=WIN<br/>before_win + addAchievement(a1)"]
157
+ M -- nao --> O["status=LOSE<br/>before_lose"]
158
+ N --> Q["loop win_chart: rewards (sec.6)"]
159
+ O --> Q
160
+ Q --> R{WIN?}
161
+ R -- sim --> S["after_win (mystery_box)"]
162
+ R -- nao --> T["after_lose (mystery_box)"]
163
+ P --> U["updatePlayerStatus (SEMPRE)"]
164
+ S --> U
165
+ T --> U
166
+ ```
167
+
168
+ ### Interação entre módulos — vitória com recompensa
169
+
170
+ ```mermaid
171
+ sequenceDiagram
172
+ participant C as Cliente
173
+ participant R as MysteryBoxRest
174
+ participant M as MysteryBoxManager
175
+ participant Tr as TriggerManager
176
+ participant A as AchievementManager
177
+ participant DB as MongoDB
178
+
179
+ C->>R: GET /v3/mystery/execute/{id}?player=X
180
+ R->>R: valida active / limit (wins) / attempts (logs)
181
+ R->>M: execute(id, player)
182
+ M->>DB: save mystery_box_log (tentativa)
183
+ M->>A: evaluate+deduct requirements
184
+ M->>M: sorteia colunas (ProbabilityUtil)
185
+ M->>Tr: before_win (mystery_box)
186
+ M->>A: addAchievement(a1 type=6) [so em WIN]
187
+ loop cada win_chart que casa
188
+ M->>M: checkRewardIsInLimit + checkInventoryIsInLimit
189
+ M->>Tr: before_win_reward (mystery_box)
190
+ M->>Tr: before_win (colecao da recompensa)
191
+ M->>A: addAchievement(a2)
192
+ M->>Tr: after_win (colecao da recompensa)
193
+ end
194
+ M->>Tr: after_win (mystery_box)
195
+ M->>A: updatePlayerStatus(player)
196
+ M-->>R: MysteryResult
197
+ R-->>C: 200 {result, status, achievements}
198
+ ```
199
+
200
+ ### 2.4 Sorteio — `ProbabilityUtil.getChoice()` (L95-104)
201
+
202
+ ```
203
+ rand = Math.random() // [0,1)
204
+ para cada item (probs):
205
+ se rand < prob(item) → retorna item
206
+ rand -= prob(item)
207
+ retorna null // "resto" não coberto = NONE
208
+ ```
209
+
210
+ - A iteração segue a ordem do `HashMap` (não determinística entre opções) — não é estável, mas é estatisticamente correta.
211
+ - Se a soma das probabilidades for `< 1`, sobra uma faixa para a qual `getChoice` devolve **`null`** → o valor `null` entra em `result.result[]` e representa "não ganhou nada" naquela coluna.
212
+
213
+ ### 2.5 Exclusão de opções esgotadas (apenas `columns == 1`) (L213-283)
214
+
215
+ Melhoria solicitada pela CH Media. Quando `columns == 1`, antes do sorteio o sistema verifica, para cada combinação vencedora de **1 elemento**, se a recompensa atingiu `limit`/`inventory`. As opções cuja recompensa está esgotada são **removidas do pool** e a probabilidade delas é **redistribuída proporcionalmente** entre as opções viáveis (algoritmo "v2", L274-279):
216
+
217
+ ```
218
+ para cada opção viável o:
219
+ novaProb(o) = o.probability + (o.probability / somaViáveis) * somaIgnoradas
220
+ ```
221
+
222
+ A substituição só ocorre se `0 < #ignoradas < #opções` (se todas estiverem esgotadas, mantém as opções originais). **Para `columns > 1` esta proteção não existe** — pode-se sortear uma combinação cuja recompensa já está esgotada (a recompensa simplesmente não é creditada no passo de win_chart).
223
+
224
+ ---
225
+
226
+ ## 3. Estrutura dos Objetos
227
+
228
+ ### 3.1 `MysteryBox` — documento raiz (coleção `mystery_box`)
229
+
230
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
231
+ |---|---|---|---|---|
232
+ | `_id` | String | `Guid.newShortGuid()` | — | Gerado se ausente/vazio (L42-44). Reenviar o mesmo `_id` substitui o documento. |
233
+ | `title` | String | — | sim (de fato) | Título exibido. Não há validação de obrigatoriedade no backend, mas é esperado pela UI. |
234
+ | `image` | `Image` (objeto) | `null` | não | Imagem de capa. |
235
+ | `options` | `MysteryOption[]` | `[]` | sim | Opções sorteáveis. Reescritas por `prepareOptions` no save (ver §2.2). |
236
+ | `columns` | int | `1` | não | Quantos valores são sorteados por jogada (os "carretéis" do slot). `<= 0` é tratado como `1` na execução (L206). |
237
+ | `requirements` | `Requirement[]` | `[]` | não | **Custo** para jogar (ex.: 1 moeda). Verificados e debitados no `execute` (L195-202). |
238
+ | `win_chart` | `MysteryWin[]` | `[]` | não | Combinações vencedoras e recompensas. Sem `win_chart`, nunca há vitória nem trigger (ver §2.3). |
239
+ | `limit` | `Limit` | `null` | não | Limite de **vitórias** (conta `achievement type=6`). |
240
+ | `attempts` | `Limit` | `null` | não | Limite de **tentativas** (conta `mystery_box_log`). |
241
+ | `folder` | String | `"default"` | não | Pasta de agrupamento. Cria o doc em `mystery_folder` se não existir (L60-67). |
242
+ | `active` | Boolean | `true` (no insert) | não | **Só `false` explícito bloqueia** o execute: `!(active==null \|\| active==true)` (L244). `null`/ausente = ativo. |
243
+ | `extra` | Map | `{}` | não | Metadado livre. **Persistido mas nunca lido** pela lógica do módulo mystery — passthrough puro (ver §7). |
244
+ | `techniques` | String[] | `null` | não | **Legado/inativo.** Nunca escrito nem lido pelo módulo (ver §3.7 e §7). |
245
+
246
+ #### Campos aceitos no JSON mas **silenciosamente ignorados**
247
+
248
+ `MysteryBox` é anotado com `@JsonIgnoreProperties(ignoreUnknown=true)`. Logo, qualquer campo não mapeado é descartado **sem erro**. Em particular:
249
+
250
+ - **`type`** (`"option_probability"` / `"combination_probability"`) — aparece nos exemplos antigos da API (L117, L149) mas **não existe** na entidade. É descartado. O comportamento de recálculo de probabilidades é o mesmo para qualquer "type".
251
+
252
+ #### Métodos de domínio em `MysteryBox`
253
+
254
+ - `containsWinCombination(result)` (L67-76) — `true` se **alguma** linha de `win_chart` casa com o resultado.
255
+ - `getOption(value)` (L78-88) — localiza a `MysteryOption` pelo `value`.
256
+
257
+ ### 3.2 `MysteryOption` — uma opção sorteável
258
+
259
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
260
+ |---|---|---|---|---|
261
+ | `value` | String | — | sim | Identificador da opção (usado nas combinações da `win_chart`). |
262
+ | `probability` | Double | `null` | não | Entre 0 e 1. `null`/`0` → calculada como parte igual do "resto"; `>1` ou `<0` → opção **descartada** (§2.2). |
263
+ | `title` | String | `null` | não | Rótulo exibível. |
264
+ | `image` | String | `null` | não | URL da imagem da opção. |
265
+
266
+ ### 3.3 `MysteryWin` — linha da `win_chart`
267
+
268
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
269
+ |---|---|---|---|---|
270
+ | `_id` | String | `"o1"`,`"o2"`,… | — | Atribuído por `prepareWinchart` se ausente (L96-108). Usado no escopo de `limit`/`inventory` isolados. |
271
+ | `combination` | String[] | — | sim | Lista de `value`s que formam a combinação vencedora. |
272
+ | `orderSensitive` | boolean | `false` | não | `true` = igualdade exata (ordem importa); `false` = **contagem por tipo** (multiset/subconjunto) — ver §5. |
273
+ | `reward` | `Requirement` | `null` | não | Recompensa. **Usa apenas `total`, `type`, `item`** (o `operation` é ignorado — comentário L21). |
274
+ | `limit` | `Limit` | `null` | não | Limite de quantas vezes a recompensa pode ser obtida. **Só `player` e `gamification`** (ver §5/§7). |
275
+ | `inventory` | Double | `null` | não | Estoque global da recompensa (cap absoluto de unidades distribuídas). |
276
+ | `isolated` | boolean | `false` | não | Se `true`, `limit`/`inventory` contam apenas conquistas **desta** mystery box (e, com `_id`, desta combinação). |
277
+
278
+ #### Campo aceito mas ignorado
279
+
280
+ - **`probability`** em cada item de `win_chart` — aparece nos exemplos antigos (L175, L179…) mas **não existe** em `MysteryWin`. É descartado. A chance de uma combinação vencedora **deriva exclusivamente** das probabilidades em `options` × `columns`, **não** de uma probabilidade por combinação.
281
+
282
+ #### `isWinCombination(result)` (L36-78)
283
+
284
+ ```
285
+ se orderSensitive:
286
+ retorna String(combination) == String(result) // igualdade EXATA e ordenada
287
+ senão:
288
+ // conta ocorrências por tipo em result e em combination
289
+ para cada tipo da combination:
290
+ se result não tem aquele tipo OU tem MENOS que o exigido → perdeu
291
+ retorna ganhou
292
+ ```
293
+
294
+ (!) Com `orderSensitive=false` é **contenção de multiconjunto**, não igualdade: `result=[a,a,b]` **satisfaz** `combination=[a,a]`. Com várias colunas, **uma única jogada pode casar várias linhas** da `win_chart` e creditar **múltiplas recompensas** (o loop L340-397 percorre todas).
295
+
296
+ ### 3.4 `MysteryResult` — saída do `execute` (não persistido)
297
+
298
+ | Campo | Tipo | Descrição |
299
+ |---|---|---|
300
+ | `mystery` | String | id da box. |
301
+ | `player` | String | id do jogador. |
302
+ | `time` | String | timestamp ISO (zoned). |
303
+ | `result` | String[] | valores sorteados (1 por coluna). **Pode conter `null`** = NONE. |
304
+ | `status` | String | `"WIN"` / `"LOSE"`. **Ausente** se `win_chart` vazia. |
305
+ | `achievements` | `Achievement[]` | a jogada (`a1`, só em WIN) + recompensas creditadas (`a2`). |
306
+
307
+ Constantes: `STATUS_WIN = "WIN"`, `STATUS_LOSE = "LOSE"`.
308
+
309
+ ### 3.5 `MysteryBoxLog` — log de tentativa (coleção `mystery_box_log`)
310
+
311
+ | Campo | Tipo | Descrição |
312
+ |---|---|---|
313
+ | `_id` | String | = id do achievement `a1` da jogada. |
314
+ | `item` | String | = id da mystery box. |
315
+ | `player` | String | jogador. |
316
+ | `time` | Date | momento da tentativa. |
317
+ | `extra` | Map | metadado enviado no `execute` (POST). |
318
+
319
+ ### 3.6 `MysteryFolder` — pasta (coleção `mystery_folder`)
320
+
321
+ | Campo | Tipo | Descrição |
322
+ |---|---|---|
323
+ | `_id` | String | id da pasta (= valor de `MysteryBox.folder`). |
324
+ | `title` | String | título; no auto-create recebe o mesmo valor do id (L64-65). |
325
+
326
+ ### 3.7 Técnicas de jogo (`techniques`)
327
+
328
+ O campo `techniques` existe em `MysteryBox` mas, **diferente de point/challenge/level/leaderboard/lastmile/virtual_good/lottery/question/competition**, o `mystery_box` **não** está na rotina de atribuição de código GT do `GameTechniqueManager` (L63-181). **Não existe constante de GT code para mystery box** na lista (L67-75). O `GameTechniqueManager` apenas lê as boxes para montar o **grafo de relacionamentos** (nós = boxes, links = box → item de recompensa, L295-297 e L636-647) — nunca grava `techniques`. Portanto: **não há código GT operacional para mystery; o campo é legado.**
329
+
330
+ ---
331
+
332
+ ## 4. Endpoints
333
+
334
+ Todos sob `@Path("v3/mystery")`, `Produces: application/json; charset=UTF-8`. Autenticação via Bearer token (`AuthBean`).
335
+
336
+ ### `GET /v3/mystery/{id}` — buscar uma box
337
+
338
+ | Aspecto | Detalhe |
339
+ |---|---|
340
+ | Finalidade | Retorna a configuração da box (`find`, L59). |
341
+ | Autenticação | Bearer token |
342
+ | (!) id inexistente | Retorna **`200 OK` com corpo `null`** — **não** 404 (não há null-check). |
343
+
344
+ ### `GET /v3/mystery` — listar todas
345
+
346
+ Retorna todas as boxes (`findAll`, L76). Coleção vazia → `200` com `[]`. Sem paginação na API (o header `Range: items=0-1000` é aplicado globalmente no nível de infra).
347
+
348
+ ### `POST /v3/mystery` — criar/atualizar (upsert)
349
+
350
+ | Aspecto | Detalhe |
351
+ |---|---|
352
+ | Finalidade | `insert` (L208-213). |
353
+ | Full replace ou patch | **Full replace** (Jongo `save` por `_id`). Omitir campo o apaga. |
354
+ | Status | `201 Created`, devolve a box já com `_id`/`options` recalculados. |
355
+ | Efeitos colaterais | Recalcula `options`, atribui `_id` às combinações, cria pasta se ausente. |
356
+
357
+ ### `DELETE /v3/mystery/{id}` — remover box
358
+
359
+ `delete` (L94). Remove o documento de `mystery_box` (`remove {_id:#}`, L82). **Não** remove logs nem achievements relacionados. Status `200`.
360
+
361
+ ### `GET /v3/mystery/execute/{id}?player={player}` — jogar (GET)
362
+
363
+ | Aspecto | Detalhe |
364
+ |---|---|
365
+ | Finalidade | Executa a box e retorna `MysteryResult` (L229-321). |
366
+ | `player=me` | Resolve o jogador a partir do token (L234-235). |
367
+ | Validações (REST, antes do manager) | box existe (400 se não), `active` (400 se `false`), `limit` de vitórias (400 se excedido), `attempts` (400 se excedido). |
368
+
369
+ ### `POST /v3/mystery/execute` — jogar (POST, com `extra`)
370
+
371
+ Body: `{ "_id": "...", "player": "...", "extra": { ... } }` (L324-421). Permite anexar `extra` (gravado no log). `player=me` → token.
372
+
373
+ ### `POST /v3/mystery/execute_by_player/{player}` — jogar por jogador na URL
374
+
375
+ Body: `{ "_id": "...", "extra": { ... } }` (L424-521). O jogador vem do path param (não do body). Mesmas validações.
376
+
377
+ ### `DELETE /v3/mystery/execute` — rollback de jogada
378
+
379
+ | Aspecto | Detalhe |
380
+ |---|---|
381
+ | Finalidade | Desfaz uma jogada (`undoExecute`, L525-540 → L158-171). |
382
+ | Body | `{ "_id": "<achievementId da jogada>" }`. |
383
+ | Comportamento | Só age se o achievement for `type=6`. Remove os achievements de recompensa (`extra.origin == id`) **e** o achievement da jogada. |
384
+ | (!) Não faz | **Não** estorna `requirements` debitados; **não** remove o `mystery_box_log`. |
385
+ | Resposta | `200 {"message":"Execution Rollback Done"}` |
386
+
387
+ ### `GET /v3/mystery/evaluate/{id}?player={player}&time={time}` — diagnóstico
388
+
389
+ | Aspecto | Detalhe |
390
+ |---|---|
391
+ | Finalidade | Avalia elegibilidade sem jogar (`evaluate` → `evaluateAnalyze`, L543-571 / L524-563). |
392
+ | `time` | Aceita epoch ms, data ISO/zoned, keyword (ex. `-1d`) ou vazio (→ agora) (L556-560). |
393
+ | Retorno | `{ params, exists, limit_wins, limit_attempts, limit, attempts, requirements, active, milliseconds }`. |
394
+ | (!) Bug | A chave `active` é **sempre `{}`** e o status de `active` é gravado por engano em `exists.status` (L556-558). Box inativa faz `exists.status="UNAUTHORIZED"` mesmo existindo (ver §7/§9). |
395
+
396
+ #### Exemplo de execução (request/response)
397
+
398
+ ```
399
+ GET /v3/mystery/execute/heads_or_tails?player=ricardo@acme.com
400
+ Authorization: Bearer eyJhbGciOiJIUzUxMiIs...
401
+ ```
402
+
403
+ ```json
404
+ {
405
+ "mystery": "heads_or_tails",
406
+ "player": "ricardo@acme.com",
407
+ "time": "2026-05-20T13:40:11-03:00",
408
+ "result": ["heads"],
409
+ "status": "WIN",
410
+ "achievements": [
411
+ { "_id": "aZ8...", "player": "ricardo@acme.com", "total": 1, "type": 6, "item": "heads_or_tails", "time": "..." },
412
+ { "_id": "bY9...", "player": "ricardo@acme.com", "total": 10, "type": 0, "item": "coin", "time": "...",
413
+ "extra": { "origin": "aZ8...", "origin_item": "heads_or_tails", "origin_type": 6, "mystery_win_id": "o1" } }
414
+ ]
415
+ }
416
+ ```
417
+
418
+ ---
419
+
420
+ ## 5. Regras de Negócio
421
+
422
+ Regras que **existem no código** e não no schema:
423
+
424
+ 1. **`limit` = vitórias, `attempts` = tentativas.** `limit` conta `achievement type=6` (gravado só em WIN); `attempts` conta `mystery_box_log` (gravado em toda jogada). Ver §2.3.
425
+ 2. **`limit.total` pode ser fórmula.** Aceita número **ou** string Mustache+exp4j avaliada por jogador, ex.: `"{{player.extra.max_plays}} * 2"` (L255-264). Erro de avaliação → 400 ("It was impossible to identify the limit…").
426
+ 3. **Tentativa falha por requisito ainda gera log.** O `mystery_box_log` é gravado antes da verificação de `requirements` (L191-192 vs L195-202). Insuficiência lança 400 **depois** do log → conta como attempt.
427
+ 4. **`limit`/`reward.limit` com `per:"team"` não é aplicado.** `checkRewardIsInLimit` só trata `PER_PLAYER` e `PER_GAME` (L483-511); com `team`, nenhum branch roda, `count=0` e o limite **nunca** bloqueia (silenciosamente permissivo).
428
+ 5. **Só recompensas `type` 0/1/2 são creditadas.** Em vitória, o `a2` só persiste se `type` for `point(0)` → `point_category`, `challenge(1)` → `challenge`, `catalog_item(2)` → `catalog_item` (L366-390). Outros tipos: `a2` é criado em memória mas `collection==null` pula triggers e `addAchievement` — recompensa **silenciosamente não registrada**.
429
+ 6. **Combinação multiset.** `orderSensitive=false` casa por contagem de tipos (subconjunto), não por igualdade — uma jogada pode disparar **múltiplas** linhas vencedoras (§3.3).
430
+ 7. **NONE é implícito.** Se as probabilidades não somam 1 (e não há opção zero para absorver), há chance de `result` conter `null` (sem prêmio) — §2.4.
431
+ 8. **Foot-gun de probabilidade > 1.** Se a soma das probabilidades informadas exceder 1, `prepareOptions` gera probabilidade **negativa** para opções zero, corrompendo o sorteio (`getChoice` pode nunca retornar certas opções / retornar `null` quase sempre). Não há validação que impeça isso. Ver §2.2.
432
+ 9. **`inventory` é cap global** por `reward.type+item` (ou isolado por box / box+combinação se `isolated`). Conta `achievement` (L421-449).
433
+ 10. **`updatePlayerStatus` sempre roda** ao final do execute (mesmo em LOSE / sem win_chart), desde que box e player existam (L414).
434
+ 11. **Multi-tenant:** isolamento por API key resolvido no `FrontController.getInstance(apiKey)`; cada tenant tem seu `ManagerFactory`/conexão Jongo. O `synchronized` do `execute` é por instância (tenant), **não** distribuído.
435
+
436
+ ---
437
+
438
+ ## 6. Comportamentos Automáticos
439
+
440
+ | Comportamento | Trigger/Origem | Impacto | Persistência |
441
+ |---|---|---|---|
442
+ | Gera `_id` da box | `insert` (L42-44) | id curto se ausente | `mystery_box` |
443
+ | `active=true` default | `insert` (L46-48) | box nasce ativa | `mystery_box` |
444
+ | Recalcula `options` | `prepareOptions` (L51) | probabilidades normalizadas, opções inválidas removidas, ordem alterada | `mystery_box` |
445
+ | Atribui `_id` às combinações | `prepareWinchart` (L54) | `o1`,`o2`,… | `mystery_box` |
446
+ | Cria pasta default | `insert` (L57-67) | doc em `mystery_folder` | `mystery_folder` |
447
+ | Log de tentativa | `execute` (L191-192) | toda jogada registrada | `mystery_box_log` |
448
+ | Débito de `requirements` | `execute` (L195-202) | custo deduzido (achievements negativos) | `achievement` |
449
+ | Achievement da jogada (`a1`) | `execute` em WIN (L330) | conta para `limit` | `achievement` (`type=6`) |
450
+ | Achievement da recompensa (`a2`) | `execute` por combinação (L386) | crédito ao jogador | `achievement` (point/challenge/catalog_item) |
451
+ | `updatePlayerStatus` | `execute` (L414) | recalcula status do jogador | `player_status` |
452
+ | Triggers | `execute` | ver fluxo abaixo | conforme handler |
453
+
454
+ ### Encadeamento de triggers numa vitória com recompensa
455
+
456
+ Eventos disparados, **nesta ordem** (L320-409):
457
+
458
+ 1. `before_win` — entidade `mystery_box` (a jogada `a1`).
459
+ 2. `addAchievement(a1)` — persiste a jogada.
460
+ 3. Para **cada** combinação vencedora que casa:
461
+ 1. `before_win_reward` — entidade `mystery_box` ((!) **string literal hardcoded** em L376, **não** é constante `Trigger.EVENT_*`; grep por constante não acha).
462
+ 2. `before_win` — entidade = **coleção da recompensa** (`point_category`/`challenge`/`catalog_item`).
463
+ 3. `addAchievement(a2)` — persiste a recompensa.
464
+ 4. `after_win` — entidade = coleção da recompensa (**após** persistir).
465
+ 4. `after_win` — entidade `mystery_box`.
466
+
467
+ Em derrota: `before_lose` (antes do loop, que não credita nada) e `after_lose` (no fim) — entidade `mystery_box`.
468
+
469
+ ```mermaid
470
+ flowchart LR
471
+ W{WIN?} -- sim --> BW["before_win (mystery_box)"]
472
+ BW --> AA["addAchievement(a1)"]
473
+ AA --> LP["loop win_chart casadas"]
474
+ LP --> BWR["before_win_reward<br/>(literal, mystery_box)"]
475
+ BWR --> BWC["before_win<br/>(colecao da recompensa)"]
476
+ BWC --> AA2["addAchievement(a2)"]
477
+ AA2 --> AWC["after_win<br/>(colecao da recompensa)"]
478
+ AWC --> AW["after_win (mystery_box)"]
479
+ W -- nao --> BL["before_lose (mystery_box)"]
480
+ BL --> AL["after_lose (mystery_box)"]
481
+ ```
482
+
483
+ ---
484
+
485
+ ## 7. Suportado vs NÃO Suportado
486
+
487
+ ### ✅ Suportado
488
+
489
+ - CRUD de mystery box (`GET`/`GET all`/`POST` upsert/`DELETE`).
490
+ - Execução por GET, POST (com `extra`) e `execute_by_player`.
491
+ - Sorteio ponderado por probabilidade, multi-coluna (`columns`).
492
+ - `win_chart` com combinação ordenada (`orderSensitive=true`) ou por contagem (`false`).
493
+ - Recompensas `point` (0), `challenge` (1), `catalog_item` (2).
494
+ - `limit` (vitórias) e `attempts` (jogadas) por `player` e `gamification`, com janela `every` e `total` numérico **ou** fórmula.
495
+ - `reward.limit` e `reward.inventory`, com isolamento por box/combinação (`isolated`).
496
+ - Exclusão dinâmica de opções com recompensa esgotada (somente `columns==1`).
497
+ - Rollback de jogada (`DELETE /v3/mystery/execute`).
498
+ - Diagnóstico (`GET /v3/mystery/evaluate/{id}`).
499
+ - Pastas (`mystery_folder`) com auto-criação.
500
+ - Triggers `before_win`/`after_win`/`before_lose`/`after_lose` + `before_win_reward`.
501
+ - Geração assistida por IA (`POST /v3/ai/build/mystery`) — **não persiste**, só devolve o JSON.
502
+
503
+ ### ❌ NÃO Suportado / Pegadinhas
504
+
505
+ - **`type`** (`option_probability`/`combination_probability`) — não existe na entidade; **descartado** silenciosamente (`@JsonIgnoreProperties`).
506
+ - **`probability` por combinação** em `win_chart` — não existe em `MysteryWin`; **descartado**. Odds vêm de `options`×`columns`.
507
+ - **`reward.operation`** — ignorado; só `total/type/item` são usados (comentário L21 de `MysteryWin`).
508
+ - **Recompensa com `type` ≠ 0/1/2** (level, crown, lottery, etc.) — `a2` criado mas **nunca persistido** (collection null).
509
+ - **`limit.per = "team"`** em `limit` da box e em `reward.limit` — branch inexistente; limite **nunca aplicado** (permissivo). `evaluateLimitAnalyze`/`evaluateAttemptsAnalyze` também só tratam `player`/`gamification`.
510
+ - **`Limit.query`** — campo existe no POJO `Limit` mas **nunca é consumido** por nenhuma verificação de mystery.
511
+ - **`MysteryBox.extra`** — persistido, mas **nunca lido** pela lógica do módulo (passthrough).
512
+ - **`MysteryBox.techniques`** — legado; nunca escrito/lido pelo módulo; sem GT code (§3.7).
513
+ - **`MysteryBoxFolder.java`** — classe **morta**, duplicata de `MysteryFolder`, sem referências.
514
+ - **`PUT`** — inexistente; atualização é re-`POST` (full replace).
515
+ - **404 em id inexistente** — `GET /{id}` devolve `200` com `null`.
516
+ - **Proteção de recompensa esgotada para `columns > 1`** — não existe (só `columns==1`).
517
+ - **Validação de probabilidade > 1** — inexistente; gera probabilidade negativa (foot-gun, §2.2/§5).
518
+ - **Estorno de `requirements` no rollback** — `undoExecute` não devolve o custo nem apaga o log.
519
+ - **Bug no `evaluate`**: chave `active` sempre `{}`; status de `active` sobrescreve `exists.status` (L556-558).
520
+ - **Copy-paste em `evaluateAttemptsAnalyze`**: mensagem de erro referencia `a.limit.total` em vez de `a.attempts.total` (L674) — NPE se `limit==null` e `attempts.total` for string malformada.
521
+ - **Scheduler/job automático** — não existe para mystery (toda execução é via endpoint).
522
+
523
+ ---
524
+
525
+ ## 8. Segurança e Permissões
526
+
527
+ - **Autenticação:** Bearer token em todos os endpoints (`@BeanParam AuthBean`). Operações de CRUD/`delete` exigem credenciais de administrador (token do jogo); `execute` pode usar `player=me` resolvendo o jogador pelo token.
528
+ - **Isolamento multi-tenant:** resolvido por `FrontController.getInstance(apiKey)` → `ManagerFactory`/conexão Jongo por tenant. Não há vazamento entre organizações desde que a API key esteja correta.
529
+ - **Concorrência:** `execute` é `synchronized` na instância do manager (por tenant/JVM). **Não** há lock distribuído — sob múltiplos pods, dois `execute` simultâneos podem ultrapassar `limit`/`inventory` (condição de corrida em estoques pequenos). Mitigação parcial: o `limit` é re-checado tanto no REST quanto no manager, mas sem atomicidade.
530
+ - **Injeção:** as queries MongoDB usam **parâmetros posicionais** do Jongo (`"{_id:#}"`, `count("{type:#,...}", ...)`), o que evita injeção via valores. Porém **`limit.total`/`attempts.total` como fórmula** são avaliados por `MustacheUtils.parse` + `exp4j` — uma fórmula maliciosa configurada por um admin pode lançar exceção (tratada → 400) ou consumir CPU; o conteúdo é definido por administrador, não pelo jogador final, então a superfície é limitada a quem já tem acesso de configuração.
531
+ - **Comportamento permissivo inseguro documentado:** `reward.limit.per="team"` **não bloqueia nada** (§5/§7) — não confie nele para conter distribuição de prêmios.
532
+
533
+ ---
534
+
535
+ ## 9. Observabilidade e Troubleshooting
536
+
537
+ ### Diagnóstico rápido
538
+
539
+ ```
540
+ GET /v3/mystery/{id} # a box existe? (200 null = não existe)
541
+ GET /v3/mystery/evaluate/{id}?player=ME # elegibilidade: limit_wins, limit_attempts, requirements
542
+ ```
543
+
544
+ > Ao ler o `evaluate`, lembre-se do bug do `active` (§4/§7): a chave `active` vem vazia e, se a box estiver inativa, `exists.status` aparecerá como `UNAUTHORIZED`.
545
+
546
+ ### Queries MongoDB úteis
547
+
548
+ ```js
549
+ // jogadas (tentativas) de um jogador numa box
550
+ db.mystery_box_log.find({ item: "<boxId>", player: "<player>" }).sort({ time: -1 })
551
+
552
+ // vitórias contam aqui (type=6)
553
+ db.achievement.count({ type: 6, item: "<boxId>", player: "<player>" })
554
+
555
+ // recompensas creditadas por esta box
556
+ db.achievement.find({ "extra.origin_item": "<boxId>" })
557
+
558
+ // recompensas de uma combinação específica
559
+ db.achievement.find({ "extra.origin_item": "<boxId>", "extra.mystery_win_id": "o1" })
560
+
561
+ // estoque já distribuído de uma recompensa (inventory)
562
+ db.achievement.count({ type: 0, item: "coin" })
563
+ ```
564
+
565
+ ### Erros comuns e causas
566
+
567
+ | Sintoma | Causa provável |
568
+ |---|---|
569
+ | `400 insufficient requirements` | Jogador não tem o custo (`requirements`) — mas o `mystery_box_log` já foi gravado. |
570
+ | `400 You have exced the limit … earned` | `limit` (vitórias) atingido no período `every`. |
571
+ | `400 You have exced the limit … executed` | `attempts` (jogadas) atingido. |
572
+ | `400 mysterybox is not active` | `active=false` explícito. |
573
+ | `400 mysterybox does not exist` | id inválido no `execute` (no `GET /{id}` retornaria 200/null). |
574
+ | Prêmio não creditado mesmo "ganhando" | `reward.type` ∉ {0,1,2}; ou `limit`/`inventory` da recompensa esgotado (log no console: `"MysteryWin not registered because of limit"`). |
575
+ | Limite "não funciona" | `reward.limit.per="team"` (não suportado); ou soma de probabilidades > 1 corrompendo o sorteio. |
576
+ | Opção nunca sai / `result` quase sempre `null` | Probabilidades somam > 1 → probabilidade negativa em opções zero (§2.2). |
577
+ | Resultado contém `null` | Probabilidades somam < 1 → faixa NONE (esperado, §2.4). |
578
+
579
+ ### O que verificar quando "não funciona"
580
+
581
+ 1. A box está `active`? (`GET /{id}` → campo `active`, lembrando que `null` = ativa).
582
+ 2. As probabilidades das `options` somam ≤ 1? Há opções `>1`/`<0` que foram descartadas no save?
583
+ 3. A combinação da `win_chart` realmente casa com o `result`? Confira `orderSensitive`.
584
+ 4. O `reward.type` é 0/1/2? Caso contrário não é creditado.
585
+ 5. `limit`/`inventory` da recompensa esgotou? Veja o console (`MysteryWin not registered because of limit`).
586
+
587
+ ---
588
+
589
+ ## 10. Exemplos Práticos
590
+
591
+ ### 10.1 Mínimo funcional — cara ou coroa
35
592
 
36
- **Exemplo de Body:**
37
593
  ```json
594
+ POST /v3/mystery
38
595
  {
596
+ "_id": "heads_or_tails",
39
597
  "title": "Heads or Tails",
40
598
  "options": [
41
599
  { "title": "Heads", "value": "heads", "probability": 0.5 },
42
600
  { "title": "Tails", "value": "tails", "probability": 0.5 }
43
601
  ],
44
602
  "columns": 1,
45
- "requirements": [],
46
603
  "win_chart": [
47
- {
48
- "combination": ["heads"],
49
- "orderSensitive": false,
50
- "reward": {
51
- "total": 1,
52
- "type": 0,
53
- "item": "coin"
54
- }
55
- }
604
+ { "combination": ["heads"], "orderSensitive": false,
605
+ "reward": { "total": 1, "type": 0, "item": "coin" } }
606
+ ]
607
+ }
608
+ ```
609
+
610
+ Jogar: `GET /v3/mystery/execute/heads_or_tails?player=me`.
611
+
612
+ ### 10.2 Avançado — slot 3 colunas, custo, limites e estoque
613
+
614
+ ```json
615
+ POST /v3/mystery
616
+ {
617
+ "_id": "office_prizes",
618
+ "title": "Office Prizes",
619
+ "options": [
620
+ { "title": "Notebook", "value": "notebook", "probability": 0.1 },
621
+ { "title": "Backpack", "value": "backpack", "probability": 0.3 },
622
+ { "title": "Mouse", "value": "mouse", "probability": 0.6 }
623
+ ],
624
+ "columns": 3,
625
+ "requirements": [
626
+ { "total": 4, "type": 0, "item": "coin", "operation": 1 }
56
627
  ],
57
- "techniques": ["GT72"],
58
- "_id": "64a5b464d8dcca49bcf7edd0"
628
+ "win_chart": [
629
+ { "combination": ["notebook","notebook","notebook"], "orderSensitive": false,
630
+ "reward": { "total": 1, "type": 2, "item": "notebook" },
631
+ "inventory": 5, "isolated": true,
632
+ "limit": { "total": 1, "per": "player", "every": "1M" } },
633
+ { "combination": ["mouse","mouse","mouse"], "orderSensitive": false,
634
+ "reward": { "total": 1, "type": 2, "item": "mouse" } }
635
+ ],
636
+ "limit": { "total": 1, "per": "player", "every": "1M" },
637
+ "attempts": { "total": 1, "per": "player", "every": "1d" }
638
+ }
639
+ ```
640
+
641
+ - `requirements`: custa 4 `coin` por jogada (`operation:1` = deduct).
642
+ - `limit`: máx. **1 vitória** por jogador por mês.
643
+ - `attempts`: máx. **1 jogada** por jogador por dia.
644
+ - `inventory: 5` + `isolated`: só 5 notebooks distribuídos por esta box.
645
+
646
+ ### 10.3 Anti-pattern — o que NÃO fazer
647
+
648
+ ```json
649
+ {
650
+ "_id": "broken_box",
651
+ "title": "Broken",
652
+ "type": "option_probability", // (X) ignorado (não existe na entidade)
653
+ "options": [
654
+ { "value": "a", "probability": 0.7 },
655
+ { "value": "b", "probability": 0.6 } // (X) soma 1.3 > 1 -> prob. negativa, sorteio corrompido
656
+ ],
657
+ "columns": 3,
658
+ "win_chart": [
659
+ { "combination": ["a","a","a"],
660
+ "probability": 0.01, // (X) ignorado (não existe em MysteryWin)
661
+ "reward": { "total": 1, "type": 3, "item": "lvl" }, // (X) type=3 (level) nunca é creditado
662
+ "limit": { "total": 5, "per": "team" } // (X) per=team não bloqueia nada
663
+ }
664
+ ]
59
665
  }
60
666
  ```
61
667
 
62
- ### Deletar Mystery Box
63
- **Método:** DELETE
64
- **Endpoint:** `/v3/mystery/:id`
668
+ Por que é errado:
65
669
 
66
- ### Avaliar Condições
67
- **Método:** GET
68
- **Endpoint:** `/v3/mystery/evaluate/:id?player=:player_id`
69
- **Descrição:** Verifica se o jogador está apto a executar a Mystery Box.
670
+ - `type` no topo e `probability` na combinação são **descartados** silenciosamente.
671
+ - Probabilidades das `options` somam **> 1** → `prepareOptions` quebra o sorteio.
672
+ - `reward.type=3` não está em {0,1,2} → o prêmio **nunca** é registrado.
673
+ - `limit.per="team"` é **silenciosamente ignorado** sem limite real.
674
+ - Sem `_id` nas combinações, mas isso é OK (o sistema gera `o1`,`o2`…) — só não dependa dele antes do primeiro save.
70
675
 
71
- ### Executar Mystery Box
72
- **Método:** GET
73
- **Endpoint:** `/v3/mystery/execute/:id?player=:player_id`
74
- **Descrição:** Executa o sorteio e retorna o resultado.
676
+ ---
75
677
 
76
- ## Validações e Testes
678
+ ## Checklist de Configuração
77
679
 
78
- - [ ] Mystery Box aparece na lista
79
- - [ ] Soma das probabilidades é 1.0
80
- - [ ] Jogador consegue executar a mystery box
81
- - [ ] Combinação vencedora entrega recompensa corretamente
82
- - [ ] Requisitos são validados antes da execução
680
+ - [ ] `title` preenchido.
681
+ - [ ] `options` com `value` único por opção; probabilidades somando **≤ 1** (evite `> 1` → quebra o sorteio).
682
+ - [ ] Nenhuma opção com `probability > 1` ou `< 0` (seriam **descartadas** no save).
683
+ - [ ] `columns` definido conforme o jogo (1 = roleta simples; >1 = slot).
684
+ - [ ] `win_chart[].combination` usa exatamente os `value`s das `options`.
685
+ - [ ] `orderSensitive` definido conscientemente (`false` = contagem por tipo, pode casar várias linhas).
686
+ - [ ] `reward.type` ∈ {0 (point), 1 (challenge), 2 (catalog_item)} — outros tipos **não** são creditados.
687
+ - [ ] Itens/pontos/desafios de `reward.item` e `requirements.item` **existem** antes de criar a box.
688
+ - [ ] Para limites por jogador/jogo use `per: "player"` ou `"gamification"` — **`team` não funciona**.
689
+ - [ ] Diferencie `limit` (vitórias) de `attempts` (jogadas): se quer limitar quantas vezes pode jogar, use `attempts`.
690
+ - [ ] Ciente de que `extra`, `techniques` e `type` no payload são metadados/ignorados.
691
+ - [ ] Ciente de que `POST` é **full replace** — reenvie o documento completo ao atualizar.
692
+ - [ ] Ciente de que rollback (`DELETE /execute`) **não** estorna `requirements`.