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,112 +1,845 @@
1
- # Lottery (Sorteio)
1
+ # `lottery`
2
2
 
3
3
  **Acesso Studio:** `/studio/lottery`
4
4
  **API Endpoint:** `/v3/lottery`
5
+ **Endpoint Legado:** `/2.0.0/lottery`
6
+ **Coleções MongoDB:** `lottery`, `lottery_folder`, `lottery_ticket` (vencedores ficam em `achievement` com `type=5`)
5
7
 
6
- ## O que é
8
+ ---
7
9
 
8
- Configuração de sorteios e concursos. Permite criar sorteios nos quais os jogadores participam com cupons obtidos em outras mecânicas, como desafios. É possível definir datas, limites de ganhadores e prêmios para os sorteados.
10
+ ## 1. Visão Geral
9
11
 
10
- ## Quando usar
12
+ O módulo `lottery` implementa a mecânica de **sorteio** (loteria/rifa) da gamificação Funifier. Jogadores acumulam **bilhetes** (`lottery_ticket`) e, em uma data de sorteio (`drawDate`), o sistema escolhe um ou mais bilhetes vencedores por um dos métodos de escolha suportados. Cada vencedor recebe um `Achievement` do tipo `TYPE_LOTTERY (5)` e, opcionalmente, recompensas (`rewards`).
11
13
 
12
- - Para criar campanhas promocionais com prêmios
13
- - Para sorteios de viagens, produtos ou experiências
14
- - Para incentivar participação em desafios (cupons como recompensa)
14
+ Papel arquitetural:
15
15
 
16
- ## Dependências
16
+ - A configuração do sorteio é um documento na coleção `lottery`. Os bilhetes são documentos independentes em `lottery_ticket`, ligados por `lottery_ticket.lottery = lottery._id`.
17
+ - **Não há coleção própria de "vencedores"**: o resultado de um sorteio é gravado na coleção `achievement` (`type=5, item=<lotteryId>`), reutilizando todo o motor de `AchievementManager` (totais, level-up, player_status, triggers, webhooks). Por isso o módulo `lottery` depende fortemente do módulo [achievement](achievement.md).
18
+ - A execução pode ser **manual** (`GET /v3/lottery/{id}/execute`) ou **automática** (thread `AsyncProcessor`, a cada 5 minutos, para loterias com `autoExecute=true` e `drawDate` no passado).
17
19
 
18
- - **Virtual Good** (opcional): itens podem ser usados como prêmio do sorteio
20
+ Problemas que resolve:
19
21
 
20
- ## Checklist de Configuração no Studio
22
+ - Distribuição de prêmios "alguém tem que ganhar" por sorteio (chance proporcional ao número de bilhetes).
23
+ - Campanhas promocionais onde bilhetes são distribuídos como recompensa de desafios/ações (via `Requirement.type = TYPE_LOTTERY_TICKET (50)` no módulo [challenge](challenge.md)).
21
24
 
22
- - [ ] Definir título e descrição do sorteio
23
- - [ ] Definir data do sorteio (drawDate)
24
- - [ ] Definir método de escolha (random_ticket)
25
- - [ ] Definir número máximo de ganhadores (maxWinners)
26
- - [ ] Definir limite por jogador (maxPerPlayer)
27
- - [ ] Configurar recompensas (rewards)
28
- - [ ] Definir se executa automaticamente (autoExecute)
25
+ Relação com outros módulos:
29
26
 
30
- ## API Endpoints
27
+ - [achievement](achievement.md) — vencedor vira `Achievement` type=5; recompensas viram achievements adicionais (pontos/items/desafios).
28
+ - [challenge](challenge.md) — desafios podem creditar bilhetes de loteria como recompensa (`TYPE_LOTTERY_TICKET`).
29
+ - [trigger](../guides/triggers.md) — dispara `before_win`, `after_win` (entidade `lottery`) e `after_execute`.
30
+ - `notification` — notificações `EVENT_WIN` configuradas no sorteio são enviadas ao vencedor.
31
+ - `virtual_good` / `catalog_item`, `point_category` — possíveis tipos de recompensa.
31
32
 
