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
@@ -0,0 +1,769 @@
1
+ # funifier-create-mystery
2
+
3
+ Create a Funifier mystery box — probability-based instant games (spin wheel, scratch card, coin flip) with configurable odds and rewards; use for random reward mechanics, not for ticket-based draws with scheduled dates (use lottery)
4
+
5
+ ---
6
+
7
+ ## Documentação do módulo (já incluída — não precisa buscar)
8
+
9
+ **Acesso Studio:** `/studio/mystery`
10
+ **API Endpoint:** `/v3/mystery`
11
+ **Coleções MongoDB:** `mystery_box`, `mystery_box_log`, `mystery_folder` (as vitórias e recompensas ficam em `achievement` com `type=6`)
12
+
13
+ > 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.
14
+
15
+ ---
16
+
17
+ ## 1. Visão Geral
18
+
19
+ 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.
20
+
21
+ Papel arquitetural:
22
+
23
+ - 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).
24
+ - **Não há 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).
25
+ - Toda execução é registrada na coleção `mystery_box_log` (auditoria de tentativas / "attempts").
26
+ - 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).
27
+
28
+ Relação com outros módulos:
29
+
30
+ - [achievement](achievement.md) — a jogada vira `Achievement type=6` (somente em vitória); cada recompensa vira um achievement adicional (ponto/desafio/item).
31
+ - [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.
32
+ - [challenge](challenge.md) (`Requirement`) — define tanto o **custo de jogar** (`requirements`) quanto a **recompensa** de cada combinação (`win_chart[].reward`).
33
+ - `point_category`, `catalog_item` (virtual good) — tipos de recompensa suportados (além de `challenge`).
34
+ - [folder](folder.md) — agrupamento via coleção `mystery_folder`.
35
+ - `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.
36
+
37
+ ---
38
+
39
+ ## 2. Arquitetura e Fluxos
40
+
41
+ ### 2.1 Classes envolvidas
42
+
43
+ | Classe | Papel |
44
+ |---|---|
45
+ | `com.funifier.engine.mystery.MysteryBox` | Entidade/POJO raiz — documento em `mystery_box` (`MysteryBox.java`, L24-89) |
46
+ | `com.funifier.engine.mystery.MysteryOption` | Uma opção sorteável (valor + probabilidade) (`MysteryOption.java`) |
47
+ | `com.funifier.engine.mystery.MysteryWin` | Linha da `win_chart` (combinação vencedora + recompensa + limites) (`MysteryWin.java`) |
48
+ | `com.funifier.engine.mystery.MysteryResult` | DTO de saída do `execute` — **não persistido** (`MysteryResult.java`) |
49
+ | `com.funifier.engine.mystery.MysteryBoxLog` | Log de tentativa — documento em `mystery_box_log` (`MysteryBoxLog.java`) |
50
+ | `com.funifier.engine.mystery.MysteryFolder` | Pasta de agrupamento — documento em `mystery_folder` |
51
+ | `com.funifier.engine.mystery.MysteryBoxFolder` | **Classe morta** — duplicata de `MysteryFolder`, sem nenhuma referência no código (ver §7) |
52
+ | `com.funifier.engine.mystery.ProbabilityUtil` | Sorteio ponderado por probabilidade (`getChoice`) (`ProbabilityUtil.java`) |
53
+ | `com.funifier.engine.mystery.MysteryBoxManager` | Manager monolítico: CRUD, execução, limites, rollback, evaluate (`MysteryBoxManager.java`, L31-754) |
54
+ | `com.funifier.rest.v3.rest.MysteryBoxRest` | Controller REST v3 (`/v3/mystery`) (`MysteryBoxRest.java`, L44-572) |
55
+
56
+ ### 2.2 Pipeline de criação — `insert(MysteryBox)` (L39-70)
57
+
58
+ `POST /v3/mystery` → `MysteryBoxManager.insert`. Comportamento de **upsert** (Jongo `c.save` grava por `_id`; reenviar o mesmo `_id` **substitui o documento inteiro**).
59
+
60
+ ```
61
+ [1] Se _id ausente/vazio → gera Guid.newShortGuid() (L42-44)
62
+ [2] Se active == null → active = true (L46-48)
63
+ [3] prepareOptions(mystery) — recalcula probabilidades das opções (L51 → L117-146)
64
+ [4] prepareWinchart(mystery) — atribui _id "o1","o2",... às combinações (L54 → L85-111)
65
+ [5] Se folder vazio → folder = "default" (L57-59)
66
+ [6] Garante que o documento da pasta exista em mystery_folder;
67
+ se não existir, cria MysteryFolder {id=folder, title=folder} (L60-67)
68
+ [7] c.save(mystery) na coleção mystery_box (L69)
69
+ ```
70
+
71
+ > **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.
72
+
73
+ #### `prepareOptions` — recálculo de probabilidades (L117-146)
74
+
75
+ 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).
76
+
77
+ ```
78
+ para cada opção o:
79
+ se o.probability == null OU == 0 → vai para zeroOptions
80
+ senão se 0 < o.probability <= 1 → vai para goodOptions; goodTotal += o.probability
81
+ senão (prob > 1 OU prob < 0) → DESCARTADA silenciosamente (!)
82
+
83
+ diffTotal = 1 - goodTotal
84
+ division = diffTotal / zeroOptions.size()
85
+ para cada opção em zeroOptions:
86
+ o.probability = division (divide o "resto" igualmente entre as opções sem prob.)
87
+
88
+ mystery.options = goodOptions ++ (zeroOptions já ajustadas) → ORDEM É ALTERADA (!)
89
+ ```
90
+
91
+ Regras emergentes (não documentadas no schema):
92
+
93
+ - Opções com `probability > 1` ou `< 0` são **removidas silenciosamente** do documento salvo.
94
+ - Opções sem probabilidade (`null`/`0`) recebem partes iguais do "resto" (`1 - soma das demais`).
95
+ - 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).
96
+ - **Foot-gun:** se `goodTotal > 1`, `diffTotal` fica **negativo** e as opções "zero" recebem probabilidade **negativa**, corrompendo `ProbabilityUtil.getChoice()` (ver §5).
97
+ - 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.
98
+
99
+ ### 2.3 Pipeline principal — `execute(id, player[, extra])` (L173-419)
100
+
101
+ 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).
102
+
103
+ 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:
104
+
105
+ ```
106
+ [1] Re-carrega MysteryBox por id; carrega Player (L178-181)
107
+ Se mystery == null OU player == null → não faz nada, retorna result vazio (L184)
108
+ [2] Cria a1 = Achievement(novoGuid, player, total=1, TYPE_MYSTERY_BOX=6, item=id, now) (L187)
109
+ [3] SALVA MysteryBoxLog(a1.id, item=id, player, time, extra) em mystery_box_log (L191-192) (!) SEMPRE
110
+ [4] Se há requirements: evaluateRequirements; se OK → deductRequirements,
111
+ senão → throw "insufficient requirements" (HTTP 400) (L195-202)
112
+ (!) O log do passo [3] já foi gravado mesmo se falhar aqui.
113
+ [5] columns = (mystery.columns > 0) ? mystery.columns : 1 (L206)
114
+ [6] [Apenas se columns == 1] Exclusão de opções com recompensa esgotada (L213-283) — ver §2.5
115
+ [7] Monta ProbabilityUtil (opções viáveis OU originais) e sorteia
116
+ `columns` valores → lista `win` (pode conter null = NONE) (L286-298)
117
+ [8] result = {mystery:id, player, time, result:win} (L301-304)
118
+ [9] SE win_chart não-vazia (L307):
119
+ status = containsWinCombination(win) ? WIN : LOSE (L313-317)
120
+ SE WIN → result.achievements.add(a1);
121
+ trigger before_win (mystery_box);
122
+ addAchievement(a1) <- a1 só persiste em VITÓRIA (!) (L320-331)
123
+ SE LOSE → trigger before_lose (mystery_box) (L333-337)
124
+ Para cada win_chart[i] que casa com `win` (L340-397):
125
+ checkRewardIsInLimit && checkInventoryIsInLimit (L345-348)
126
+ se reward != null E disponível → cria a2, dispara triggers,
127
+ addAchievement(a2) (só p/ reward type 0/1/2) (L351-390)
128
+ senão → System.out.println("MysteryWin not registered because of limit") (L393)
129
+ SE WIN → trigger after_win (mystery_box) (L400-403)
130
+ SE LOSE → trigger after_lose (mystery_box) (L407-409)
131
+ [10] updatePlayerStatus(player) — SEMPRE que mystery!=null && player!=null (L414)
132
+ ```
133
+
134
+ > **Distinção crítica entre `limit` e `attempts`:** o achievement `a1` (`type=6`) só é gravado em **vitória** (passo [9], L330). Logo:
135
+ > - **`limit`** conta documentos `achievement type=6` → é um limite de **VITÓRIAS**.
136
+ > - **`attempts`** conta documentos `mystery_box_log` → é um limite de **TENTATIVAS/jogadas** (gravado sempre, passo [3]).
137
+ >
138
+ > Uma derrota **não** incrementa `limit`, mas **incrementa** `attempts`.
139
+
140
+ > **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.
141
+
142
+ ### Fluxo de execução — `execute(id, player)`
143
+
144
+ ```mermaid
145
+ flowchart TB
146
+ A["REST: validacoes<br/>(existe? active? limit? attempts?)"] --> B["Manager.execute<br/>(synchronized)"]
147
+ B --> C{"mystery != null<br/>&& player != null?"}
148
+ C -- nao --> Zr["retorna result vazio"]
149
+ C -- sim --> D["cria a1 (Achievement type=6)"]
150
+ D --> E["salva MysteryBoxLog (SEMPRE)"]
151
+ E --> F{requirements OK?}
152
+ F -- nao --> Z["throw 400<br/>(log ja persistido)"]
153
+ F -- sim --> G["deductRequirements"]
154
+ G --> H{columns == 1?}
155
+ H -- sim --> I["exclui opcoes c/ recompensa<br/>esgotada e renormaliza"]
156
+ H -- nao --> J["usa opcoes originais"]
157
+ I --> K["sorteia colunas<br/>(ProbabilityUtil)"]
158
+ J --> K
159
+ K --> L{win_chart vazia?}
160
+ L -- sim --> P["sem status, sem trigger"]
161
+ L -- nao --> M{containsWinCombination?}
162
+ M -- sim --> N["status=WIN<br/>before_win + addAchievement(a1)"]
163
+ M -- nao --> O["status=LOSE<br/>before_lose"]
164
+ N --> Q["loop win_chart: rewards (sec.6)"]
165
+ O --> Q
166
+ Q --> R{WIN?}
167
+ R -- sim --> S["after_win (mystery_box)"]
168
+ R -- nao --> T["after_lose (mystery_box)"]
169
+ P --> U["updatePlayerStatus (SEMPRE)"]
170
+ S --> U
171
+ T --> U
172
+ ```
173
+
174
+ ### Interação entre módulos — vitória com recompensa
175
+
176
+ ```mermaid
177
+ sequenceDiagram
178
+ participant C as Cliente
179
+ participant R as MysteryBoxRest
180
+ participant M as MysteryBoxManager
181
+ participant Tr as TriggerManager
182
+ participant A as AchievementManager
183
+ participant DB as MongoDB
184
+
185
+ C->>R: GET /v3/mystery/execute/{id}?player=X
186
+ R->>R: valida active / limit (wins) / attempts (logs)
187
+ R->>M: execute(id, player)
188
+ M->>DB: save mystery_box_log (tentativa)
189
+ M->>A: evaluate+deduct requirements
190
+ M->>M: sorteia colunas (ProbabilityUtil)
191
+ M->>Tr: before_win (mystery_box)
192
+ M->>A: addAchievement(a1 type=6) [so em WIN]
193
+ loop cada win_chart que casa
194
+ M->>M: checkRewardIsInLimit + checkInventoryIsInLimit
195
+ M->>Tr: before_win_reward (mystery_box)
196
+ M->>Tr: before_win (colecao da recompensa)
197
+ M->>A: addAchievement(a2)
198
+ M->>Tr: after_win (colecao da recompensa)
199
+ end
200
+ M->>Tr: after_win (mystery_box)
201
+ M->>A: updatePlayerStatus(player)
202
+ M-->>R: MysteryResult
203
+ R-->>C: 200 {result, status, achievements}
204
+ ```
205
+
206
+ ### 2.4 Sorteio — `ProbabilityUtil.getChoice()` (L95-104)
207
+
208
+ ```
209
+ rand = Math.random() // [0,1)
210
+ para cada item (probs):
211
+ se rand < prob(item) → retorna item
212
+ rand -= prob(item)
213
+ retorna null // "resto" não coberto = NONE
214
+ ```
215
+
216
+ - A iteração segue a ordem do `HashMap` (não determinística entre opções) — não é estável, mas é estatisticamente correta.
217
+ - 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.
218
+
219
+ ### 2.5 Exclusão de opções esgotadas (apenas `columns == 1`) (L213-283)
220
+
221
+ 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):
222
+
223
+ ```
224
+ para cada opção viável o:
225
+ novaProb(o) = o.probability + (o.probability / somaViáveis) * somaIgnoradas
226
+ ```
227
+
228
+ 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).
229
+
230
+ ---
231
+
232
+ ## 3. Estrutura dos Objetos
233
+
234
+ ### 3.1 `MysteryBox` — documento raiz (coleção `mystery_box`)
235
+
236
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
237
+ |---|---|---|---|---|
238
+ | `_id` | String | `Guid.newShortGuid()` | — | Gerado se ausente/vazio (L42-44). Reenviar o mesmo `_id` substitui o documento. |
239
+ | `title` | String | — | sim (de fato) | Título exibido. Não há validação de obrigatoriedade no backend, mas é esperado pela UI. |
240
+ | `image` | `Image` (objeto) | `null` | não | Imagem de capa. |
241
+ | `options` | `MysteryOption[]` | `[]` | sim | Opções sorteáveis. Reescritas por `prepareOptions` no save (ver §2.2). |
242
+ | `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). |
243
+ | `requirements` | `Requirement[]` | `[]` | não | **Custo** para jogar (ex.: 1 moeda). Verificados e debitados no `execute` (L195-202). |
244
+ | `win_chart` | `MysteryWin[]` | `[]` | não | Combinações vencedoras e recompensas. Sem `win_chart`, nunca há vitória nem trigger (ver §2.3). |
245
+ | `limit` | `Limit` | `null` | não | Limite de **vitórias** (conta `achievement type=6`). |
246
+ | `attempts` | `Limit` | `null` | não | Limite de **tentativas** (conta `mystery_box_log`). |
247
+ | `folder` | String | `"default"` | não | Pasta de agrupamento. Cria o doc em `mystery_folder` se não existir (L60-67). |
248
+ | `active` | Boolean | `true` (no insert) | não | **Só `false` explícito bloqueia** o execute: `!(active==null \|\| active==true)` (L244). `null`/ausente = ativo. |
249
+ | `extra` | Map | `{}` | não | Metadado livre. **Persistido mas nunca lido** pela lógica do módulo mystery — passthrough puro (ver §7). |
250
+ | `techniques` | String[] | `null` | não | **Legado/inativo.** Nunca escrito nem lido pelo módulo (ver §3.7 e §7). |
251
+
252
+ #### Campos aceitos no JSON mas **silenciosamente ignorados**
253
+
254
+ `MysteryBox` é anotado com `@JsonIgnoreProperties(ignoreUnknown=true)`. Logo, qualquer campo não mapeado é descartado **sem erro**. Em particular:
255
+
256
+ - **`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".
257
+
258
+ #### Métodos de domínio em `MysteryBox`
259
+
260
+ - `containsWinCombination(result)` (L67-76) — `true` se **alguma** linha de `win_chart` casa com o resultado.
261
+ - `getOption(value)` (L78-88) — localiza a `MysteryOption` pelo `value`.
262
+
263
+ ### 3.2 `MysteryOption` — uma opção sorteável
264
+
265
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
266
+ |---|---|---|---|---|
267
+ | `value` | String | — | sim | Identificador da opção (usado nas combinações da `win_chart`). |
268
+ | `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). |
269
+ | `title` | String | `null` | não | Rótulo exibível. |
270
+ | `image` | String | `null` | não | URL da imagem da opção. |
271
+
272
+ ### 3.3 `MysteryWin` — linha da `win_chart`
273
+
274
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
275
+ |---|---|---|---|---|
276
+ | `_id` | String | `"o1"`,`"o2"`,… | — | Atribuído por `prepareWinchart` se ausente (L96-108). Usado no escopo de `limit`/`inventory` isolados. |
277
+ | `combination` | String[] | — | sim | Lista de `value`s que formam a combinação vencedora. |
278
+ | `orderSensitive` | boolean | `false` | não | `true` = igualdade exata (ordem importa); `false` = **contagem por tipo** (multiset/subconjunto) — ver §5. |
279
+ | `reward` | `Requirement` | `null` | não | Recompensa. **Usa apenas `total`, `type`, `item`** (o `operation` é ignorado — comentário L21). |
280
+ | `limit` | `Limit` | `null` | não | Limite de quantas vezes a recompensa pode ser obtida. **Só `player` e `gamification`** (ver §5/§7). |
281
+ | `inventory` | Double | `null` | não | Estoque global da recompensa (cap absoluto de unidades distribuídas). |
282
+ | `isolated` | boolean | `false` | não | Se `true`, `limit`/`inventory` contam apenas conquistas **desta** mystery box (e, com `_id`, desta combinação). |
283
+
284
+ #### Campo aceito mas ignorado
285
+
286
+ - **`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.
287
+
288
+ #### `isWinCombination(result)` (L36-78)
289
+
290
+ ```
291
+ se orderSensitive:
292
+ retorna String(combination) == String(result) // igualdade EXATA e ordenada
293
+ senão:
294
+ // conta ocorrências por tipo em result e em combination
295
+ para cada tipo da combination:
296
+ se result não tem aquele tipo OU tem MENOS que o exigido → perdeu
297
+ retorna ganhou
298
+ ```
299
+
300
+ (!) 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).
301
+
302
+ ### 3.4 `MysteryResult` — saída do `execute` (não persistido)
303
+
304
+ | Campo | Tipo | Descrição |
305
+ |---|---|---|
306
+ | `mystery` | String | id da box. |
307
+ | `player` | String | id do jogador. |
308
+ | `time` | String | timestamp ISO (zoned). |
309
+ | `result` | String[] | valores sorteados (1 por coluna). **Pode conter `null`** = NONE. |
310
+ | `status` | String | `"WIN"` / `"LOSE"`. **Ausente** se `win_chart` vazia. |
311
+ | `achievements` | `Achievement[]` | a jogada (`a1`, só em WIN) + recompensas creditadas (`a2`). |
312
+
313
+ Constantes: `STATUS_WIN = "WIN"`, `STATUS_LOSE = "LOSE"`.
314
+
315
+ ### 3.5 `MysteryBoxLog` — log de tentativa (coleção `mystery_box_log`)
316
+
317
+ | Campo | Tipo | Descrição |
318
+ |---|---|---|
319
+ | `_id` | String | = id do achievement `a1` da jogada. |
320
+ | `item` | String | = id da mystery box. |
321
+ | `player` | String | jogador. |
322
+ | `time` | Date | momento da tentativa. |
323
+ | `extra` | Map | metadado enviado no `execute` (POST). |
324
+
325
+ ### 3.6 `MysteryFolder` — pasta (coleção `mystery_folder`)
326
+
327
+ | Campo | Tipo | Descrição |
328
+ |---|---|---|
329
+ | `_id` | String | id da pasta (= valor de `MysteryBox.folder`). |
330
+ | `title` | String | título; no auto-create recebe o mesmo valor do id (L64-65). |
331
+
332
+ ### 3.7 Técnicas de jogo (`techniques`)
333
+
334
+ 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.**
335
+
336
+ ---
337
+
338
+ ## 4. Endpoints
339
+
340
+ Todos sob `@Path("v3/mystery")`, `Produces: application/json; charset=UTF-8`. Autenticação via Bearer token (`AuthBean`).
341
+
342
+ ### `GET /v3/mystery/{id}` — buscar uma box
343
+
344
+ | Aspecto | Detalhe |
345
+ |---|---|
346
+ | Finalidade | Retorna a configuração da box (`find`, L59). |
347
+ | Autenticação | Bearer token |
348
+ | (!) id inexistente | Retorna **`200 OK` com corpo `null`** — **não** 404 (não há null-check). |
349
+
350
+ ### `GET /v3/mystery` — listar todas
351
+
352
+ 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).
353
+
354
+ ### `POST /v3/mystery` — criar/atualizar (upsert)
355
+
356
+ | Aspecto | Detalhe |
357
+ |---|---|
358
+ | Finalidade | `insert` (L208-213). |
359
+ | Full replace ou patch | **Full replace** (Jongo `save` por `_id`). Omitir campo o apaga. |
360
+ | Status | `201 Created`, devolve a box já com `_id`/`options` recalculados. |
361
+ | Efeitos colaterais | Recalcula `options`, atribui `_id` às combinações, cria pasta se ausente. |
362
+
363
+ ### `DELETE /v3/mystery/{id}` — remover box
364
+
365
+ `delete` (L94). Remove o documento de `mystery_box` (`remove {_id:#}`, L82). **Não** remove logs nem achievements relacionados. Status `200`.
366
+
367
+ ### `GET /v3/mystery/execute/{id}?player={player}` — jogar (GET)
368
+
369
+ | Aspecto | Detalhe |
370
+ |---|---|
371
+ | Finalidade | Executa a box e retorna `MysteryResult` (L229-321). |
372
+ | `player=me` | Resolve o jogador a partir do token (L234-235). |
373
+ | 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). |
374
+
375
+ ### `POST /v3/mystery/execute` — jogar (POST, com `extra`)
376
+
377
+ Body: `{ "_id": "...", "player": "...", "extra": { ... } }` (L324-421). Permite anexar `extra` (gravado no log). `player=me` → token.
378
+
379
+ ### `POST /v3/mystery/execute_by_player/{player}` — jogar por jogador na URL
380
+
381
+ Body: `{ "_id": "...", "extra": { ... } }` (L424-521). O jogador vem do path param (não do body). Mesmas validações.
382
+
383
+ ### `DELETE /v3/mystery/execute` — rollback de jogada
384
+
385
+ | Aspecto | Detalhe |
386
+ |---|---|
387
+ | Finalidade | Desfaz uma jogada (`undoExecute`, L525-540 → L158-171). |
388
+ | Body | `{ "_id": "<achievementId da jogada>" }`. |
389
+ | Comportamento | Só age se o achievement for `type=6`. Remove os achievements de recompensa (`extra.origin == id`) **e** o achievement da jogada. |
390
+ | (!) Não faz | **Não** estorna `requirements` debitados; **não** remove o `mystery_box_log`. |
391
+ | Resposta | `200 {"message":"Execution Rollback Done"}` |
392
+
393
+ ### `GET /v3/mystery/evaluate/{id}?player={player}&time={time}` — diagnóstico
394
+
395
+ | Aspecto | Detalhe |
396
+ |---|---|
397
+ | Finalidade | Avalia elegibilidade sem jogar (`evaluate` → `evaluateAnalyze`, L543-571 / L524-563). |
398
+ | `time` | Aceita epoch ms, data ISO/zoned, keyword (ex. `-1d`) ou vazio (→ agora) (L556-560). |
399
+ | Retorno | `{ params, exists, limit_wins, limit_attempts, limit, attempts, requirements, active, milliseconds }`. |
400
+ | (!) 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). |
401
+
402
+ #### Exemplo de execução (request/response)
403
+
404
+ ```
405
+ GET /v3/mystery/execute/heads_or_tails?player=ricardo@acme.com
406
+ Authorization: Bearer eyJhbGciOiJIUzUxMiIs...
407
+ ```
408
+
409
+ ```json
410
+ {
411
+ "mystery": "heads_or_tails",
412
+ "player": "ricardo@acme.com",
413
+ "time": "2026-05-20T13:40:11-03:00",
414
+ "result": ["heads"],
415
+ "status": "WIN",
416
+ "achievements": [
417
+ { "_id": "aZ8...", "player": "ricardo@acme.com", "total": 1, "type": 6, "item": "heads_or_tails", "time": "..." },
418
+ { "_id": "bY9...", "player": "ricardo@acme.com", "total": 10, "type": 0, "item": "coin", "time": "...",
419
+ "extra": { "origin": "aZ8...", "origin_item": "heads_or_tails", "origin_type": 6, "mystery_win_id": "o1" } }
420
+ ]
421
+ }
422
+ ```
423
+
424
+ ---
425
+
426
+ ## 5. Regras de Negócio
427
+
428
+ Regras que **existem no código** e não no schema:
429
+
430
+ 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.
431
+ 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…").
432
+ 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.
433
+ 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).
434
+ 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**.
435
+ 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).
436
+ 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.
437
+ 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.
438
+ 9. **`inventory` é cap global** por `reward.type+item` (ou isolado por box / box+combinação se `isolated`). Conta `achievement` (L421-449).
439
+ 10. **`updatePlayerStatus` sempre roda** ao final do execute (mesmo em LOSE / sem win_chart), desde que box e player existam (L414).
440
+ 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.
441
+
442
+ ---
443
+
444
+ ## 6. Comportamentos Automáticos
445
+
446
+ | Comportamento | Trigger/Origem | Impacto | Persistência |
447
+ |---|---|---|---|
448
+ | Gera `_id` da box | `insert` (L42-44) | id curto se ausente | `mystery_box` |
449
+ | `active=true` default | `insert` (L46-48) | box nasce ativa | `mystery_box` |
450
+ | Recalcula `options` | `prepareOptions` (L51) | probabilidades normalizadas, opções inválidas removidas, ordem alterada | `mystery_box` |
451
+ | Atribui `_id` às combinações | `prepareWinchart` (L54) | `o1`,`o2`,… | `mystery_box` |
452
+ | Cria pasta default | `insert` (L57-67) | doc em `mystery_folder` | `mystery_folder` |
453
+ | Log de tentativa | `execute` (L191-192) | toda jogada registrada | `mystery_box_log` |
454
+ | Débito de `requirements` | `execute` (L195-202) | custo deduzido (achievements negativos) | `achievement` |
455
+ | Achievement da jogada (`a1`) | `execute` em WIN (L330) | conta para `limit` | `achievement` (`type=6`) |
456
+ | Achievement da recompensa (`a2`) | `execute` por combinação (L386) | crédito ao jogador | `achievement` (point/challenge/catalog_item) |
457
+ | `updatePlayerStatus` | `execute` (L414) | recalcula status do jogador | `player_status` |
458
+ | Triggers | `execute` | ver fluxo abaixo | conforme handler |
459
+
460
+ ### Encadeamento de triggers numa vitória com recompensa
461
+
462
+ Eventos disparados, **nesta ordem** (L320-409):
463
+
464
+ 1. `before_win` — entidade `mystery_box` (a jogada `a1`).
465
+ 2. `addAchievement(a1)` — persiste a jogada.
466
+ 3. Para **cada** combinação vencedora que casa:
467
+ 1. `before_win_reward` — entidade `mystery_box` ((!) **string literal hardcoded** em L376, **não** é constante `Trigger.EVENT_*`; grep por constante não acha).
468
+ 2. `before_win` — entidade = **coleção da recompensa** (`point_category`/`challenge`/`catalog_item`).
469
+ 3. `addAchievement(a2)` — persiste a recompensa.
470
+ 4. `after_win` — entidade = coleção da recompensa (**após** persistir).
471
+ 4. `after_win` — entidade `mystery_box`.
472
+
473
+ Em derrota: `before_lose` (antes do loop, que não credita nada) e `after_lose` (no fim) — entidade `mystery_box`.
474
+
475
+ ```mermaid
476
+ flowchart LR
477
+ W{WIN?} -- sim --> BW["before_win (mystery_box)"]
478
+ BW --> AA["addAchievement(a1)"]
479
+ AA --> LP["loop win_chart casadas"]
480
+ LP --> BWR["before_win_reward<br/>(literal, mystery_box)"]
481
+ BWR --> BWC["before_win<br/>(colecao da recompensa)"]
482
+ BWC --> AA2["addAchievement(a2)"]
483
+ AA2 --> AWC["after_win<br/>(colecao da recompensa)"]
484
+ AWC --> AW["after_win (mystery_box)"]
485
+ W -- nao --> BL["before_lose (mystery_box)"]
486
+ BL --> AL["after_lose (mystery_box)"]
487
+ ```
488
+
489
+ ---
490
+
491
+ ## 7. Suportado vs NÃO Suportado
492
+
493
+ ### ✅ Suportado
494
+
495
+ - CRUD de mystery box (`GET`/`GET all`/`POST` upsert/`DELETE`).
496
+ - Execução por GET, POST (com `extra`) e `execute_by_player`.
497
+ - Sorteio ponderado por probabilidade, multi-coluna (`columns`).
498
+ - `win_chart` com combinação ordenada (`orderSensitive=true`) ou por contagem (`false`).
499
+ - Recompensas `point` (0), `challenge` (1), `catalog_item` (2).
500
+ - `limit` (vitórias) e `attempts` (jogadas) por `player` e `gamification`, com janela `every` e `total` numérico **ou** fórmula.
501
+ - `reward.limit` e `reward.inventory`, com isolamento por box/combinação (`isolated`).
502
+ - Exclusão dinâmica de opções com recompensa esgotada (somente `columns==1`).
503
+ - Rollback de jogada (`DELETE /v3/mystery/execute`).
504
+ - Diagnóstico (`GET /v3/mystery/evaluate/{id}`).
505
+ - Pastas (`mystery_folder`) com auto-criação.
506
+ - Triggers `before_win`/`after_win`/`before_lose`/`after_lose` + `before_win_reward`.
507
+ - Geração assistida por IA (`POST /v3/ai/build/mystery`) — **não persiste**, só devolve o JSON.
508
+
509
+ ### ❌ NÃO Suportado / Pegadinhas
510
+
511
+ - **`type`** (`option_probability`/`combination_probability`) — não existe na entidade; **descartado** silenciosamente (`@JsonIgnoreProperties`).
512
+ - **`probability` por combinação** em `win_chart` — não existe em `MysteryWin`; **descartado**. Odds vêm de `options`×`columns`.
513
+ - **`reward.operation`** — ignorado; só `total/type/item` são usados (comentário L21 de `MysteryWin`).
514
+ - **Recompensa com `type` ≠ 0/1/2** (level, crown, lottery, etc.) — `a2` criado mas **nunca persistido** (collection null).
515
+ - **`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`.
516
+ - **`Limit.query`** — campo existe no POJO `Limit` mas **nunca é consumido** por nenhuma verificação de mystery.
517
+ - **`MysteryBox.extra`** — persistido, mas **nunca lido** pela lógica do módulo (passthrough).
518
+ - **`MysteryBox.techniques`** — legado; nunca escrito/lido pelo módulo; sem GT code (§3.7).
519
+ - **`MysteryBoxFolder.java`** — classe **morta**, duplicata de `MysteryFolder`, sem referências.
520
+ - **`PUT`** — inexistente; atualização é re-`POST` (full replace).
521
+ - **404 em id inexistente** — `GET /{id}` devolve `200` com `null`.
522
+ - **Proteção de recompensa esgotada para `columns > 1`** — não existe (só `columns==1`).
523
+ - **Validação de probabilidade > 1** — inexistente; gera probabilidade negativa (foot-gun, §2.2/§5).
524
+ - **Estorno de `requirements` no rollback** — `undoExecute` não devolve o custo nem apaga o log.
525
+ - **Bug no `evaluate`**: chave `active` sempre `{}`; status de `active` sobrescreve `exists.status` (L556-558).
526
+ - **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.
527
+ - **Scheduler/job automático** — não existe para mystery (toda execução é via endpoint).
528
+
529
+ ---
530
+
531
+ ## 8. Segurança e Permissões
532
+
533
+ - **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.
534
+ - **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.
535
+ - **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.
536
+ - **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.
537
+ - **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.
538
+
539
+ ---
540
+
541
+ ## 9. Observabilidade e Troubleshooting
542
+
543
+ ### Diagnóstico rápido
544
+
545
+ ```
546
+ GET /v3/mystery/{id} # a box existe? (200 null = não existe)
547
+ GET /v3/mystery/evaluate/{id}?player=ME # elegibilidade: limit_wins, limit_attempts, requirements
548
+ ```
549
+
550
+ > 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`.
551
+
552
+ ### Queries MongoDB úteis
553
+
554
+ ```js
555
+ // jogadas (tentativas) de um jogador numa box
556
+ db.mystery_box_log.find({ item: "<boxId>", player: "<player>" }).sort({ time: -1 })
557
+
558
+ // vitórias contam aqui (type=6)
559
+ db.achievement.count({ type: 6, item: "<boxId>", player: "<player>" })
560
+
561
+ // recompensas creditadas por esta box
562
+ db.achievement.find({ "extra.origin_item": "<boxId>" })
563
+
564
+ // recompensas de uma combinação específica
565
+ db.achievement.find({ "extra.origin_item": "<boxId>", "extra.mystery_win_id": "o1" })
566
+
567
+ // estoque já distribuído de uma recompensa (inventory)
568
+ db.achievement.count({ type: 0, item: "coin" })
569
+ ```
570
+
571
+ ### Erros comuns e causas
572
+
573
+ | Sintoma | Causa provável |
574
+ |---|---|
575
+ | `400 insufficient requirements` | Jogador não tem o custo (`requirements`) — mas o `mystery_box_log` já foi gravado. |
576
+ | `400 You have exced the limit … earned` | `limit` (vitórias) atingido no período `every`. |
577
+ | `400 You have exced the limit … executed` | `attempts` (jogadas) atingido. |
578
+ | `400 mysterybox is not active` | `active=false` explícito. |
579
+ | `400 mysterybox does not exist` | id inválido no `execute` (no `GET /{id}` retornaria 200/null). |
580
+ | 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"`). |
581
+ | Limite "não funciona" | `reward.limit.per="team"` (não suportado); ou soma de probabilidades > 1 corrompendo o sorteio. |
582
+ | Opção nunca sai / `result` quase sempre `null` | Probabilidades somam > 1 → probabilidade negativa em opções zero (§2.2). |
583
+ | Resultado contém `null` | Probabilidades somam < 1 → faixa NONE (esperado, §2.4). |
584
+
585
+ ### O que verificar quando "não funciona"
586
+
587
+ 1. A box está `active`? (`GET /{id}` → campo `active`, lembrando que `null` = ativa).
588
+ 2. As probabilidades das `options` somam ≤ 1? Há opções `>1`/`<0` que foram descartadas no save?
589
+ 3. A combinação da `win_chart` realmente casa com o `result`? Confira `orderSensitive`.
590
+ 4. O `reward.type` é 0/1/2? Caso contrário não é creditado.
591
+ 5. `limit`/`inventory` da recompensa esgotou? Veja o console (`MysteryWin not registered because of limit`).
592
+
593
+ ---
594
+
595
+ ## 10. Exemplos Práticos
596
+
597
+ ### 10.1 Mínimo funcional — cara ou coroa
598
+
599
+ ```json
600
+ POST /v3/mystery
601
+ {
602
+ "_id": "heads_or_tails",
603
+ "title": "Heads or Tails",
604
+ "options": [
605
+ { "title": "Heads", "value": "heads", "probability": 0.5 },
606
+ { "title": "Tails", "value": "tails", "probability": 0.5 }
607
+ ],
608
+ "columns": 1,
609
+ "win_chart": [
610
+ { "combination": ["heads"], "orderSensitive": false,
611
+ "reward": { "total": 1, "type": 0, "item": "coin" } }
612
+ ]
613
+ }
614
+ ```
615
+
616
+ Jogar: `GET /v3/mystery/execute/heads_or_tails?player=me`.
617
+
618
+ ### 10.2 Avançado — slot 3 colunas, custo, limites e estoque
619
+
620
+ ```json
621
+ POST /v3/mystery
622
+ {
623
+ "_id": "office_prizes",
624
+ "title": "Office Prizes",
625
+ "options": [
626
+ { "title": "Notebook", "value": "notebook", "probability": 0.1 },
627
+ { "title": "Backpack", "value": "backpack", "probability": 0.3 },
628
+ { "title": "Mouse", "value": "mouse", "probability": 0.6 }
629
+ ],
630
+ "columns": 3,
631
+ "requirements": [
632
+ { "total": 4, "type": 0, "item": "coin", "operation": 1 }
633
+ ],
634
+ "win_chart": [
635
+ { "combination": ["notebook","notebook","notebook"], "orderSensitive": false,
636
+ "reward": { "total": 1, "type": 2, "item": "notebook" },
637
+ "inventory": 5, "isolated": true,
638
+ "limit": { "total": 1, "per": "player", "every": "1M" } },
639
+ { "combination": ["mouse","mouse","mouse"], "orderSensitive": false,
640
+ "reward": { "total": 1, "type": 2, "item": "mouse" } }
641
+ ],
642
+ "limit": { "total": 1, "per": "player", "every": "1M" },
643
+ "attempts": { "total": 1, "per": "player", "every": "1d" }
644
+ }
645
+ ```
646
+
647
+ - `requirements`: custa 4 `coin` por jogada (`operation:1` = deduct).
648
+ - `limit`: máx. **1 vitória** por jogador por mês.
649
+ - `attempts`: máx. **1 jogada** por jogador por dia.
650
+ - `inventory: 5` + `isolated`: só 5 notebooks distribuídos por esta box.
651
+
652
+ ### 10.3 Anti-pattern — o que NÃO fazer
653
+
654
+ ```json
655
+ {
656
+ "_id": "broken_box",
657
+ "title": "Broken",
658
+ "type": "option_probability", // (X) ignorado (não existe na entidade)
659
+ "options": [
660
+ { "value": "a", "probability": 0.7 },
661
+ { "value": "b", "probability": 0.6 } // (X) soma 1.3 > 1 -> prob. negativa, sorteio corrompido
662
+ ],
663
+ "columns": 3,
664
+ "win_chart": [
665
+ { "combination": ["a","a","a"],
666
+ "probability": 0.01, // (X) ignorado (não existe em MysteryWin)
667
+ "reward": { "total": 1, "type": 3, "item": "lvl" }, // (X) type=3 (level) nunca é creditado
668
+ "limit": { "total": 5, "per": "team" } // (X) per=team não bloqueia nada
669
+ }
670
+ ]
671
+ }
672
+ ```
673
+
674
+ Por que é errado:
675
+
676
+ - `type` no topo e `probability` na combinação são **descartados** silenciosamente.
677
+ - Probabilidades das `options` somam **> 1** → `prepareOptions` quebra o sorteio.
678
+ - `reward.type=3` não está em {0,1,2} → o prêmio **nunca** é registrado.
679
+ - `limit.per="team"` é **silenciosamente ignorado** → sem limite real.
680
+ - Sem `_id` nas combinações, mas isso é OK (o sistema gera `o1`,`o2`…) — só não dependa dele antes do primeiro save.
681
+
682
+ ---
683
+
684
+ ## Checklist de Configuração
685
+
686
+ - [ ] `title` preenchido.
687
+ - [ ] `options` com `value` único por opção; probabilidades somando **≤ 1** (evite `> 1` → quebra o sorteio).
688
+ - [ ] Nenhuma opção com `probability > 1` ou `< 0` (seriam **descartadas** no save).
689
+ - [ ] `columns` definido conforme o jogo (1 = roleta simples; >1 = slot).
690
+ - [ ] `win_chart[].combination` usa exatamente os `value`s das `options`.
691
+ - [ ] `orderSensitive` definido conscientemente (`false` = contagem por tipo, pode casar várias linhas).
692
+ - [ ] `reward.type` ∈ {0 (point), 1 (challenge), 2 (catalog_item)} — outros tipos **não** são creditados.
693
+ - [ ] Itens/pontos/desafios de `reward.item` e `requirements.item` **existem** antes de criar a box.
694
+ - [ ] Para limites por jogador/jogo use `per: "player"` ou `"gamification"` — **`team` não funciona**.
695
+ - [ ] Diferencie `limit` (vitórias) de `attempts` (jogadas): se quer limitar quantas vezes pode jogar, use `attempts`.
696
+ - [ ] Ciente de que `extra`, `techniques` e `type` no payload são metadados/ignorados.
697
+ - [ ] Ciente de que `POST` é **full replace** — reenvie o documento completo ao atualizar.
698
+ - [ ] Ciente de que rollback (`DELETE /execute`) **não** estorna `requirements`.
699
+
700
+ ## Steps
701
+
702
+ ### Rules — follow exactly, no exceptions
703
+
704
+ **Technique:** always `["GT72"]`.
705
+ **`options[].probability` must sum to exactly `1.0`** — build will not enforce, runtime will produce wrong results.
706
+ **`win_chart[].combination`:** array of option values that triggers the reward.
707
+ **Evaluate before execute:** `GET /v3/mystery/evaluate/:id?player=<id>` checks eligibility.
708
+
709
+ ---
710
+
711
+ ### 1. Check if mystery box already exists
712
+
713
+ ```
714
+ funifier_list type=mystery search=<title>
715
+ ```
716
+
717
+ ### 2. Design options and win combinations
718
+
719
+ Coin flip example (50/50):
720
+ ```json
721
+ {
722
+ "title": "Coin Flip",
723
+ "options": [
724
+ { "title": "Heads", "value": "heads", "probability": 0.5 },
725
+ { "title": "Tails", "value": "tails", "probability": 0.5 }
726
+ ],
727
+ "columns": 1,
728
+ "requirements": [],
729
+ "win_chart": [
730
+ {
731
+ "combination": ["heads"],
732
+ "orderSensitive": false,
733
+ "reward": { "total": 10, "type": 0, "item": "coin" }
734
+ }
735
+ ],
736
+ "techniques": ["GT72"]
737
+ }
738
+ ```
739
+
740
+ Spin wheel with 3 slots (unequal odds):
741
+ ```json
742
+ {
743
+ "title": "Spin Wheel",
744
+ "options": [
745
+ { "title": "Gold", "value": "gold", "probability": 0.1 },
746
+ { "title": "Silver", "value": "silver", "probability": 0.3 },
747
+ { "title": "Bronze", "value": "bronze", "probability": 0.6 }
748
+ ],
749
+ "columns": 3,
750
+ "win_chart": [
751
+ { "combination": ["gold", "gold", "gold"], "orderSensitive": true, "reward": { "total": 100, "type": 0, "item": "coin" } },
752
+ { "combination": ["silver", "silver", "silver"], "orderSensitive": true, "reward": { "total": 50, "type": 0, "item": "coin" } }
753
+ ],
754
+ "techniques": ["GT72"]
755
+ }
756
+ ```
757
+
758
+ ### 3. Save
759
+
760
+ ```
761
+ funifier_save type=mystery payload=<json>
762
+ ```
763
+
764
+ ### 4. Test
765
+
766
+ ```
767
+ GET /v3/mystery/evaluate/<_id>?player=<player_id>
768
+ GET /v3/mystery/execute/<_id>?player=<player_id>
769
+ ```