32
- ### Listar Sorteios
33
- **Método:** GET
34
- **Endpoint:** `/v3/lottery`
33
+ > **Fora de escopo:** existe um módulo distinto **`brazilian_lottery`** (`com.funifier.engine.brazilian.lottery.*`, coleção `brazilian_lottery`), que modela a loteria federal brasileira (padrão de números da sorte, `FederalResult`, `WinnerRule`). Apesar do nome semelhante, é um módulo separado, com entidade, manager e REST próprios — **não** é coberto por este documento.
34
+
35
+ ---
36
+
37
+ ## 2. Arquitetura e Fluxos
38
+
39
+ ### 2.1 Classes envolvidas
40
+
41
+ | Classe | Papel |
42
+ |---|---|
43
+ | `com.funifier.engine.lottery.Lottery` | Entidade/POJO do sorteio — documento raiz (`Lottery.java`, L26-128) |
44
+ | `com.funifier.engine.lottery.LotteryTicket` | Bilhete — documento em `lottery_ticket` |
45
+ | `com.funifier.engine.lottery.LotteryFolder` | Agrupamento (pasta) de sorteios |
46
+ | `com.funifier.engine.lottery.Participant` | DTO de aggregation (`_id`=player, `tickets`=contagem) — não persistido |
47
+ | `com.funifier.engine.lottery.LotteryManager` | Manager monolítico (`LotteryManager.java`, L36-888): CRUD, tickets, execução do sorteio, rollback |
48
+ | `com.funifier.rest.v3.rest.LotteryRest` | Controller REST v3 (`/v3/lottery`) |
49
+ | `com.funifier.rest.engine.LotteryRest` | Controller REST legado (`/2.0.0/lottery`) |
50
+ | `com.funifier.controller.AsyncProcessor` | Thread assíncrona que executa sorteios automáticos (L22-54) |
51
+ | `com.funifier.engine.challenge.Requirement` | Estrutura de uma recompensa (`rewards[]`) |
52
+
53
+ **Não há `Repository`/`Dao` dedicado** — toda manipulação MongoDB é feita diretamente via `Jongo` dentro de `LotteryManager`.
54
+
55
+ ### 2.2 Pipeline principal — `execute(String id)` (L607-848)
56
+
57
+ Método **`synchronized`**. Síncrono. Não há transação MongoDB — cada `save`/`addAchievement` é independente.
58
+
59
+ ```
60
+ [1] Carrega Lottery por id (L609).
61
+ ⚠️ Se não existir → NPE em L612 (lottery.maxWinners) → HTTP 500.
62
+ [2] max = maxWinners > 0 ? maxWinners : 1 (usado só por first_ticket/last_ticket, L612)
63
+ [3] Cria índice {random:1} em lottery_ticket (L619) — VESTIGIAL (ver §7).
64
+ [4] who = teamManager.findUserIdsByPrincipalIds(lottery.principals) (L622)
65
+ - principals null/vazio → who = null → sorteio considera TODOS os jogadores.
66
+ - principals com ids → expande times em userIds + inclui os próprios ids.
67
+ [5] Despacha por choiceMethod (L625-639):
68
+ random_ticket → executeRandomTicket(...) ($sample do MongoDB)
69
+ random_value → executeRandomValue(...) (escolhe um value e pega todos os tickets com esse value)
70
+ first_ticket → sort {created:1}, limit max
71
+ last_ticket → sort {created:-1}, limit max
72
+ [6] SE selected != null E selected.size() > 0 E lottery.executedDate == null (L643):
73
+ Para cada ticket vencedor (L647):
74
+ [6.1] Achievement(novoId, ticket.player, total=1, TYPE_LOTTERY=5, item=lottery.id, now)
75
+ achievement.extra.ticket = ticket.id (L650-652)
76
+ [6.2] trigger.execute(lottery, BEFORE_WIN, player, context) (L659)
77
+ [6.3] achievementManager.addAchievement(achievement) (L661)
78
+ [6.4] trigger.execute(lottery, AFTER_WIN, player, context) (L663)
79
+ [6.5] Processa rewards[] (L668-819) — ver §6
80
+ [6.6] Envia notifications EVENT_WIN ao vencedor (L824-830)
81
+ [6.7] Recarrega lottery2, seta executedDate = now, save (L835-837)
82
+ [6.8] trigger.execute(lottery2, "after_execute", null, {lottery, winners}) (L840-844)
83
+ [7] Retorna a lista de tickets vencedores (selected).
84
+ ```
85
+
86
+ **Consequência crítica (L643):** se nenhum ticket for selecionado (ex.: loteria sem bilhetes), `executedDate` **não** é gravado. Uma loteria `autoExecute=true` sem bilhetes é reprocessada pelo `AsyncProcessor` a cada 5 minutos **indefinidamente** (ver §5 e §9).
87
+
88
+ ### Fluxo de execução — `execute(id)`
89
+
90
+ ```mermaid
91
+ flowchart TB
92
+ A["execute(id) synchronized"] --> B{lottery existe?}
93
+ B -- não --> Z["NPE → HTTP 500"]
94
+ B -- sim --> C["who = principals → userIds"]
95
+ C --> D{choiceMethod}
96
+ D -- random_ticket --> E["executeRandomTicket\n$sample"]
97
+ D -- random_value --> F["executeRandomValue\nescolhe value, pega todos iguais"]
98
+ D -- first_ticket --> G["sort created asc, limit max"]
99
+ D -- last_ticket --> H["sort created desc, limit max"]
100
+ E --> I{selected > 0 E\nexecutedDate == null?}
101
+ F --> I
102
+ G --> I
103
+ H --> I
104
+ I -- não --> Y["NÃO grava executedDate\n(loteria continua aberta)"]
105
+ I -- sim --> J["loop vencedores"]
106
+ J --> K["Achievement type=5\n+ trigger before_win/after_win"]
107
+ K --> L["processa rewards (§6)"]
108
+ L --> M["notifications EVENT_WIN"]
109
+ M --> N["executedDate = now (save)"]
110
+ N --> O["trigger after_execute"]
111
+ ```
112
+
113
+ ### 2.3 Algoritmo `executeRandomTicket` (L408-498)
114
+
115
+ Método de escolha `random_ticket`: cada bilhete tem chance igual; jogador com mais bilhetes tem mais chance.
116
+
117
+ ```
118
+ totTickets = count({lottery:id}) (L411)
119
+ totPlayers = distinct(player) onde lottery=id [e player ∈ who] (L412-414)
120
+
121
+ maxWinners = maxWinners < 0 ? totPlayers : (maxWinners == 0 ? 1 : maxWinners) (L417)
122
+ maxPerPlayer= maxPerPlayer < 0 ? totTickets : maxPerPlayer (L418)
123
+ maxTickets = min(maxWinners * maxPerPlayer, totTickets) (L419)
124
+
125
+ required = maxTickets
126
+ enquanto required > 0:
127
+ list = aggregate({lottery:id, used != true [, player ∈ who]})
128
+ .and($sample: { size: required }) (L438-446)
129
+ para cada ticket em list:
130
+ se player novo E (#jogadores já escolhidos >= maxWinners):
131
+ rejeita (L453-455)
132
+ senão:
133
+ incrementa contador do jogador
134
+ se contador <= maxPerPlayer: seleciona (L466-467)
135
+ senão: rejeita
136
+ ticket.used = true; save(ticket) (L475-476)
137
+ se não há mais tickets com used != true: required = 0 (L484-485)
138
+ senão se selected >= maxTickets: required = 0 (L487-488)
139
+ senão: required = maxTickets - selected.size() (L491)
140
+ ```
141
+
142
+ Observações:
143
+ - A seleção é feita por `$sample` do MongoDB (amostra aleatória), **não** pelo campo `random` do bilhete.
144
+ - Todos os bilhetes percorridos no loop recebem `used=true` (mesmo os rejeitados), o que evita reescolha na próxima iteração.
145
+
146
+ ### 2.4 Algoritmo `executeRandomValue` (L502-600)
147
+
148
+ Método `random_value`: escolhe **um valor** aleatório entre os bilhetes e premia **todos** os bilhetes com aquele `value` (ex.: "número sorteado").
149
+
150
+ ```
151
+ n = count({lottery:id})
152
+ r = floor(random() * n)
153
+ sample = find({lottery:id}).skip(r).limit(1) (L529-531)
154
+ se sample.value != null: (L532)
155
+ seleciona (respeitando maxWinners/maxPerPlayer) todos os
156
+ tickets com {lottery:id, value: sample.value, used != true} (L541-543)
157
+ marcando used=true em cada um percorrido.
158
+ senão:
159
+ retorna lista VAZIA → nenhum vencedor (ver §5.9)
160
+ ```
161
+
162
+ Limites `maxWinners`/`maxPerPlayer`/`maxTickets` calculados igual a `executeRandomTicket`.
163
+
164
+ ⚠️ **Falha silenciosa (L532):** toda a seleção é condicionada a `sample.value != null`. Se o bilhete sorteado aleatoriamente tiver `value` nulo, o método retorna lista vazia mesmo havendo bilhetes. Combinado com a guarda de L643, o sorteio "ghost": não há vencedor e `executedDate` não é gravado.
165
+
166
+ ### 2.5 Execução automática — `AsyncProcessor` + `findLotteriesToExecute`
167
+
168
+ ```mermaid
169
+ sequenceDiagram
170
+ participant Thread as AsyncProcessor (1 por gamificação)
171
+ participant LM as LotteryManager
172
+ participant Mongo as lottery / lottery_ticket
173
+ participant AM as AchievementManager
174
+ participant Trg as TriggerManager
175
+ participant Ntf as NotificationManager
176
+
177
+ loop a cada 5 minutos (L29)
178
+ Thread->>LM: findLotteriesToExecute()
179
+ LM->>Mongo: find {executedDate:{$exists:false},<br/>drawDate:{$lte:now}, autoExecute:true}
180
+ Mongo-->>LM: loterias pendentes
181
+ loop para cada loteria
182
+ Thread->>LM: execute(lottery.id)
183
+ LM->>Mongo: seleciona tickets vencedores
184
+ LM->>Trg: before_win / after_win
185
+ LM->>AM: addAchievement(type=5)
186
+ LM->>Ntf: notification EVENT_WIN
187
+ LM->>Mongo: set executedDate=now
188
+ LM->>Trg: after_execute
189
+ end
190
+ end
191
+ ```
192
+
193
+ Query de `findLotteriesToExecute()` (L399-402):
194
+
195
+ ```js
196
+ db.lottery.find({
197
+ executedDate: { $exists: false },
198
+ drawDate: { $lte: <now> },
199
+ autoExecute: true
200
+ })
201
+ ```
202
+
203
+ Execução **manual** (`GET /v3/lottery/{id}/execute`) **ignora** `autoExecute` e `drawDate` — executa imediatamente qualquer loteria ainda não executada.
204
+
205
+ ### 2.6 Rollback — `undoExecute(String id)` (L860-887)
206
+
207
+ ```
208
+ SE lottery != null E lottery.executedDate != null:
209
+ [1] Para cada Achievement (type=5, item=lottery.id): (L866)
210
+ remove achievements onde {extra.origin: <id_do_achievement>} (L867)
211
+ → apaga as recompensas derivadas (pontos/items)
212
+ [2] remove achievements {type:5, item:lottery.id} (L871)
213
+ → apaga os registros de vencedor
214
+ [3] update {lottery:id} → $unset {used:""} em todos os tickets (L874-876)
215
+ → bilhetes voltam a ficar disponíveis
216
+ [4] lottery.autoExecute = false (L880) ⚠️
217
+ lottery.executedDate = null
218
+ save(lottery) (L882)
219
+ [5] remove notifications {item.type:lottery, item.id:lottery.id} (L885)
220
+ ```
221
+
222
+ ⚠️ **Efeitos colaterais não óbvios:**
223
+ - `autoExecute` é forçado para `false` (L880). Após um rollback, a loteria **não** será reexecutada automaticamente pelo scheduler — só manualmente.
224
+ - `drawDate` **não** é resetada (L879, comentada).
225
+ - `undoExecute` **não** chama `updateUserStatus` — o `player_status` do ex-vencedor permanece desatualizado (mostrando pontos/items já removidos) até o próximo evento que recalcule o status.
226
+
227
+ ---
228
+
229
+ ## 3. Estrutura dos Objetos
230
+
231
+ ### 3.1 `Lottery` — documento raiz (coleção `lottery`)
232
+
233
+ Anotação: `@JsonIgnoreProperties(ignoreUnknown=true)` — campos não mapeados enviados no POST são **silenciosamente descartados**.
234
+
235
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
236
+ |---|---|---|---|---|
237
+ | `_id` | String | `Guid.shortTimeMillis()` se ausente | Não (auto) | Id curto interno (`insert`, L48). **Não** é ObjectId Mongo |
238
+ | `title` | String | — | "Sim" só na doc | Título. **Sem validação server-side** — aceita ausente |
239
+ | `description` | String | — | "Sim" só na doc | Descrição. Sem validação |
240
+ | `image` | String | — | "Sim" só na doc | URL da imagem. Sem validação |
241
+ | `tags` | List\<String> | `[]` | Não | Tags (filtráveis por `$all` em `GET /v3/lottery`) |
242
+ | `drawDate` | Date | — | "Sim" só na doc | Data do sorteio. Usada pelo scheduler. Sem validação |
243
+ | `autoExecute` | boolean | `false` | Não | Se `true`, o scheduler executa após `drawDate` |
244
+ | `choiceMethod` | String | `"random_ticket"` se null (L53-54) | Não (auto) | Método de escolha (ver §3.5) |
245
+ | `maxWinners` | long | `-1` | Não | Máx. de jogadores premiados. `-1`=ilimitado (todos jogadores distintos); `0`→tratado como `1` (L417) |
246
+ | `maxPerPlayer` | long | `-1` | Não | Máx. de bilhetes premiados por jogador. `-1`=ilimitado |
247
+ | `notifications` | List\<NotificationDefinition> | `[]` | Não | Notificações disparadas ao vencedor (`EVENT_WIN`) |
248
+ | `link` | String | — | "Sim" só na doc | Call-to-action. Sem validação, sem uso no backend |
249
+ | `extra` | Map\<String,Object> | `{}` | Não | Campos arbitrários |
250
+ | `principals` | List\<String> | `null` | Não | Ids de jogadores/times elegíveis. `null`/vazio = todos |
251
+ | `folder` | String | `"default"` se null/vazio (L58-60) | Não (auto) | Pasta. Cria `LotteryFolder` automaticamente se não existir (L61-67) |
252
+ | `techniques` | List\<String> | `null` | Não | Códigos de técnica de jogo (ver §3.6). Auto-preenchido externamente |
253
+ | `executedDate` | Date | `null` | — (controle) | Preenchido após execução (L836). Marca loteria como sorteada |
254
+ | `createdDate` | Date | `new Date()` se null (L50-51) | Não (auto) | Data de criação |
255
+ | `rewards` | List\<Requirement> | `[]` | Não | Recompensas dos vencedores (ver §3.4 e §6) |
256
+
257
+ #### Campos computados / não persistidos
258
+
259
+ Nenhum — todos os campos de `Lottery` são persistidos via Jongo `save` (full replace).
260
+
261
+ #### Comportamento de `save` (full replace)
262
+
263
+ `insert()` faz `c.save(lottery)` (L69). O Jongo serializa o **POJO inteiro**: qualquer campo ausente no objeto vira ausente no documento. Logo:
264
+
265
+ - **`PUT`/`POST` é sempre full-replace, nunca merge parcial.** Omitir um campo no body o **apaga** do documento (exceto os que recebem default no `insert`).
266
+ - Omitir `createdDate` num update faz `insert()` re-setar `createdDate = now` (L50-51) → **a data de criação é resetada a cada atualização** que não reenvie o valor.
267
+
268
+ #### Métodos com nomes específicos (uso via trigger/Groovy)
269
+
270
+ `Lottery` expõe setters individuais (`setExecutedDate`, `setId`, `setTitle`, `setAutoExecute`, `setCreatedDate`, L113-127) — comentário em L112 indica que existem para alterar campos via trigger.
271
+
272
+ ### 3.2 `LotteryTicket` — bilhete (coleção `lottery_ticket`)
273
+
274
+ `@JsonIgnoreProperties(ignoreUnknown=true)`.
275
+
276
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
277
+ |---|---|---|---|---|
278
+ | `_id` | String | `Guid.newShortGuid()` se ausente (L300-301) | Não (auto) | Id do bilhete. **Diferente** do `Lottery._id` que usa `shortTimeMillis()` |
279
+ | `lottery` | String | — | **Sim** (silencioso) | Id da loteria. Se ausente, `insertTicket` faz **no-op silencioso** (L299) |
280
+ | `player` | String | — | **Sim** (silencioso) | Jogador dono. Se ausente, no-op silencioso |
281
+ | `value` | Object | — | Não | Valor livre (número/texto). Usado por `random_value` |
282
+ | `created` | Date | `new Date()` se null (L303-304) | Não (auto) | Data de criação |
283
+ | `random` | long | `random()*10_000_000` se `-1` (L306-309) | Não (auto) | Número aleatório. **Vestigial neste módulo** (ver §7) |
284
+ | `extra` | Map\<String,Object> | `null` | Não | Atributos arbitrários. Convenção: `extra.origin` quando o bilhete vem de recompensa |
285
+ | `used` | Boolean | `null` | — (controle) | Marcado `true` durante a execução; `$unset` no rollback |
286
+
287
+ ### 3.3 `LotteryFolder` — pasta (coleção `lottery_folder`)
288
+
289
+ | Campo | Tipo | Padrão | Descrição |
290
+ |---|---|---|---|
291
+ | `_id` | String | = `folder` informado | Id da pasta |
292
+ | `title` | String | = `_id` na criação automática (L65) | Título |
293
+
294
+ Criada automaticamente no `insert` da loteria quando o `folder` informado ainda não existe (L61-67). A pasta `"default"` é criada na primeira loteria sem `folder`.
295
+
296
+ ### 3.4 `Requirement` — recompensa (`Lottery.rewards[]`)
297
+
298
+ Mesma classe usada por `challenge`/`competition` (`com.funifier.engine.challenge.Requirement`). No contexto de loteria, apenas alguns campos são lidos por `execute` (ver §6):
299
+
300
+ | Campo | Tipo | Uso em lottery |
301
+ |---|---|---|
302
+ | `total` | int | Quantidade. `total == 0` → recompensa ignorada (L694). Valor negativo é convertido para positivo (`abs`, L696) |
303
+ | `type` | int | Tipo da recompensa (ver tabela abaixo) |
304
+ | `item` | String | Id do item; valor especial `"FUNIFIER_RANDOM"` para item de catálogo aleatório (L703) |
305
+ | `folder` | String | Filtra catálogo no caso `FUNIFIER_RANDOM` (L708-713) |
306
+ | `restrict` | boolean | Se `true`, só credita item de catálogo válido/disponível (L716-720) |
307
+ | `operation`, `period`, `perPlayer` | — | **Ignorados** no fluxo de loteria (lógica de time/dedução está comentada, L671-689) |
308
+
309
+ **Tipos de recompensa (`Requirement.TYPE_*`) efetivamente creditados pela loteria:**
310
+
311
+ | Valor | Constante | Creditado? | Observação |
312
+ |---|---|---|---|
313
+ | `0` | `TYPE_POINT` | ✅ Sim | Coleção `point_category` (L796-797) |
314
+ | `1` | `TYPE_CHALLENGE` | ✅ Sim | Coleção `challenge` (L799-800) |
315
+ | `2` | `TYPE_CATALOG_ITEM` | ✅ Sim | Coleção `catalog_item`; suporta `FUNIFIER_RANDOM` (L802-803) |
316
+ | `50` | `TYPE_LOTTERY_TICKET` | ❌ **NÃO** | Passa pelo filtro (L694) mas é **silenciosamente descartado** — ver §6 e §7 |
317
+ | `3,4,6,7,...` | LEVEL, CROWN, MYSTERY_BOX… | ❌ Não | Nem entram no filtro de L694 |
318
+
319
+ ### 3.5 Métodos de escolha (`choiceMethod`)
320
+
321
+ Constantes definidas em `Lottery.java` (L31-47):
322
+
323
+ | Valor | Constante | Comportamento |
324
+ |---|---|---|
325
+ | `random_ticket` | `CHOICE_METHOD_RANDOM_TICKET` | (padrão) Escolhe bilhetes aleatórios via `$sample`, respeitando `maxWinners`/`maxPerPlayer` |
326
+ | `random_value` | `CHOICE_METHOD_RANDOM_VALUE` | Sorteia um `value` e premia todos os bilhetes com esse valor |
327
+ | `first_ticket` | `CHOICE_METHOD_FIRST_TICKET` | Os `max` (=`maxWinners` ou 1) bilhetes mais antigos por `created` |
328
+ | `last_ticket` | `CHOICE_METHOD_LAST_TICKET` | Os `max` bilhetes mais recentes por `created` |
329
+ | ~~`more_tickets`~~ | comentado (L38) | **NÃO suportado** — código comentado |
330
+ | ~~`less_tickets`~~ | comentado (L41) | **NÃO suportado** — código comentado |
331
+
332
+ > Para `first_ticket`/`last_ticket`, `maxPerPlayer` é **ignorado** — o `$limit` usa apenas `max` (L612, 634, 638).
333
+
334
+ ### 3.6 Técnica de jogo (`techniques`) — `GT74`
335
+
336
+ O campo `techniques` **não** é preenchido pelo módulo de loteria no CRUD. Ele é populado por uma rotina de manutenção do `GameTechniqueManager`, que percorre as loterias sem técnica e adiciona o código `LOTTERY_CODE = "GT74"` (`GameTechniqueManager.java`, L73 + L149-159):
337
+
338
+ ```java
339
+ String LOTTERY_CODE = "GT74";
340
+ ...
341
+ for(Lottery o : lotteries) {
342
+ if(o.techniques == null) { o.techniques = new ArrayList<>(); }
343
+ o.techniques.add(LOTTERY_CODE);
344
+ jongo.getCollection(Entity.LOTTERY.collection).save(o);
345
+ }
346
+ ```
347
+
348
+ `GT74` é o código operacional da técnica **Lottery** no grafo de técnicas do Funifier.
349
+
350
+ ### 3.7 Ciclo de vida da loteria
351
+
352
+ ```mermaid
353
+ stateDiagram-v2
354
+ [*] --> Aberta: insert (executedDate=null)
355
+ Aberta --> Aberta: drawDate no futuro\n(scheduler ignora)
356
+ Aberta --> Executada: GET /{id}/execute (manual)\nou scheduler (autoExecute + drawDate<now)
357
+ Aberta --> Aberta: execute sem bilhetes\n(executedDate NÃO gravado → re-tentado)
358
+ Executada --> Revertida: DELETE /{id}/execute\n(undoExecute)
359
+ Revertida --> Executada: re-executar (manual)\n— autoExecute=false impede scheduler
360
+ Executada --> [*]: DELETE /{id}\n(orfãos: achievements type=5 ficam)
361
+ Aberta --> [*]: DELETE /{id}
362
+ note right of Revertida
363
+ executedDate=null
364
+ autoExecute=false
365
+ used dos tickets removido
366
+ end note
367
+ ```
368
+
369
+ ---
370
+
371
+ ## 4. Endpoints
372
+
373
+ Todos os endpoints v3 exigem **Bearer token** (resolvido via `AuthBean`/`FrontController.getInstance(apiKey)`).
374
+
375
+ ### 4.1 `GET /v3/lottery/{id}`
376
+
377
+ Busca uma loteria por id. Retorna o documento `Lottery` (ou `null` serializado se não existir). (L51-57)
378
+
379
+ ### 4.2 `GET /v3/lottery`
380
+
381
+ **Listar loterias (aggregation com filtros).** (L81-105)
382
+
383
+ | Param | Tipo | Descrição |
384
+ |---|---|---|
385
+ | `id` | String | Filtra por `_id` exato |
386
+ | `title` | String | Regex case-insensitive em `title` |
387
+ | `description` | String | Regex case-insensitive em `description` |
388
+ | `executed_min` / `executed_max` | String | Faixa sobre `executedDate` (RFC 3339) |
389
+ | `published_min` / `published_max` | String | Faixa sobre `createdDate` (RFC 3339 ou keyword `-1d`, `-30m`…) |
390
+ | `tags` | CSV | `tags: {$all: [...]}` |
391
+ | `fields` | CSV | Projeção `$project` |
392
+ | `orderby` | String | Campo de ordenação |
393
+ | `reverse` | bool | `true`=desc |
394
+ | `max_results` | int | Limite (default 100; ≤0 vira 100, L144) |
395
+
396
+ **Comportamento real:**
397
+ - **Não existe parâmetro `q`** neste endpoint (diferente de `/ticket`). Não há injeção de pipeline aqui.
398
+ - A resposta usa `toJsonRemoveNullFields` — campos `null` são omitidos do JSON.
399
+
400
+ ### 4.3 `POST /v3/lottery`
401
+
402
+ **Criar loteria.** Body = objeto `Lottery`. (L164-169)
403
+
404
+ | Aspecto | Detalhe |
405
+ |---|---|
406
+ | Persistência | `c.save(lottery)` — full replace por `_id` |
407
+ | Defaults automáticos | `_id`=`shortTimeMillis()`, `createdDate`=now, `choiceMethod`=`random_ticket`, `folder`=`default` (+ cria a pasta) |
408
+ | Validação | **Nenhuma** sobre campos "obrigatórios" da doc (title, drawDate, etc.) |
409
+ | Status | `201 Created` com o objeto serializado |
410
+
411
+ ### 4.4 `PUT /v3/lottery/{id}`
412
+
413
+ **Atualizar loteria.** (L181-187)
414
+
415
+ ⚠️ **Comportamento não-óbvio:** o handler chama `getLotteryManager().insert(lottery)` **sem usar o `{id}` da URL**. Consequências:
416
+
417
+ - O id efetivo é `lottery._id` **do body**, não o da URL.
418
+ - Se o body **não** trouxer `_id`, `insert()` gera um **novo** id (`shortTimeMillis`) e **cria uma loteria nova** — o `PUT` não atualiza nada.
419
+ - É full-replace: campos omitidos são apagados; `createdDate` omitido é re-setado para now.
420
+ - Não há diferença real entre `POST` e `PUT` além do código de status — ambos chamam `insert`.
421
+
422
+ ### 4.5 `DELETE /v3/lottery/{id}`
423
+
424
+ Remove a loteria. (L118-124, manager L221-224)
425
+
426
+ ```js
427
+ db.lottery_ticket.remove({ lottery: id }) // remove TODOS os bilhetes
428
+ db.lottery.remove({ _id: id }) // remove a loteria
429
+ ```
430
+
431
+ ⚠️ **Não remove**: achievements de vencedor (`type=5, item=id`) nem notificações. Deletar uma loteria já executada deixa **achievements órfãos** na coleção `achievement` (continuam contando no saldo dos jogadores).
432
+
433
+ ### 4.6 `POST /v3/lottery/ticket`
434
+
435
+ **Criar bilhete.** Body = `LotteryTicket`. (L234-259)
436
+
437
+ | Aspecto | Detalhe |
438
+ |---|---|
439
+ | `player` especial | `"me"` → resolve para o jogador do token (L246-248) |
440
+ | Validação | `ticket == null` → 400; `ticket.player == null` → 400 |
441
+ | Restrições | `checkRestrictions(lottery, player)` — se houver, retorna **400** com a lista |
442
+ | Defaults | `_id`=`newShortGuid()`, `created`=now, `random`=aleatório |
443
+
444
+ `checkRestrictions` (L250-274) pode retornar: `item_not_exist`, `was_executed`, `principal_not_allowed`.
445
+
446
+ ⚠️ **Bug — NPE quando a loteria não existe:** em L259-264, após `if(item == null) restrictions.add("item_not_exist")`, o código segue para `if(item.executedDate != null)` **sem `else`**, desreferenciando `item` nulo → **`NullPointerException` → HTTP 500** (em vez de um 400 limpo com `item_not_exist`). O mesmo ocorre se `ticket.lottery` for `null` (o `findOne` retorna null).
447
+
448
+ ### 4.7 `GET /v3/lottery/ticket`
449
+
450
+ **Listar bilhetes.** (L281-310, manager L330-392)
451
+
452
+ | Param | Tipo | Descrição |
453
+ |---|---|---|
454
+ | `id`, `lottery`, `player` | String | Filtros exatos (`player="me"` resolve via token) |
455
+ | `value` | String | Filtro por valor |
456
+ | `q` | String | **Critério MongoDB raw** injetado no `$match` (L354) — ver §8 |
457
+ | `fields`, `orderby`, `reverse`, `max_results` | — | Projeção/ordenação/limite |
458
+ | `published_min`/`max` | String | Faixa sobre `created` |
459
+
460
+ ### 4.8 `GET /v3/lottery/winner`
461
+
462
+ **Listar vencedores.** (L357-400)
463
+
464
+ Internamente busca em `achievement`: `findAllAchievements(player, TYPE_LOTTERY=5, lottery, ...)`. O parâmetro `lottery` mapeia para `achievement.item`. Cada resultado é enriquecido com `extra.player_name` e `extra.player_image` (L387-397).
465
+
466
+ | Param | Descrição |
467
+ |---|---|
468
+ | `lottery` | Id da loteria (= `achievement.item`) |
469
+ | `player` | Filtra por jogador (`"me"` suportado) |
470
+ | `q`, `fields`, `published_min/max`, `orderby`, `reverse`, `max_results` | Padrão de query de achievement |
471
+
472
+ **Response (200):**
35
473
 
36
- **Exemplo de Resposta:**
37
474
  ```json
38
475
  [
39
476
  {
40
- "title": "Travel to Cancun",
41
- "description": "Travel with a companion to Cancun, with airfare and accommodation, for 7 days.",
42
- "drawDate": 1690824650503,
43
- "autoExecute": true,
44
- "choiceMethod": "random_ticket",
45
- "maxWinners": 1,
46
- "maxPerPlayer": 1,
47
- "techniques": ["GT74"],
48
- "rewards": [
49
- {
50
- "total": 1,
51
- "type": 2,
52
- "item": "flight_ticket"
53
- }
54
- ],
55
- "_id": "DTj0x5z"
477
+ "player": "player@funifier.com",
478
+ "total": 1,
479
+ "type": 5,
480
+ "item": "DTj0x5z",
481
+ "time": 1497962136775,
482
+ "extra": {
483
+ "ticket": "5943ceba8517662bd342620d",
484
+ "player_name": "Cleia Santos",
485
+ "player_image": { "small": { "url": "photo.png" } }
486
+ },
487
+ "_id": "594916988517661881b33b96"
56
488
  }
57
489
  ]
58
490
  ```
59
491
 
60
- ### Criar Sorteio
61
- **Método:** POST
62
- **Endpoint:** `/v3/lottery`
63
- (Body igual ao exemplo de resposta acima)
492
+ ### 4.9 `GET /v3/lottery/participants`
493
+
494
+ **Painel de participação.** (L402-435)
495
+
496
+ Param: `lottery` (id). Agrega `lottery_ticket` agrupando por `player` (`Participant{_id, tickets}`) e combina com a contagem de vencedores.
497
+
498
+ **Response (200):**
64
499
 
65
- ### Deletar Sorteio
66
- **Método:** DELETE
67
- **Endpoint:** `/v3/lottery/:id`
500
+ ```json
501
+ {
502
+ "total_participants": 2,
503
+ "total_tickets": 15,
504
+ "total_winners": 1,
505
+ "participants": [ { "_id": "tom", "tickets": 10 }, { "_id": "ana", "tickets": 5 } ],
506
+ "lottery": { "_id": "DTj0x5z", "title": "..." },
507
+ "winners": [ { "player": "tom", "type": 5, "item": "DTj0x5z" } ]
508
+ }
509
+ ```
68
510
 
69
- ### Listar Cupons
70
- **Método:** GET
71
- **Endpoint:** `/v3/lottery/ticket?lottery=:id`
511
+ ### 4.10 `GET /v3/lottery/{id}/execute`
72
512
 
73
- ### Criar Cupom
74
- **Método:** POST
75
- **Endpoint:** `/v3/lottery/ticket`
513
+ **Executar o sorteio manualmente.** (L447-459) Ignora `autoExecute`/`drawDate`. Chama `manager.execute(id)`.
514
+
515
+ **Response (200):**
76
516
 
77
- **Exemplo de Body:**
78
517
  ```json
79
518
  {
80
- "lottery": "DTj0x5z",
81
- "player": "tom"
519
+ "lottery": { "_id": "DTj0x5z", "executedDate": 1690824999999, "...": "..." },
520
+ "tickets": [ { "_id": "tkt1", "lottery": "DTj0x5z", "player": "tom", "used": true } ]
82
521
  }
83
522
  ```
84
523
 
85
- ### Deletar Cupom
86
- **Método:** DELETE
87
- **Endpoint:** `/v3/lottery/ticket/:id`
524
+ ⚠️ Executar um id inexistente → NPE no manager (L612) → **HTTP 500**.
525
+
526
+ ### 4.11 `DELETE /v3/lottery/{id}/execute`
527
+
528
+ **Reverter a execução (rollback).** (L462-473) Chama `undoExecute(id)` (ver §2.6).
529
+
530
+ **Response (200):**
531
+
532
+ ```json
533
+ { "lottery": { "...": "..." }, "message": "Execution Rollback Done" }
534
+ ```
535
+
536
+ ### 4.12 Endpoints legados `/2.0.0/lottery`
537
+
538
+ Controller `com.funifier.rest.engine.LotteryRest`. Auth via query param `?api_key=...` (sem Bearer token).
539
+
540
+ | Endpoint | Comportamento |
541
+ |---|---|
542
+ | `GET /2.0.0/lottery/{id}` | `find(id)` |
543
+ | `GET /2.0.0/lottery` | `findAll()` — **sem filtros** (lista tudo) |
544
+ | `DELETE /2.0.0/lottery/{id}` | `delete(id)` |
545
+ | `POST /2.0.0/lottery` | `insert(lottery)` |
546
+ | `POST /2.0.0/lottery/ticket` | `insertTicket(ticket)` — **sem** `checkRestrictions` e **sem** validação de `player` (no-op silencioso se faltar `lottery`/`player`) |
547
+ | `GET /2.0.0/lottery/execute/{id}` | `execute(id)` — note o **formato de path diferente** do v3 (`/execute/{id}` vs `/{id}/execute`) |
548
+
549
+ O legado **não** possui endpoints de `winner`, `participants`, `update` (PUT) nem `undoExecute`. Não recomendado para novas integrações.
550
+
551
+ ---
552
+
553
+ ## 5. Regras de Negócio
554
+
555
+ Regras presentes no código mas ausentes do schema das entidades:
556
+
557
+ ### 5.1 Semântica de `maxWinners` / `maxPerPlayer` (L417-419)
558
+
559
+ - `maxWinners = -1` (padrão) → **ilimitado** = total de jogadores distintos.
560
+ - `maxWinners = 0` → tratado como **1** (não como "nenhum").
561
+ - `maxPerPlayer = -1` (padrão) → ilimitado = total de bilhetes.
562
+ - `maxTickets = min(maxWinners * maxPerPlayer, totTickets)` limita o total premiado.
563
+ - Em `first_ticket`/`last_ticket`, `maxPerPlayer` é **ignorado**.
564
+
565
+ ### 5.2 Elegibilidade por `principals` (L276-295)
566
+
567
+ `checkIsPrincipalAllowed`:
568
+ - `principals` null/vazio → **todos** podem participar.
569
+ - Contém o id do jogador/time → permitido.
570
+ - Contém um id de **time** → permitido se o jogador for membro do time (`isUserOfTeam`).
571
+
572
+ `principals` afeta **somente** a criação de bilhetes (`checkRestrictions`) e o universo `who` da execução. **Não** restringe a leitura (`GET /v3/lottery`/`winner`/`participants`).
573
+
574
+ ### 5.3 Loteria não pode receber bilhetes após executada
575
+
576
+ `checkRestrictions` adiciona `was_executed` quando `executedDate != null` (L264-266). A criação de bilhete via v3 é bloqueada (400). (O legado `/2.0.0` **não** valida isso.)
577
+
578
+ ### 5.4 Execução é idempotente por `executedDate`
579
+
580
+ `execute` só registra vencedores se `executedDate == null` (L643). Chamar `execute` duas vezes não duplica vencedores — a segunda chamada seleciona tickets (e pode marcá-los `used`) mas não grava achievements porque a guarda falha. Para reexecutar, é preciso `undoExecute` antes.
581
+
582
+ ### 5.5 Loteria sem bilhetes nunca "fecha" (loop infinito do scheduler)
583
+
584
+ Como `executedDate` só é gravado quando `selected.size() > 0` (L643), uma loteria `autoExecute=true` com `drawDate` no passado e **zero bilhetes** continua aparecendo em `findLotteriesToExecute()` e é reprocessada a cada 5 minutos indefinidamente. Sem efeito de dados (não há vencedor), mas consome ciclos do `AsyncProcessor`.
585
+
586
+ ### 5.6 Recompensa de tipo `LOTTERY_TICKET` é silenciosamente descartada
587
+
588
+ Ver §6 e §7. Diferente do módulo [achievement](achievement.md), onde `fireAction` cria bilhetes para `TYPE_LOTTERY_TICKET`; em `lottery.execute` essa lógica está comentada.
589
+
590
+ ### 5.7 `delete` não limpa vencedores
591
+
592
+ `delete` remove loteria + bilhetes, mas não os achievements `type=5`. Saldos dos jogadores continuam refletindo a loteria removida (ver §4.5).
593
+
594
+ ### 5.8 Multi-tenant
595
+
596
+ Não há campo `apiKey` nos documentos de loteria/bilhete. O isolamento é dado **inteiramente** pelo roteamento de connection do Jongo (`manager.getJongoConnection()`), uma por gamificação.
597
+
598
+ ### 5.9 `random_value` depende de `value` não-null no bilhete sorteado
599
+
600
+ `executeRandomValue` sorteia **um único** bilhete e usa o `value` dele como número vencedor (L529-531). Se esse bilhete tiver `value == null` (L532), o método retorna **lista vazia** — nenhum vencedor, mesmo havendo bilhetes válidos com valor. É uma falha **probabilística**: depende de qual bilhete o `skip(random)` sortear. Diferente de §5.5 (zero bilhetes), aqui **há** bilhetes, mas o sorteio não premia ninguém e a loteria permanece aberta. Mitigação: em loterias `random_value`, garanta que **todos** os bilhetes tenham `value`.
601
+
602
+ ---
603
+
604
+ ## 6. Comportamentos Automáticos
605
+
606
+ | Comportamento | Trigger | Impacto | Persistência |
607
+ |---|---|---|---|
608
+ | `_id` da loteria (`shortTimeMillis`) | `insert` se `_id` null | Cria id curto | Sim |
609
+ | `createdDate = now` | `insert` se null | **Reseta em update sem o campo** | Sim |
610
+ | `choiceMethod = random_ticket` | `insert` se null | Default silencioso | Sim |
611
+ | `folder = default` + cria `LotteryFolder` | `insert` se folder vazio | Cria pasta automática | Sim |
612
+ | `_id`/`created`/`random` do bilhete | `insertTicket` | Defaults silenciosos | Sim |
613
+ | Índice `{random:1}` em `lottery_ticket` | Toda `execute` (L619) | Cria índice (vestigial) | Sim |
614
+ | Achievement `type=5` por vencedor | `execute` | Registra vencedor | Sim (coleção `achievement`) |
615
+ | Trigger `before_win`/`after_win` (entidade `lottery`) | Em torno de `addAchievement` do vencedor | Scripts customizados | Side effects |
616
+ | Trigger `after_execute` | Fim de `execute` com vencedores | Notifica fim do sorteio | Side effects |
617
+ | Recompensas (POINT/CHALLENGE/CATALOG_ITEM) | `execute`, por vencedor | Achievements adicionais + `updateUserStatus` | Sim |
618
+ | Notification `EVENT_WIN` | `execute`, por vencedor | Documentos em `notification` | Sim |
619
+ | `used=true` nos bilhetes | `executeRandomTicket`/`executeRandomValue` | Marca bilhetes processados | Sim |
620
+ | Execução automática | `AsyncProcessor` a cada 5 min | Sorteios `autoExecute` vencidos | Sim |
621
+ | Auto-tag `GT74` | `GameTechniqueManager` (rotina externa) | Preenche `techniques` | Sim |
622
+
623
+ ### Fluxo de recompensas — o ramo `LOTTERY_TICKET` descartado
624
+
625
+ ```mermaid
626
+ flowchart TB
627
+ A["Para cada reward em lottery.rewards"] --> B{"total != 0 E type ∈\n{POINT, CATALOG_ITEM,\nCHALLENGE, LOTTERY_TICKET}?"}
628
+ B -- não --> X["Ignorado (LEVEL, CROWN,\nMYSTERY_BOX, etc.)"]
629
+ B -- sim --> C{"item == FUNIFIER_RANDOM\nE type == CATALOG_ITEM?"}
630
+ C -- sim --> D["catalogManager.findRandom/\nfindValidRandom → a1"]
631
+ C -- não --> E["código de criação de TICKET\nestá COMENTADO (L725-781)"]
632
+ E --> F["else: a1 = Achievement(type=r.type)"]
633
+ D --> G{"collection != null?\n(POINT/CHALLENGE/CATALOG_ITEM)"}
634
+ F --> G
635
+ G -- sim --> H["addAchievement(a1)\n+ triggers + updateUserStatus"]
636
+ G -- não --> I["LOTTERY_TICKET (50):\ncollection = null →\na1 NUNCA é salvo ❌"]
637
+ ```
638
+
639
+ Detalhamento do descarte de `TYPE_LOTTERY_TICKET (50)`:
640
+
641
+ 1. **L694** — o filtro de entrada aceita `type=50`.
642
+ 2. **L703-781** — os ramos que criariam bilhetes (`FUNIFIER_RANDOM` lottery e lottery específica) estão **inteiramente comentados** (`/* ... */`).
643
+ 3. **L783-785** — cai no `else`, criando `a1 = new Achievement(..., r.getType()=50, r.getItem(), ...)`.
644
+ 4. **L795-805** — `collection` só é resolvida para `TYPE_POINT`/`TYPE_CHALLENGE`/`TYPE_CATALOG_ITEM`. Para `type=50`, `collection` permanece `null`.
645
+ 5. **L806** — `if(collection != null)` é **falso** → `a1` **nunca é persistido**, nenhum bilhete é criado, nenhum trigger dispara.
646
+
647
+ Resultado: configurar `rewards: [{type:50,...}]` numa loteria **não tem efeito algum**.
648
+
649
+ ---
88
650
 
89
- ### Executar Sorteio
90
- **Método:** GET
91
- **Endpoint:** `/v3/lottery/:id/execute`
92
- **Descrição:** Executa o sorteio e seleciona os vencedores aleatoriamente.
651
+ ## 7. Suportado vs NÃO Suportado
93
652
 
94
- ### Reverter Execução do Sorteio
95
- **Método:** DELETE
96
- **Endpoint:** `/v3/lottery/:id/execute`
653
+ ### Suportado
97
654
 
98
- ### Listar Vencedores
99
- **Método:** GET
100
- **Endpoint:** `/v3/lottery/winner?lottery=:id`
655
+ - CRUD de loterias via `/v3/lottery` (GET por id, GET lista filtrada, POST, PUT*, DELETE).
656
+ - CRUD de bilhetes (`/v3/lottery/ticket` POST/GET/DELETE).
657
+ - Execução manual (`GET /{id}/execute`) e rollback (`DELETE /{id}/execute`).
658
+ - Execução automática via scheduler (`autoExecute=true` + `drawDate` vencida).
659
+ - 4 métodos de escolha: `random_ticket`, `random_value`, `first_ticket`, `last_ticket`.
660
+ - Limites `maxWinners` e `maxPerPlayer` (com semântica de §5.1).
661
+ - Elegibilidade por jogador/time (`principals`), inclusive expansão de membros de time.
662
+ - Recompensas de **ponto, desafio e item de catálogo** (incl. catálogo aleatório `FUNIFIER_RANDOM`).
663
+ - Notificações `EVENT_WIN` ao vencedor.
664
+ - Triggers `before_win`, `after_win`, `after_execute`.
665
+ - Listagem de vencedores e painel de participantes.
666
+ - Agrupamento em pastas (`folder`).
667
+ - Multi-tenant por roteamento de connection.
668
+
669
+ ### ❌ NÃO Suportado
670
+
671
+ - **Recompensa `TYPE_LOTTERY_TICKET (50)`** — aceita na config mas **silenciosamente descartada** na execução (§6). Não cria bilhete nem achievement.
672
+ - **Métodos `more_tickets` / `less_tickets`** — constantes comentadas em `Lottery.java` (L38, L41).
673
+ - **`PUT` real por path id** — `PUT /v3/lottery/{id}` ignora o `{id}` da URL e usa o `_id` do body; sem `_id` no body, cria loteria nova (§4.4).
674
+ - **Merge parcial em update** — todo save é full-replace; campos omitidos são apagados, `createdDate` é resetada.
675
+ - **Validação de campos "obrigatórios"** — `title`, `description`, `image`, `drawDate`, `link` são marcados "(required)" só nos javadocs; o backend não valida.
676
+ - **Recompensas de LEVEL, CROWN, MYSTERY_BOX, etc.** — não entram no filtro de recompensa (L694).
677
+ - **`recompensa.operation` (deduct), `period`, `perPlayer`, lógica de team challenge** — comentados/ignorados na loteria (L671-689).
678
+ - **Parâmetro `q` em `GET /v3/lottery`** — não existe (existe só em `/ticket`).
679
+ - **Limpeza de vencedores no `DELETE`** — achievements `type=5` ficam órfãos (§5.7).
680
+ - **Reexecução automática após rollback** — `undoExecute` zera `autoExecute` (§2.6).
681
+ - **Atomicidade/transação** — execução grava achievements, status e `executedDate` em passos separados, sem rollback em falha parcial.
682
+ - **Campo `random` do bilhete na escolha** — embora indexado e preenchido, não é usado para sortear neste módulo (`$sample` é usado); é vestígio do `brazilian_lottery`.
683
+
684
+ ---
685
+
686
+ ## 8. Segurança e Permissões
687
+
688
+ - **Autenticação:** v3 exige Bearer token (`AuthBean`); legado `/2.0.0` usa `?api_key=` em query string (menos seguro, evitar).
689
+ - **Autorização:** não há checagem de role/escopo por endpoint. Qualquer token válido do tenant pode criar/executar/deletar loterias. `principals` controla apenas **quem participa**, não quem administra.
690
+ - **Isolamento multi-tenant:** garantido apenas pelo roteamento de connection do Jongo. Não há `apiKey` nos documentos.
691
+ - **Superfície de injeção de pipeline MongoDB:**
692
+ - `GET /v3/lottery/ticket?q=...` — o `q` é concatenado **literalmente** no `$match` (`LotteryManager.findAllTickets`, L354: `query.append(", " + q)`). Um `q` malicioso pode injetar operadores/estágios adicionais. Como em outros módulos, o isolamento por connection limita o impacto ao tenant, mas permite leitura arbitrária dentro da coleção.
693
+ - `GET /v3/lottery/winner?q=...` — repassado ao `AchievementManager.findAllAchievements` (mesma característica de query raw do módulo achievement).
694
+ - **DoS leve não intencional:** loteria `autoExecute` sem bilhetes (§5.5) é reprocessada a cada 5 min — não é injeção, mas é um custo recorrente silencioso.
695
+ - **Falhas convertidas em 500:** ids inexistentes em `execute` (L612) e loteria inexistente/`null` em `checkRestrictions` (L259-264) causam NPE → 500. Não vazam dados, mas dificultam diagnóstico no cliente.
696
+
697
+ ---
698
+
699
+ ## 9. Observabilidade e Troubleshooting
700
+
701
+ ### Diagnóstico rápido
702
+
703
+ ```
704
+ GET /v3/lottery/{id} → confirma existência e executedDate
705
+ GET /v3/lottery/{id}/... (participants) → total de bilhetes e vencedores
706
+ GET /v3/lottery/ticket?lottery={id}&max_results=1000 → bilhetes registrados
707
+ GET /v3/lottery/winner?lottery={id} → vencedores (achievements type=5)
708
+ ```
709
+
710
+ ### Queries MongoDB úteis
711
+
712
+ ```js
713
+ // A loteria foi executada?
714
+ db.lottery.findOne({ _id: "DTj0x5z" }, { executedDate: 1, autoExecute: 1, drawDate: 1 })
715
+
716
+ // Loterias que o scheduler vai (tentar) executar
717
+ db.lottery.find({ executedDate: { $exists: false }, drawDate: { $lte: new Date() }, autoExecute: true })
718
+
719
+ // Quantos bilhetes por jogador
720
+ db.lottery_ticket.aggregate([
721
+ { $match: { lottery: "DTj0x5z" } },
722
+ { $group: { _id: "$player", tickets: { $sum: 1 } } }
723
+ ])
724
+
725
+ // Vencedores (não há coleção "winner" — é achievement type=5)
726
+ db.achievement.find({ type: 5, item: "DTj0x5z" })
727
+
728
+ // Bilhetes já marcados como usados (indício de execução)
729
+ db.lottery_ticket.count({ lottery: "DTj0x5z", used: true })
730
+ ```
731
+
732
+ ### Erros comuns e causas
733
+
734
+ | Sintoma | Causa provável |
735
+ |---|---|
736
+ | `POST /ticket` retorna 500 (NPE) | `lottery` inexistente ou ausente no body → bug L259-264 (§4.6). Verifique se a loteria existe |
737
+ | `POST /ticket` retorna 400 `was_executed` | Loteria já sorteada (`executedDate != null`). Use `undoExecute` para reabrir |
738
+ | `POST /ticket` retorna 400 `principal_not_allowed` | Jogador não está em `principals` (nem em time elegível) |
739
+ | Bilhete "criado" mas não aparece | `lottery`/`player` ausentes → `insertTicket` faz no-op silencioso (L299) |
740
+ | Sorteio "não acontece" automaticamente | `autoExecute=false`, ou `drawDate` no futuro, ou já tem `executedDate` |
741
+ | Sorteio reexecuta sozinho repetidamente | Sem bilhetes → `executedDate` nunca grava (§5.5) |
742
+ | Sorteio `random_value` não premia ninguém apesar de haver bilhetes | O bilhete sorteado tinha `value` null (§5.9). Garanta `value` em todos os bilhetes desse método |
743
+ | Recompensa de bilhete não é creditada | `type=50` é descartado (§6). Use o módulo challenge para distribuir bilhetes |
744
+ | Update via `PUT` "não salva" / cria duplicata | `PUT` ignora o id da URL (§4.4). Envie `_id` no body |
745
+ | `createdDate` mudou após editar | Full-replace re-setou `createdDate` (§3.1) |
746
+ | Após rollback, scheduler não reexecuta | `undoExecute` setou `autoExecute=false` (§2.6) |
747
+ | Saldo do jogador conta loteria deletada | `DELETE /{id}` não remove achievements type=5 (§5.7) |
748
+
749
+ ---
750
+
751
+ ## 10. Exemplos Práticos
752
+
753
+ ### 10.1 Exemplo mínimo (sorteio simples por bilhete aleatório)
754
+
755
+ ```json
756
+ POST /v3/lottery
757
+ Authorization: Bearer <token>
758
+ Content-Type: application/json
759
+ {
760
+ "title": "Sorteio Relâmpago",
761
+ "drawDate": 1690824650503,
762
+ "autoExecute": true,
763
+ "choiceMethod": "random_ticket",
764
+ "maxWinners": 1
765
+ }
766
+ ```
767
+
768
+ Defaults aplicados: `_id` gerado, `createdDate`=agora, `folder`="default", `maxPerPlayer`=-1.
769
+
770
+ ### 10.2 Exemplo avançado (prêmios, elegibilidade, notificação)
771
+
772
+ ```json
773
+ POST /v3/lottery
774
+ {
775
+ "title": "Travel to Cancun",
776
+ "description": "Viagem para Cancún com acompanhante.",
777
+ "image": "https://cdn.exemplo.com/cancun.png",
778
+ "tags": ["viagem", "premium"],
779
+ "drawDate": 1690824650503,
780
+ "autoExecute": true,
781
+ "choiceMethod": "random_ticket",
782
+ "maxWinners": 1,
783
+ "maxPerPlayer": 1,
784
+ "principals": ["team_vendas"],
785
+ "rewards": [
786
+ { "total": 1, "type": 2, "item": "flight_ticket" },
787
+ { "total": 500, "type": 0, "item": "points" }
788
+ ],
789
+ "notifications": [
790
+ { "event": 0, "type": 0, "scope": 0, "content": "Parabéns, você ganhou a viagem!" }
791
+ ]
792
+ }
793
+ ```
794
+
795
+ ```json
796
+ POST /v3/lottery/ticket
797
+ { "lottery": "<id_retornado>", "player": "me" }
798
+ ```
799
+
800
+ ```
801
+ GET /v3/lottery/<id>/execute # sorteia 1 vencedor entre membros de team_vendas
802
+ ```
803
+
804
+ ### 10.3 Anti-pattern (o que NÃO fazer)
805
+
806
+ ```json
807
+ // ❌ Recompensa de bilhete de loteria — silenciosamente ignorada (§6)
808
+ {
809
+ "title": "Acumule bilhetes",
810
+ "rewards": [ { "total": 5, "type": 50, "item": "outra_loteria" } ]
811
+ }
812
+ // type=50 não cria bilhetes nem achievements. Para distribuir bilhetes,
813
+ // use rewards de TYPE_LOTTERY_TICKET em um CHALLENGE, não em uma loteria.
814
+ ```
815
+
816
+ ```
817
+ // ❌ "Atualizar" a loteria sem reenviar o _id
818
+ PUT /v3/lottery/DTj0x5z
819
+ { "title": "Novo título" }
820
+ // Cria uma loteria NOVA (id gerado), não atualiza a DTj0x5z (§4.4).
821
+ // Correto: incluir "_id": "DTj0x5z" no body e reenviar TODOS os campos.
822
+ ```
823
+
824
+ ```
825
+ // ❌ Esperar que DELETE limpe vencedores
826
+ DELETE /v3/lottery/DTj0x5z
827
+ // Os achievements type=5 dos vencedores permanecem. Para reverter prêmios,
828
+ // rode DELETE /v3/lottery/DTj0x5z/execute (undoExecute) ANTES de deletar.
829
+ ```
101
830
 
102
- ### Listar Participantes
103
- **Método:** GET
104
- **Endpoint:** `/v3/lottery/participants?lottery=:id`
831
+ ---
105
832
 
106
- ## Validações e Testes
833
+ ## Checklist de Configuração
107
834
 
108
- - [ ] Sorteio aparece na lista GET /v3/lottery
109
- - [ ] Cupom é criado corretamente para jogador
110
- - [ ] Ao executar sorteio, vencedor é selecionado
111
- - [ ] Lista de vencedores retorna dados corretos
112
- - [ ] Reverter execução funciona
835
+ - [ ] `title`/`description`/`image` preenchidos (o backend **não** valida, mas o Studio espera).
836
+ - [ ] `drawDate` definida (necessária para execução automática).
837
+ - [ ] `choiceMethod` escolhido entre `random_ticket` | `random_value` | `first_ticket` | `last_ticket` (default `random_ticket`).
838
+ - [ ] `maxWinners`/`maxPerPlayer` ajustados (lembre: `-1`=ilimitado, `0`→1).
839
+ - [ ] `autoExecute=true` **somente** se quiser execução pelo scheduler (e garanta que haverá bilhetes — §5.5).
840
+ - [ ] `rewards` usam apenas `type` 0/1/2 (ponto/desafio/catálogo). **`type=50` não funciona** (§6).
841
+ - [ ] Bilhetes existem **antes** do `drawDate` (sorteio sem bilhetes não fecha).
842
+ - [ ] `principals` definido se a loteria for restrita a jogadores/times específicos.
843
+ - [ ] Para distribuir **bilhetes como recompensa**, configure no módulo `challenge`, não em `lottery.rewards`.
844
+ - [ ] Antes de `DELETE` de loteria executada, rode `undoExecute` para não deixar achievements órfãos.
845
+ - [ ] Em update, reenvie o `_id` no body e todos os campos (full-replace + PUT ignora id da URL).