funifier-mcp 0.2.26 → 0.2.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/.cursor/rules/funifier.mdc +38 -41
  2. package/.github/copilot-instructions.md +38 -41
  3. package/AGENTS.md +56 -49
  4. package/README.md +40 -22
  5. package/datasource-funifier-docs/.coverage.json +326 -0
  6. package/datasource-funifier-docs/.validation.json +593 -0
  7. package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
  8. package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
  9. package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
  10. package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
  11. package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
  12. package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
  13. package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
  14. package/datasource-funifier-docs/knowledge/index.md +4 -1
  15. package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
  16. package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
  17. package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
  18. package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
  19. package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
  20. package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
  21. package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
  22. package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
  23. package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
  24. package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
  25. package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
  26. package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
  27. package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
  28. package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
  29. package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
  30. package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
  31. package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
  32. package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
  33. package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
  34. package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
  35. package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
  36. package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
  37. package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
  38. package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
  39. package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
  40. package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
  41. package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
  42. package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
  43. package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
  44. package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
  45. package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
  46. package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
  47. package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
  48. package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
  49. package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
  50. package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
  51. package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
  52. package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
  53. package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
  54. package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
  55. package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
  56. package/dist/cli/init.d.ts.map +1 -1
  57. package/dist/cli/init.js +42 -1
  58. package/dist/cli/init.js.map +1 -1
  59. package/dist/cli/init.test.js +74 -3
  60. package/dist/cli/init.test.js.map +1 -1
  61. package/dist/cli/persona.d.ts +3 -0
  62. package/dist/cli/persona.d.ts.map +1 -0
  63. package/dist/cli/persona.js +25 -0
  64. package/dist/cli/persona.js.map +1 -0
  65. package/dist/mcp/bundle.js +119 -93
  66. package/dist/mcp/check-update.d.ts +5 -0
  67. package/dist/mcp/check-update.d.ts.map +1 -1
  68. package/dist/mcp/check-update.js +21 -10
  69. package/dist/mcp/check-update.js.map +1 -1
  70. package/dist/mcp/check-update.test.d.ts +2 -0
  71. package/dist/mcp/check-update.test.d.ts.map +1 -0
  72. package/dist/mcp/check-update.test.js +33 -0
  73. package/dist/mcp/check-update.test.js.map +1 -0
  74. package/dist/mcp/index.js +2 -2
  75. package/dist/mcp/index.js.map +1 -1
  76. package/dist/mcp/prompts/templates.d.ts.map +1 -1
  77. package/dist/mcp/prompts/templates.js +35 -0
  78. package/dist/mcp/prompts/templates.js.map +1 -1
  79. package/dist/mcp/resources/documentation.d.ts +1 -1
  80. package/dist/mcp/resources/documentation.d.ts.map +1 -1
  81. package/dist/mcp/resources/documentation.js +39 -3
  82. package/dist/mcp/resources/documentation.js.map +1 -1
  83. package/dist/mcp/tools/connect.d.ts.map +1 -1
  84. package/dist/mcp/tools/connect.js +18 -8
  85. package/dist/mcp/tools/connect.js.map +1 -1
  86. package/dist/mcp/tools/database.d.ts.map +1 -1
  87. package/dist/mcp/tools/database.js +59 -47
  88. package/dist/mcp/tools/database.js.map +1 -1
  89. package/dist/mcp/tools/database.test.js +2 -2
  90. package/dist/mcp/tools/database.test.js.map +1 -1
  91. package/dist/mcp/tools/delete.d.ts.map +1 -1
  92. package/dist/mcp/tools/delete.js +13 -3
  93. package/dist/mcp/tools/delete.js.map +1 -1
  94. package/dist/mcp/tools/execute.d.ts.map +1 -1
  95. package/dist/mcp/tools/execute.js +20 -9
  96. package/dist/mcp/tools/execute.js.map +1 -1
  97. package/dist/mcp/tools/folder.d.ts.map +1 -1
  98. package/dist/mcp/tools/folder.js +22 -12
  99. package/dist/mcp/tools/folder.js.map +1 -1
  100. package/dist/mcp/tools/get.d.ts.map +1 -1
  101. package/dist/mcp/tools/get.js +16 -6
  102. package/dist/mcp/tools/get.js.map +1 -1
  103. package/dist/mcp/tools/index.d.ts +1 -1
  104. package/dist/mcp/tools/index.d.ts.map +1 -1
  105. package/dist/mcp/tools/index.js +28 -1
  106. package/dist/mcp/tools/index.js.map +1 -1
  107. package/dist/mcp/tools/list.d.ts.map +1 -1
  108. package/dist/mcp/tools/list.js +38 -14
  109. package/dist/mcp/tools/list.js.map +1 -1
  110. package/dist/mcp/tools/logs.d.ts.map +1 -1
  111. package/dist/mcp/tools/logs.js +15 -5
  112. package/dist/mcp/tools/logs.js.map +1 -1
  113. package/dist/mcp/tools/save.d.ts.map +1 -1
  114. package/dist/mcp/tools/save.js +14 -4
  115. package/dist/mcp/tools/save.js.map +1 -1
  116. package/dist/mcp/tools/save.test.js +3 -3
  117. package/dist/mcp/tools/save.test.js.map +1 -1
  118. package/dist/mcp/tools/search-docs.d.ts +3 -0
  119. package/dist/mcp/tools/search-docs.d.ts.map +1 -0
  120. package/dist/mcp/tools/search-docs.js +102 -0
  121. package/dist/mcp/tools/search-docs.js.map +1 -0
  122. package/package.json +6 -2
  123. package/skills/acquire-funifier-knowledge/SKILL.md +155 -0
  124. package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
  125. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
  126. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
  127. package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
  128. package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
  129. package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
  130. package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +86 -0
  131. package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
  132. package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
  133. package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
  134. package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
  135. package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
  136. package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
  137. package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
  138. package/skills/funifier/SKILL.md +88 -0
  139. package/skills/funifier/references/configure-security.md +96 -0
  140. package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
  141. package/skills/funifier/references/create-aggregate.md +144 -0
  142. package/skills/funifier/references/create-challenge.md +116 -0
  143. package/skills/funifier/references/create-competition.md +98 -0
  144. package/skills/funifier/references/create-crossword.md +574 -0
  145. package/skills/funifier/references/create-custom-object.md +91 -0
  146. package/skills/funifier/references/create-custom-page.md +135 -0
  147. package/skills/funifier/references/create-folder.md +104 -0
  148. package/skills/funifier/references/create-lastmile.md +643 -0
  149. package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
  150. package/skills/funifier/references/create-level.md +94 -0
  151. package/skills/funifier/references/create-lottery.md +913 -0
  152. package/skills/funifier/references/create-mystery.md +769 -0
  153. package/skills/funifier/references/create-notification.md +75 -0
  154. package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
  155. package/skills/funifier/references/create-quiz.md +98 -0
  156. package/skills/funifier/references/create-scheduler.md +141 -0
  157. package/skills/funifier/references/create-story.md +636 -0
  158. package/skills/funifier/references/create-swap.md +95 -0
  159. package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
  160. package/skills/funifier/references/create-virtual-good.md +96 -0
  161. package/skills/funifier/references/create-webhook.md +72 -0
  162. package/skills/funifier/references/create-websocket.md +71 -0
  163. package/skills/funifier/references/create-widget.md +76 -0
  164. package/skills/funifier/references/debug.md +87 -0
  165. package/skills/funifier/references/help.md +81 -0
  166. package/skills/funifier/references/implement-frontend.md +106 -0
  167. package/skills/funifier/references/import-csv.md +75 -0
  168. package/skills/funifier/references/manage-player.md +82 -0
  169. package/skills/funifier/references/manage-team.md +76 -0
  170. package/skills/funifier/references/upload-file.md +91 -0
  171. package/skills/funifier-create-aggregate/SKILL.md +0 -127
  172. package/skills/funifier-create-challenge/SKILL.md +0 -88
  173. package/skills/funifier-create-custom-page/SKILL.md +0 -127
  174. package/skills/funifier-create-level/SKILL.md +0 -87
  175. package/skills/funifier-create-quiz/SKILL.md +0 -87
  176. package/skills/funifier-create-scheduler/SKILL.md +0 -127
  177. package/skills/funifier-create-virtual-good/SKILL.md +0 -87
  178. package/skills/funifier-debug/SKILL.md +0 -92
  179. package/skills/funifier-help/SKILL.md +0 -86
  180. package/skills/funifier-implement-frontend/SKILL.md +0 -90
  181. package/skills/funifier-index/SKILL.md +0 -58
@@ -1,189 +1,931 @@
1
- # Trigger (Trigger)
1
+ # `trigger`
2
2
 
3
3
  **Acesso Studio:** `/studio/trigger`
4
4
  **API Endpoint:** `/v3/trigger`
5
+ **Coleção MongoDB:** `trigger` (definições) + `trigger_log` (logs de execução)
5
6
 
6
- ## O que é
7
+ ---
7
8
 
8
- Execução automática de códigos JAVA quando eventos específicos acontecem na gamificação. Permite programar ações customizadas disparadas por eventos, como o cadastro de um jogador ou a realização de uma compra. As triggers dão liberdade para alterar o comportamento padrão dos módulos, integrar com sistemas externos ou implementar automações em tempo real.
9
+ ## 1. Visão Geral
9
10
 
10
- ## Quando usar
11
+ O módulo `trigger` é o motor de **execução de scripts Groovy** acionados por eventos do ciclo de vida das entidades da plataforma. Cada documento `trigger` armazena um script Groovy que será compilado, cacheado em memória e executado quando uma combinação `(entity, event)` for disparada por algum outro manager do serviço.
11
12
 
12
- - Para enviar emails automáticos (ex: boas-vindas)
13
- - Para integrar com APIs externas (Zapier, CRM, etc.)
14
- - Para alterar pontuação com regras especiais
15
- - Para executar lógica de negócio personalizada
16
- - **Importante:** usar apenas quando as configurações padrão não atendem
13
+ Papel arquitetural:
17
14
 
18
- ## Checklist de Configuração no Studio
15
+ - Único ponto de extensibilidade server-side da plataforma — permite estender qualquer comportamento da gamificação sem recompilar o serviço.
16
+ - Compila o script com `GroovyClassLoader` configurado por `SecureASTCustomizer` + `TriggerExpressionChecker` (sandbox parcial — ver seção 8).
17
+ - Mantém cache de classes compiladas indexado por `trigger.id` (`TriggerManager.compiled`), invalidado por `trigger.updated`.
18
+ - Executa cada disparo em uma thread dedicada (`Executors.newSingleThreadExecutor()`), com timeout duro por trigger.
19
+ - É chamado de forma síncrona — `TriggerManager.execute(...)` é uma chamada bloqueante que aguarda todos os triggers daquela combinação `(entity, event)` rodarem antes de retornar ao manager chamador.
19
20
 
20
- - [ ] Definir nome e descrição da trigger
21
- - [ ] Definir entidade observada (player, challenge, action, achievement, etc.)
22
- - [ ] Definir evento (before_create, after_create, before_win, after_win, etc.)
23
- - [ ] Escrever script Java com método `void trigger(event, entity, player, database)`
24
- - [ ] Testar trigger em ambiente de homologação
21
+ Quem dispara o módulo (encontrado no código `getTriggerManager().execute(...)`):
25
22
 
26
- ## Eventos Disponíveis
23
+ - `ActionManager.trackSynchonous` — `(action, before_win)` e `(action, after_win)` em volta do `addLog`.
24
+ - `AchievementManager.fireAction` — `(point_category, before/after_win)`, `(challenge, before/after_win)`, `(<collection>, before/after_win)` em torno de cada Achievement persistido.
25
+ - `AchievementManager.evaluateLevelUp` — `(level, before/after_win)` no instante do level up.
26
+ - `CharacterStarManager.evaluateLevelUp` — `(character_star_stats_level, before/after_win)`.
27
+ - `DatabaseManager.bulkInsert` — `(<collection>, before_bulk)`, `(<collection>, before_create)`, `(<collection>, after_create)` por item, `(<collection>, after_bulk)` no final.
28
+ - `DatabaseRest.insert/update/delete` — `(<collection>, before_create|after_create|before_update|after_update|before_delete|after_delete)`.
29
+ - `CatalogManager.purchase` / `PurchaseManager.buy` — `(catalog_item, before_purchase_validation)`, `(catalog_item, before_win)`, `(catalog_item, after_win)`.
30
+ - `SwapManager.*` — `(swap, before/after_create|delete|win)`, `(swap, before_acquire_validation)`, `(swap_counter_offer, ...)`.
31
+ - `MysteryBoxManager.execute` — `(mystery_box, before_win)`, `(mystery_box, before_lose)`, `(mystery_box, before_win_reward)`, `(mystery_box, after_win|after_lose)`.
32
+ - `LotteryManager.execute` — `(lottery_ticket, before/after_win)`.
33
+ - `CompetitionManager.execute/insert/insertJoin` — `(competition, ...)`.
34
+ - `CrowningManager.calculateLeaderBoardV3/calculateCrown` — `(crown, ...)`.
35
+ - `QuizManager.insert/start/finish`, `QuestionManager.insert/insertLog` — eventos `before/after_create|update`.
36
+ - `FolderManager.insertLog/deleteLog` / `FolderRest.progress` — `(folder_log, before/after_create|delete)` e `("folder_progress", after_create)`.
37
+ - `PlayerManager.insert/delete`, `PlayerRest.changePassword(Request)` — `(player, before/after_create|delete)` e `(password_change, ...)`.
38
+ - `BackupManager.asyncExecute`, `CompactManager.asyncExecute`, `BetManager.delete`, `CsvManager.insertBulkSync(Helper)`, `UploadRest.uploadFile`, `CrmManager.*` — disparam eventos próprios sobre suas coleções.
27
39
 
28
- | Prefixo | Sufixo | Descrição |
29
- |---------|--------|-----------|
30
- | before_ | create | Antes de criar |
31
- | after_ | create | Depois de criar |
32
- | before_ | update | Antes de atualizar |
33
- | after_ | delete | Depois de deletar |
34
- | before_ | win | Antes de conquistar |
35
- | after_ | win | Depois de conquistar |
40
+ Total: **29 arquivos** invocando `manager.getTriggerManager().execute(...)`, **146 sites de chamada** no `src/main/java` (sem testes).
36
41
 
37
- ## Entidades Disponíveis
42
+ Relação com outros módulos:
38
43
 
39
- - `player`, `action`, `challenge`, `achievement`, `level`, `catalog_item`, `lottery`, `mystery_box`, `competition`, `question_log`, `<custom>__c`
44
+ - **Statistic / GamificationLimits** antes de cada execução, `SystemFactory.getInstance().getStatisticManager(apiKey).newTriggerExecution(trigger.id)` é chamado. Se o limite diário (`GamificationLimits.dailyTriggerExecutions`) for excedido, a execução é silenciosamente bloqueada (apenas um `System.out.println` de erro).
45
+ - **TriggerExpressionChecker** — bloqueia uso de `.execute`, `.getDB`, `.getMongo`, `.dropDatabase` e dos tipos `System`, `ProcessBuilder`, `File`, `GroovyShell`, `GroovyObject` no AST.
46
+ - **GameDao** — wrapper fino sobre Jongo que é injetado como `database` em todo script.
40
47
 
41
- ## API Endpoints
48
+ ---
42
49
 
43
- ### Listar Triggers
44
- **Método:** GET
45
- **Endpoint:** `/v3/trigger`
50
+ ## 2. Arquitetura e Fluxos
46
51
 
47
- ### Criar Trigger
48
- **Método:** POST
49
- **Endpoint:** `/v3/trigger`
52
+ ### 2.1 Classes envolvidas
50
53
 
51
- **Exemplo de Body:**
52
- ```json
53
- {
54
- "name": "Make the player's name uppercase",
55
- "_id": "DTv7uHc",
56
- "description": "Before creating the player, change the letters of the name to uppercase",
57
- "entity": "player",
58
- "event": "before_create",
59
- "script": "void trigger(event, entity, player, database){ entity.name = entity.name.toUpperCase(); }"
54
+ | Classe | Arquivo | Papel |
55
+ |---|---|---|
56
+ | `Trigger` | `engine/integration/trigger/Trigger.java` | Entidade persistida — script + metadata. |
57
+ | `Reference` | `engine/integration/trigger/Reference.java` | Subdocumento (vínculo visual com outros itens da estratégia). |
58
+ | `ObjectStyle` | `engine/integration/trigger/ObjectStyle.java` | Subdocumento (`background`, `visible`) para apresentação no Studio. |
59
+ | `TriggerContext` | `engine/integration/trigger/TriggerContext.java` | Objeto de transporte runtime (`manager`, `origin`, `extra`). |
60
+ | `TriggerDaoMongo` | `engine/integration/trigger/TriggerDaoMongo.java` | DAO Jongo direto (CRUD + `findByEntityAndEvent`). |
61
+ | `TriggerManager` | `engine/integration/trigger/TriggerManager.java` | Orquestrador — mantém os caches `compiled` e `dates`. |
62
+ | `TriggerRunner` | `engine/integration/trigger/TriggerRunner.java` | Compila e roda o script em executor isolado com timeout. |
63
+ | `TriggerExpressionChecker` | `engine/integration/trigger/TriggerExpressionChecker.java` | `SecureASTCustomizer.ExpressionChecker` — bloqueia tokens/classes proibidas. |
64
+ | `TriggerLog` | `engine/integration/trigger/TriggerLog.java` | Documento gravado em `trigger_log` após cada execução. |
65
+ | `GameDao` | `engine/integration/trigger/GameDao.java` | Wrapper sobre Jongo passado ao script como `database`. |
66
+ | `TriggerStatistics` | `engine/stats/TriggerStatistics.java` | Contadores agregados de execução por trigger / dia / hora / mês. |
67
+ | `TriggerDetailStatistics` | `engine/stats/TriggerDetailStatistics.java` | Mesmas estatísticas por `trigger.id`. |
68
+ | `TriggerHtml` | `engine/integration/html/TriggerHtml.java` | **Não é trigger server-side.** Modelo client-side — coleção `trigger_html` — usado pelo SDK web. |
69
+ | `TriggerRest` | `rest/v3/rest/TriggerRest.java` | Endpoints REST `/v3/trigger`. |
70
+
71
+ ### 2.2 Pipeline principal — `TriggerManager.execute(id, o, entity, event, player, context)`
72
+
73
+ ```
74
+ [1] start = System.currentTimeMillis()
75
+ [2] runner = new TriggerRunner()
76
+ [3] Para cada trigger em mongo.findByEntityAndEvent(entity, event):
77
+ [3.1] limitOk = SystemFactory.getInstance()
78
+ .getStatisticManager(manager.getApiKey())
79
+ .newTriggerExecution(trigger.id)
80
+ [3.2] Se limitOk:
81
+ runner.run(trigger, o, player, manager, context)
82
+ Senão:
83
+ System.out.println("Error | ... You have exceeded your trigger daily executions limits")
84
+ (trigger NÃO executa, NÃO há log gerado, NÃO há exceção propagada)
85
+ [3.3] Se qualquer Exception subir do runner:
86
+ e.printStackTrace() — engole o erro, continua para próximo trigger
87
+ [4] Retorna (int) ms gastos no laço inteiro
88
+ ```
89
+
90
+ Observações críticas:
91
+
92
+ - O parâmetro `id` é o "id do item que originou o evento" (ex: `action.getActionId()`) — **NÃO é o id da trigger**. Ele não é usado para filtrar quais triggers rodam; serve apenas como dado disponível ao script.
93
+ - `findByEntityAndEvent` é uma query MongoDB direta — **não há cache de triggers ativos** (mas há cache de classes compiladas):
94
+ ```js
95
+ db.trigger.find({ entity: <entity>, event: <event> })
96
+ ```
97
+ - Se ninguém criou um trigger para `(entity, event)`, o laço é um no-op (`O(1)` do MongoDB) e retorna em milissegundos.
98
+
99
+ ### 2.3 Diagrama — Pipeline de avaliação
100
+
101
+ ```mermaid
102
+ flowchart LR
103
+ A[Caller<br/>ex: ActionManager.trackSynchonous] --> B[TriggerManager.execute<br/>id, o, entity, event, player, ctx]
104
+ B --> C[mongo.findByEntityAndEvent]
105
+ C --> D{Para cada<br/>trigger}
106
+ D --> E[StatisticManager<br/>newTriggerExecution]
107
+ E -->|limit OK| F[TriggerRunner.run]
108
+ E -->|excedido| X[System.out.println<br/>silencia]
109
+ F --> G[Cache de classe<br/>compiled trigger.id]
110
+ G -->|hit + dates &gt; updated| H[executor.submit<br/>Callable]
111
+ G -->|miss / stale| I[getScript<br/>compile<br/>cache]
112
+ I --> H
113
+ H --> J{future.get<br/>timeout}
114
+ J -->|OK| K[addLog trigger_log]
115
+ J -->|TimeoutException| L[exceptions.add<br/>future.cancel<br/>executor.shutdownNow]
116
+ L --> K
117
+ D -->|throw qualquer| Y[e.printStackTrace<br/>continua loop]
118
+ ```
119
+
120
+ ### 2.4 Sub-pipeline — `TriggerRunner.run(...)`
121
+
122
+ ```
123
+ [1] start = System.currentTimeMillis()
124
+ [2] exceptions = []; outputs = []
125
+ [3] Se trigger.updated == null:
126
+ manager.getTriggerManager().add(trigger)
127
+ (efeito colateral: persiste o trigger no mongo só para popular updated;
128
+ usado pelo modo cluster — qualquer nó que receber um trigger sem updated
129
+ força um save antes de seguir)
130
+ [4] lastCompilation = TriggerManager.dates.get(trigger.id)
131
+ [5] clazz = TriggerManager.compiled.get(trigger.id)
132
+ [6] Se clazz != null && lastCompilation > trigger.updated:
133
+ (cache hit) executor(clazz, ...)
134
+ Senão:
135
+ script = getScript(trigger) # monta imports + wrapper + getScript()
136
+ clazz = compile(script) # GroovyClassLoader com SecureASTCustomizer
137
+ compiled.put(trigger.id, clazz)
138
+ dates.put(trigger.id, now)
139
+ executor(clazz, ...)
140
+ (se compile falhar → CompilationFailedException é capturada,
141
+ exceptions.add(e.getMessage()), nenhuma classe é cacheada)
142
+ [7] ms = elapsed
143
+ [8] trigger_log.save( new TriggerLog(trigger.id, trigger.id, outputs, exceptions, ms) )
144
+ [9] Retorna Map { trigger, exceptions, outputs, millis }
145
+ ```
146
+
147
+ ### 2.5 Sub-pipeline — `executor(clazz, ...)`
148
+
149
+ ```
150
+ [1] timeout = (trigger.timeout != null && > 0) ? trigger.timeout : 5
151
+ # default 5 segundos; valor literal no código, sem configuração externa
152
+ [2] executor = Executors.newSingleThreadExecutor()
153
+ [3] GroovyObject obj = clazz.newInstance()
154
+ [4] future = executor.submit(callable):
155
+ obj.invokeMethod("setContext", new Object[]{ context })
156
+ obj.invokeMethod("setManager", new Object[]{ manager })
157
+ obj.invokeMethod("trigger", new Object[]{
158
+ trigger.getEvent(), o, player, new GameDao(jongo, manager)
159
+ })
160
+ obj.invokeMethod("addAllOutput", new Object[]{ outputs })
161
+ [5] future.get(timeout, TimeUnit.SECONDS)
162
+ [6] Catches:
163
+ MultipleCompilationErrorsException → exceptions.add; future.cancel(true); executor.shutdownNow
164
+ SecurityException → exceptions.add; future.cancel(true); executor.shutdownNow
165
+ TimeoutException → exceptions.add("Timeout after Xs (timeout allowed Ys)");
166
+ exceptions.add(e.getMessage()); future.cancel(true); shutdownNow
167
+ InterruptedException → exceptions.add(...) (NÃO chama future.cancel — bug latente)
168
+ ExecutionException → exceptions.add; future.cancel(true); shutdownNow
169
+ Exception (catch-all) → exceptions.add; future.cancel(true); shutdownNow
170
+ ```
171
+
172
+ ### 2.6 Sub-pipeline — `getScript(trigger)` — wrapper de imports
173
+
174
+ `TriggerRunner.getScript` constrói um script que envelopa o `trigger.script` do usuário em uma classe `FunifierTrigger` Groovy com:
175
+
176
+ 1. ~40 imports automáticos (groovyx.net.http, Unirest, Apache HttpClient, Thumbnailator, javamail-simple, todos os domains do Funifier — `Action`, `ActionLog`, `Achievement`, `Lottery`, `Challenge`, `Player`, `Point`, `Team`, `Catalog`, `CatalogItem`, `Notification`, `FunifierMail`, `DateUtil`, `JsonUtil`, `ExcelUtil`, `HttpUtil`, `MustacheUtils`, `ManagerFactory`, `TriggerContext` etc.).
177
+ 2. `@TimedInterrupt(value = 200L, unit = TimeUnit.SECONDS)` na classe (limite por thread, distinto do timeout do `future.get`).
178
+ 3. Campos `TriggerContext context`, `ManagerFactory manager`, `List output`.
179
+ 4. Setters: `setContext(TriggerContext)`, `setManager(ManagerFactory)`.
180
+ 5. Override de `println(msg)` → `output.add(msg)` (todos os `println` viram `outputs` no `TriggerLog`).
181
+ 6. `addAllOutput(List out)` → `out.addAll(output)`.
182
+ 7. O script do usuário (`trigger.getScript()`) — espera uma assinatura `void trigger(event, entity, player, database) { ... }`.
183
+
184
+ Cabeçalho final efetivo do script:
185
+
186
+ ```groovy
187
+ @TimedInterrupt(value = 200L, unit = TimeUnit.SECONDS)
188
+ class FunifierTrigger {
189
+ TriggerContext context = null
190
+ ManagerFactory manager = null
191
+ List output = new ArrayList()
192
+ void setContext(TriggerContext c) { context = c }
193
+ void setManager(ManagerFactory c) { manager = c }
194
+ void println(msg) { output.add(msg) }
195
+ void addAllOutput(List out) { out.addAll(output) }
196
+
197
+ // <<< trigger.script é concatenado aqui >>>
60
198
  }
61
199
  ```
62
200
 
63
- ### Deletar Trigger
64
- **Método:** DELETE
65
- **Endpoint:** `/v3/trigger/:id`
201
+ ### 2.7 Diagrama — Interação cross-módulo (ActionManager.trackSynchonous)
202
+
203
+ ```mermaid
204
+ sequenceDiagram
205
+ autonumber
206
+ participant Caller as Caller (REST)
207
+ participant AM as ActionManager
208
+ participant TM as TriggerManager
209
+ participant SM as StatisticManager
210
+ participant TR as TriggerRunner
211
+ participant Mongo as Mongo (trigger / trigger_log)
212
+
213
+ Caller->>AM: trackSynchonous(actionLog)
214
+ AM->>AM: ctx = new TriggerContext()<br/>ctx.extra["achievements"] = []
215
+ AM->>TM: execute(actionId, actionLog, "action", "before_win", userId, ctx)
216
+ TM->>Mongo: find({entity:"action", event:"before_win"})
217
+ loop cada trigger
218
+ TM->>SM: newTriggerExecution(trigger.id)
219
+ alt limite OK
220
+ TM->>TR: run(trigger, actionLog, userId, manager, ctx)
221
+ TR->>TR: compile (ou cache hit)
222
+ TR->>TR: invokeMethod("trigger", [event,o,player,gameDao])
223
+ TR->>Mongo: trigger_log.save(...)
224
+ else excedeu
225
+ TM->>TM: stdout error (silencia)
226
+ end
227
+ end
228
+ AM->>AM: actionManager.addLog(actionLog)
229
+ AM->>TM: execute(actionId, actionLog, "action", "after_win", userId, ctx)
230
+ AM->>AM: result = achievementManager.fireAction(actionLog)
231
+ AM->>Caller: List<Achievement>
232
+ ```
233
+
234
+ ### 2.8 Política de timeouts (dupla — relevante para debug)
235
+
236
+ | Limite | Onde está | Valor | O que acontece |
237
+ |---|---|---|---|
238
+ | `trigger.timeout` (segundos) | `TriggerRunner.executor` linha 266–269 | default `5`, sobrescrito pelo campo se `> 0` | `future.get` lança `TimeoutException`, exception registrada no `trigger_log`, `executor.shutdownNow` mata a thread. |
239
+ | `@TimedInterrupt(value = 200L, ...)` | `TriggerRunner.getScript` linha 164 | **200 segundos fixo** | Anotação Groovy que injeta checks em loops/métodos. Trava o script se `200s` excederem mesmo dentro de um único método. **Não respeita `trigger.timeout`** — é um limite teto independente. |
240
+
241
+ Implicação operacional: definir `trigger.timeout = 600` **não vai funcionar**. O `future.get` chega aos `600s` no executor, mas o `@TimedInterrupt` da própria classe Groovy aborta em `200s`. Para todos os efeitos, o teto real é `min(trigger.timeout, 200)`.
242
+
243
+ ### 2.9 Cache de classes compiladas
244
+
245
+ `TriggerManager` mantém **dois mapas em memória de processo** (não distribuídos):
246
+
247
+ - `Map<String, Class> compiled` — chave = `trigger.id`, valor = classe Groovy compilada.
248
+ - `Map<String, Date> dates` — chave = `trigger.id`, valor = data da última compilação.
249
+
250
+ Invalidação:
251
+
252
+ | Evento | Efeito |
253
+ |---|---|
254
+ | `add(trigger)` / `update(trigger)` | Faz `compiled.remove(id)` e `dates.remove(id)` após o save. |
255
+ | `delete(id)` | Idem. |
256
+ | `clear(trigger)` (via `PUT /v3/trigger/compile/{id}`) | Idem. |
257
+ | `PUT /v3/trigger/compile/all` | Itera todos e chama `clear` em cada um. |
258
+ | `TriggerRunner.run` (cache hit) | Reutiliza a classe **apenas se** `dates.get(id).getTime() > trigger.updated.getTime()`. Caso contrário, recompila. |
259
+
260
+ **Cluster awareness:** o comentário `//cluster:...` no código indica que essa lógica foi feita para um ambiente multi-nó — cada nó tem seu próprio cache, e a comparação `dates > updated` recompila localmente quando um nó vizinho atualizou o trigger no Mongo.
261
+
262
+ ---
263
+
264
+ ## 3. Estrutura dos Objetos
265
+
266
+ ### 3.1 `Trigger` — documento raiz (`trigger`)
267
+
268
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
269
+ |---|---|---|---|---|
270
+ | `_id` | String | `Guid.shortTimeMillis()` no `add` se ausente | — | Identificador único. |
271
+ | `name` | String | — | recomendado | Rótulo legível. Usado nos filtros `findAll`. |
272
+ | `description` | String | — | não | Texto livre. **Não é validado/indexado**. |
273
+ | `entity` | String | — | **sim** | Nome da coleção/entidade alvo (ex: `action`, `point_category`, `challenge`, `level`, `lottery_ticket`, `swap`, `mystery_box`, ou qualquer string usada por chamadores customizados como `"deal"`, `"folder_progress"`). |
274
+ | `event` | String | — | **sim** | Tipo de evento (ver enums abaixo + tabela 3.4). |
275
+ | `script` | String | — | **sim** | Código Groovy do corpo. Espera definir `void trigger(event, entity, player, database) { ... }`. |
276
+ | `creation` | Date | `new Date()` no `add` se ausente | — | Timestamp de criação. |
277
+ | `updated` | Date | `new Date()` em todo `add`/`update` | — | Usado para invalidar cache em cluster. Sobrescreve `creation`-like updates. |
278
+ | `timeout` | Long | `null` → cai para `5` segundos no executor | não | Timeout de execução em **segundos**. Teto real efetivo `min(timeout, 200)` por causa do `@TimedInterrupt`. |
279
+ | `techniques` | `List<String>` | `null` | não | Lista de códigos de técnica de jogo (GT...) para metadata no Studio. **Apenas persiste — não é lida pelo runtime.** |
280
+ | `style` | `ObjectStyle` | `null` | não | Atributos visuais (`background`, `visible`). Usado apenas pelo Studio. |
281
+ | `references` | `List<Reference>` | `null` | não | Vínculo visual com outros itens da estratégia (consumido por relatórios do Studio). |
282
+ | `errors` | `String[]` | `null` | — | **Campo legado** — escrito apenas pelo método comentado `SaveTrigger(...)` que não é mais chamado. Permanece no schema (com getter/setter) mas nunca é populado no fluxo atual. |
283
+ | `logs` | `String[]` | `null` | — | **Campo legado** — mesma situação de `errors`. O log moderno vai para a coleção `trigger_log`. |
284
+
285
+ Configuração de serialização:
286
+
287
+ - `@JsonIgnoreProperties(ignoreUnknown=true)` — qualquer campo extra enviado no JSON é **silenciosamente descartado**.
288
+ - `@JsonProperty("_id")` no `id` — o JSON da API expõe `_id`.
289
+ - `JsonUtil.toJsonRemoveNullFields` nos endpoints — campos `null` (`description`, `creation`, `errors`, `logs`, `style`, `references`, `techniques`, `timeout`, `updated`) **não aparecem no JSON de resposta**, mesmo que estejam no documento Mongo.
290
+
291
+ #### Campos legados (aceitos mas sem efeito runtime)
292
+
293
+ - `Trigger.errors`, `Trigger.logs` — getters/setters existem; apenas o método privado `SaveTrigger(...)` (comentado entre linhas 540–551 de `TriggerRunner.java`) os populava. Hoje, todo log de execução vai para `trigger_log`.
294
+
295
+ #### Constantes de Entity (em `Trigger.java`)
296
+
297
+ ```java
298
+ ENTITY_ACTION = "action"
299
+ ENTITY_PLAYER = "player"
300
+ ENTITY_WIN_STATE = "challenge"
301
+ ENTITY_POINT = "point_category"
302
+ ENTITY_LEVEL = "level"
303
+ ENTITY_CROWN = "crown"
304
+ ENTITY_CATALOG_ITEM = "catalog_item"
305
+ ENTITY_LOTTERY = "lottery"
306
+ ENTITY_CHARACTER_STAR_STATS = "character_star_stats_level"
307
+ ENTITY_UPLOAD = "upload"
308
+ ENTITY_MYSTERY_BOX = "mystery_box"
309
+
310
+ // Comentadas/removidas (não aceitar como valor canônico):
311
+ // ENTITY_ACTIONLOG = "action_log"
312
+ // ENTITY_ACHIEVEMENT = "achievement"
313
+ // ENTITY_COMPETITION = "competition"
314
+ ```
315
+
316
+ Importante: o campo `entity` aceita **qualquer string** — não há validação. Strings observadas no código fora dessas constantes:
317
+
318
+ - `"swap"`, `"swap_counter_offer"` — disparado por `SwapManager`.
319
+ - `"folder_log"`, `"folder_progress"` — `FolderManager` / `FolderRest`.
320
+ - `"quiz"`, `"quiz_log"`, `"question"`, `"question_log"` — `QuizManager`, `QuestionManager`.
321
+ - `"deal"` — `CrmManager` (módulo CRM B2B).
322
+ - `"compact_log"`, `"backup_log"` — `CompactManager`, `BackupManager`.
323
+ - `"password_change"` — `PlayerRest.changePassword`.
324
+ - Qualquer string de coleção passada por `DatabaseRest`/`DatabaseManager` (no `before_create`/`after_create`/`before_update`/`after_update`/`before_delete`/`after_delete`/`before_bulk`/`after_bulk`).
325
+
326
+ #### Constantes de Event (em `Trigger.java`)
327
+
328
+ ```java
329
+ EVENT_BEFORE_WIN = "before_win"
330
+ EVENT_AFTER_WIN = "after_win"
331
+ EVENT_BEFORE_LOSE = "before_lose"
332
+ EVENT_AFTER_LOSE = "after_lose"
333
+ EVENT_BEFORE_CREATE = "before_create"
334
+ EVENT_AFTER_CREATE = "after_create"
335
+ EVENT_BEFORE_UPDATE = "before_update"
336
+ EVENT_AFTER_UPDATE = "after_update"
337
+ EVENT_BEFORE_DELETE = "before_delete"
338
+ EVENT_AFTER_DELETE = "after_delete"
339
+ ```
340
+
341
+ Eventos extras NÃO declarados como constante mas usados ad-hoc:
342
+
343
+ - `"before_bulk"`, `"after_bulk"` — `DatabaseManager.bulkInsert`.
344
+ - `"before_purchase_validation"` — `CatalogManager.purchase`.
345
+ - `"before_acquire_validation"` — `SwapManager.buyerAcquire`.
346
+ - `"before_win_reward"` — `MysteryBoxManager.execute` (dispara antes de cada reward individual de uma caixa).
347
+ - `"after_finish"` — `CompactManager.asyncExecute`.
66
348
 
67
- ## Script Runtime Environment (Wrapper Class)
349
+ ### 3.2 `Reference` subdocumento (`references[]`)
68
350
 
69
- O script da trigger **não é standalone** ele é inserido dentro de uma classe Java wrapper gerada pelo Funifier, idêntica à do Public Endpoint (ver `public.md` para a lista completa de imports).
351
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
352
+ |---|---|---|---|---|
353
+ | `_id` | String | — | sim | Id do item referenciado. |
354
+ | `type` | String | — | sim | Tipo/coleção (ex: `Entity.POINT.collection`). |
355
+ | `title` | String | — | sim | Texto exibido no card visual do Studio. |
356
+ | `way` | String | — | sim | `"from"` ou `"to"` (`Reference.WAY_FROM` / `WAY_TO`). |
357
+ | `linkLabel` | String | `""` (via `getLinkLabel()`) | não | Rótulo sobre a seta no diagrama de estratégia do Studio. |
70
358
 
71
- ### Regras
359
+ Apenas metadata para o Studio — **não é lida pelo runtime do trigger**.
72
360
 
73
- 1. **NÃO use `import`**todos os imports já estão no wrapper (Unirest, Groovy JSON, Funifier entities/utils, Apache HTTP, Simple Java Mail, etc.)
74
- 2. Se precisar de uma classe não importada, use o nome completo: `com.example.MinhaClasse`
75
- 3. **`manager`** (ManagerFactory) está disponível como campo da classe
76
- 4. **Timeout padrão de 10 segundos** — pode ser customizado via campo `timeout` (em **segundos**) na API: `POST /v3/trigger` com `{"_id": "trigger_id", "timeout": 30}`. Este campo não aparece no Studio.
77
- 5. Scripts rodam em **Groovy** — cuidado com `$` em GStrings (usar `String.valueOf((char)0x24)` para operadores MongoDB)
78
- 6. Use apenas **ASCII em comentários** — UTF-8 especial pode causar parse errors
361
+ ### 3.3 `ObjectStyle`subdocumento (`style`)
79
362
 
80
- > 📖 Ver `public.md` **Script Runtime Environment** para a lista completa de imports e bibliotecas disponíveis.
363
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
364
+ |---|---|---|---|---|
365
+ | `background` | String | — | não | Cor de fundo no Studio. |
366
+ | `visible` | Boolean | `null` | não | Se `false`, o item é ocultado nas visualizações do Studio. |
81
367
 
82
- ## Parâmetros do Script
368
+ ### 3.4 `TriggerLog` — documento de log (`trigger_log`)
83
369
 
84
- - `event` (String): evento acionado
85
- - `entity` (Object): objeto sendo manipulado (Player, ActionLog, Achievement, etc.)
86
- - `player` (String): ID do jogador
87
- - `database` (Object): utilitário para acessar o banco de dados
370
+ Gerado em **toda** execução pelo `TriggerRunner.run`, inclusive em caso de falha de compilação ou timeout (carrega o erro em `err`).
88
371
 
89
- ### ⚠️ CRITICAL: `entity` Type Depends on the Trigger Entity
372
+ | Campo | Tipo | Padrão | Descrição |
373
+ |---|---|---|---|
374
+ | `_id` | String | `Guid.newShortGuid()` no `addLog` se ausente | Id do log. |
375
+ | `item` | String | — | **No fluxo atual = `trigger.id`** (o `TriggerLog` é construído como `new TriggerLog(trigger.id, trigger.id, ...)`). O nome do campo sugere "id do item disparado" mas hoje duplica o `trigger.id`. |
376
+ | `time` | Date | `new Date()` no construtor + `new Date()` no `addLog` se null | Quando o log foi registrado. |
377
+ | `out` | `List<String>` | `null` se vazio | Captura de todos os `println` do script. **Se a lista chegar vazia (`size == 0`), é gravada como `null`** (linha 37 de `TriggerLog.java`). |
378
+ | `err` | `List<String>` | `null` se vazio | Stack messages das exceções (compilação, timeout, security, etc.). Mesma regra de `out`: vazio → `null`. |
379
+ | `ms` | long | — | Tempo total de execução em milissegundos (medido do início do `run` até depois do `addLog`). |
90
380
 
91
- O tipo do parâmetro `entity` varia conforme a entidade da trigger:
381
+ ### 3.5 Subentidade `TriggerContext` (runtime, não persistido)
92
382
 
93
- | Trigger Entity | Tipo de `entity` | Como acessar campos |
383
+ Não é persistido no Mongo. É construído ad-hoc pelo manager chamador e injetado no script via `setContext`.
384
+
385
+ | Campo | Tipo | Uso |
386
+ |---|---|---|
387
+ | `manager` | `ManagerFactory` | Acesso a todos os managers da plataforma para o tenant. |
388
+ | `origin` | Object | Convenção: a entidade que originou a cadeia de eventos (ex: o `ActionLog` original quando o evento atual é um `point_category.after_win` derivado). |
389
+ | `extra` | `Map<String, Object>` | Bag de dados arbitrários. Chaves observadas no código: `"achievements"` (lista mutável em que o script pode adicionar Achievements), `"parentActionLog"`, `"deal"`, `"level"`. |
390
+
391
+ ### 3.6 `TriggerStatistics` / `TriggerDetailStatistics` (estatísticas in-memory)
392
+
393
+ Não são persistidos por trigger. Vivem dentro do `StatisticManager` por `apiKey` e são serializados periodicamente como `Entity.STATISTIC_LOG`.
394
+
395
+ | Campo | Tipo | Descrição |
94
396
  |---|---|---|
95
- | `player` | `com.funifier.engine.player.Player` (objeto Java) | `entity.id`, `entity.name`, `entity.email`, `entity.extra` |
96
- | `<custom>__c` | `java.util.Map` (documento MongoDB) | `entity.get("campo")`, `entity.put("campo", valor)` |
97
- | `action` | `com.funifier.engine.action.ActionLog` | `entity.userId`, `entity.actionId`, `entity.attributes` |
98
- | `achievement` | `com.funifier.engine.achievement.Achievement` | `entity.achievementId`, `entity.userId` |
397
+ | `lastTriggerExecution` | Date | Última execução de qualquer trigger. |
398
+ | `hourlyTriggerExecutions` | long | Contador da hora atual (reset ao trocar de hora). |
399
+ | `dailyTriggerExecutions` | long | Contador do dia atual (reset ao trocar de dia do ano). |
400
+ | `monthlyTriggerExecutions` | long | Contador do mês atual (reset ao trocar de mês). |
401
+ | `dailyTriggerExecutionsLimit` | long | Limite diário (`@JsonIgnore` — não vaza pelo JSON). Carregado de `GamificationLimits.dailyTriggerExecutions`. |
402
+ | `triggers` | `Map<String, TriggerDetailStatistics>` | Detalhe por `trigger.id`. |
403
+
404
+ Lógica de reset (linha 57–78 de `TriggerStatistics.java`): compara hora/dia/mês atuais com os armazenados — quando muda, o contador é **resetado para 1** (não 0). Logo, no primeiro tick de cada dia o contador começa em 1.
99
405
 
100
- **Erros comuns:**
101
- - ❌ `entity.get("_id")` em trigger de `player` → `MissingMethodException: No signature of method: Player.get()`
102
- - ✅ `entity.id` em trigger de `player`
103
- - ❌ `entity.id` em trigger de `signup__c` → campo não existe no Map
104
- - ✅ `entity.get("_id")` em trigger de `signup__c`
406
+ Decisão de bloqueio (linha 92–98):
105
407
 
106
- **Player fields (via `PlayerManager.java`):**
408
+ ```java
409
+ if (dailyTriggerExecutions <= dailyTriggerExecutionsLimit) return true;
410
+ else return false;
107
411
  ```
108
- entity.id → String (_id do player)
109
- entity.name → String
110
- entity.email → String
111
- entity.password → String (hash BCrypt)
112
- entity.extra → Map<String, Object> (campos customizados)
113
- entity.image → Image object
114
- entity.business → boolean
115
- entity.developer boolean
412
+
413
+ Observação: o teste é `<=`, então o limite é **inclusivo** — quando `daily == limit`, ainda passa; bloqueia a partir de `daily > limit`.
414
+
415
+ ### 3.7 `TriggerHtml` **NÃO faz parte do runtime de Trigger**
416
+
417
+ Apesar do nome, `TriggerHtml` (coleção `trigger_html`) é um modelo client-side de eventos DOM do SDK web. Está em `engine/integration/html/TriggerHtml.java` e é gerenciado pelo `ActionDaoMongo` (não pelo `TriggerManager`). Existe documentação separada — não confundir.
418
+
419
+ ### 3.8 Técnicas de jogo (`techniques`)
420
+
421
+ Apenas armazenamento — o array de códigos GT (ex: `["GT001", "GT042"]`) é persistido e devolvido pela API mas **não influencia o runtime**. Usado pelo Studio para taggear visualmente os triggers em relatórios de cobertura de técnicas.
422
+
423
+ ### 3.9 Ciclo de vida do `Trigger` no cache
424
+
425
+ ```mermaid
426
+ stateDiagram-v2
427
+ [*] --> Persistido: add(trigger)<br/>updated = now
428
+ Persistido --> Compilado: TriggerRunner.run<br/>compile + cache
429
+ Compilado --> Compilado: run reutiliza cache<br/>dates &gt; updated
430
+ Compilado --> Persistido: trigger.updated alterado<br/>(outro nó editou)
431
+ Compilado --> Invalidado: update(trigger)<br/>delete(trigger)<br/>clear(trigger)
432
+ Invalidado --> Compilado: próxima run recompila
433
+ Persistido --> [*]: delete(id)
116
434
  ```
117
435
 
118
- **Fonte:** `PlayerManager.delete()` chama `TriggerRunner.run()` com `new Object[] { event, playerObject, playerId, gameDao }` — logo `entity` é o objeto `Player` completo, não um Map.
436
+ ---
119
437
 
120
- ## Verificando Logs de Trigger (Studio)
438
+ ## 4. Endpoints
121
439
 
122
- Para ver se uma trigger executou com sucesso ou com erro:
440
+ ### 4.1 `GET /v3/trigger` listar com filtros
123
441
 
124
- 1. Acessar **Studio** menu lateral → **Triggers**
125
- 2. Na lista de triggers, cada uma tem um botão **🔥 Logs** na coluna OPERAÇÕES
126
- 3. Clicar em **Logs** abre um modal com:
127
- - **Log Id**: identificador da trigger
128
- - **Executou em**: data/hora da última execução
129
- - **Tempo de resposta**: tempo de execução em ms
130
- - **Erros**: se houve erro, mostra a exception completa em vermelho
131
- 4. Se não houver seção "Erros", a trigger executou com sucesso
442
+ | Aspecto | Detalhe |
443
+ |---|---|
444
+ | Finalidade | Listar/filtrar triggers via aggregation. |
445
+ | Autenticação | Bearer token. |
446
+ | Tipo | Read. |
132
447
 
133
- **Dica:** Após alterar uma trigger via API (`POST /v3/trigger`), teste-a e verifique o log no Studio para confirmar que não há erros de compilação ou runtime.
448
+ **Query params:**
134
449
 
135
- ## Utilitários Disponíveis no Contexto
450
+ | Param | Tipo | Descrição |
451
+ |---|---|---|
452
+ | `id` | String | Filtro exato por `_id`. |
453
+ | `name` | String | Filtro `$regex` case-insensitive (`{$regex: <name>, $options: 'i'}`). |
454
+ | `entity` | String | Filtro exato. |
455
+ | `event` | String | Filtro exato. |
456
+ | `script` | String | Filtro `$regex` case-insensitive no corpo do script. |
457
+ | `q` | String | **Fragmento bruto** de query Mongo, concatenado no `$match` (ex: `q=tags:{$all:["x"]}`). Sem sanitização — ver seção 8. |
458
+ | `fields` | String CSV | Projeção (`{$project:{a:1,b:1}}`). |
459
+ | `orderby` | String | Campo de ordenação. |
460
+ | `reverse` | boolean (string) | `true` → `-1`, qualquer outro valor → `1`. |
461
+ | `max_results` | int (string) | Limite. Se `<= 0` → default **`100`**. |
462
+
463
+ **Comportamento real:**
464
+
465
+ - `max_results` é sempre clamped para `100` quando `<= 0` (linha 149 do `TriggerManager.findAllTriggers`).
466
+ - Argumentos vazios/`null` são ignorados sem warning.
467
+ - `name` e `script` aceitam **regex livre** (não há escape de metacaracteres).
468
+ - `q` é literalmente concatenado dentro de `{ $match: { ..., <q> } }` — **injeção de query Mongo é possível**.
469
+
470
+ **Exemplo:**
471
+
472
+ ```http
473
+ GET /v3/trigger?entity=action&event=after_win&max_results=50
474
+ Authorization: Bearer eyJ...
475
+ ```
136
476
 
137
- | Classe/Método | Descrição |
477
+ ```json
478
+ [
479
+ {
480
+ "_id": "5f3a1b...",
481
+ "name": "Notificar Slack após sell",
482
+ "entity": "action",
483
+ "event": "after_win",
484
+ "script": "void trigger(event, entity, player, database){ ... }",
485
+ "creation": "2024-08-01T13:21:09Z",
486
+ "updated": "2024-08-12T17:02:55Z",
487
+ "timeout": 10
488
+ }
489
+ ]
490
+ ```
491
+
492
+ ### 4.2 `GET /v3/trigger/{id}` — buscar por id
493
+
494
+ | Aspecto | Detalhe |
495
+ |---|---|
496
+ | Finalidade | Retorna um único trigger por `_id`. |
497
+ | Autenticação | Bearer token. |
498
+ | Comportamento se não encontrado | `findById` retorna `null`; resposta é `"null"` literal (após `toJsonRemoveNullFields`). **Não retorna 404.** |
499
+
500
+ ### 4.3 `POST /v3/trigger/execute/{id}?player=<player>` — debug execution
501
+
502
+ | Aspecto | Detalhe |
138
503
  |---|---|
139
- | `Guid.newShortGuid()` | Gera um ID curto único (7 chars, ex: `FnwjKEY`) |
140
- | `Guid.newGuid()` | Gera um GUID completo |
141
- | `newShortGuid()` | **NÃO FUNCIONA**precisa do prefixo `Guid.` |
142
- | `com.funifier.engine.util.BCrypt.hashpw(pwd, BCrypt.gensalt())` | Hash de senha BCrypt |
143
- | `new Date().getTime()` | Timestamp atual em milissegundos |
144
- | `manager` | ManagerFactory — acesso a todos os managers |
504
+ | Finalidade | Executar um trigger isolado para debug. |
505
+ | Autenticação | Bearer token. |
506
+ | Body | JSON livre (`Map<String, Object>`) passado como `o` ao script. |
507
+ | Tipo | Execução side-effectful. |
145
508
 
146
- > ⚠️ `System.currentTimeMillis()` é BLOQUEADO pelo SecureASTCustomizer. Usar `new Date().getTime()`.
509
+ **Comportamento real:**
147
510
 
148
- ## Managers Disponíveis
511
+ - **Não chama `TriggerManager.execute`** — instancia um `new TriggerRunner()` e chama `run(trigger, o, player, manager, null)` diretamente.
512
+ - **NÃO valida `dailyTriggerExecutionsLimit`** (pula `StatisticManager.newTriggerExecution`).
513
+ - `context` é `null` — qualquer script que faça `context.extra.put(...)` lança NPE.
514
+ - Body é passado ao script como o objeto `o` da assinatura `trigger(event, entity, player, database)`.
149
515
 
150
- - `manager.getPlayerManager()` - findById, insert, delete
151
- - `manager.getActionManager()` - findActionById, track, trackSynchonous
152
- - `manager.getCatalogManager()` - findItemById, purchase, undoPurchase
153
- - `manager.getLotteryManager()` - find, insertTicket, execute
154
- - `manager.getAchievementManager()` - addAchievement
155
- - `manager.getJongoConnection()` - acesso direto ao MongoDB
516
+ **Resposta:**
156
517
 
157
- ## Envio de Email
518
+ ```json
519
+ {
520
+ "trigger": { ... documento completo ... },
521
+ "exceptions": [],
522
+ "outputs": ["valor que veio do println do script"],
523
+ "millis": 47
524
+ }
525
+ ```
158
526
 
159
- As triggers podem enviar emails usando o servidor SMTP configurado na gamificação. As classes do Simple Java Mail e utilitários Funifier já estão disponíveis no contexto (não precisa importar).
527
+ Se a trigger não existir, o endpoint passa `null` para `TriggerRunner.run` que faz NPE em `trigger.updated` retorna 500.
160
528
 
161
- ### Classes disponíveis
162
- - `EmailBuilder` / `MailerBuilder` — Simple Java Mail (construir e enviar)
163
- - `com.funifier.engine.mail.MailContext` — config SMTP da gamificação
164
- - `com.funifier.controller.Configuration` — acesso à configuração atual
165
- - `com.funifier.engine.util.MustacheUtils` — parse de templates com variáveis `{{campo}}`
529
+ ### 4.4 `POST /v3/trigger` — criar/atualizar
166
530
 
167
- ### Exemplo básico
168
- ```java
169
- Email email = EmailBuilder.startingBlank()
170
- .from("App", "noreply@funifier.com")
171
- .to(nome, emailDestinatario)
172
- .withSubject("Assunto")
173
- .withHTMLText("<h1>Olá</h1>")
174
- .buildEmail();
531
+ | Aspecto | Detalhe |
532
+ |---|---|
533
+ | Finalidade | Criar/sobrescrever trigger. |
534
+ | Autenticação | Bearer token. |
535
+ | Full replace ou patch | **Full replace.** O `_id`, se informado, sobrescreve o documento existente (Jongo `save`). Não é patch. |
536
+
537
+ **Comportamento real (`TriggerRest.insert`):**
538
+
539
+ 1. Se o body for `null`, devolve `201 Created` com `{}` — não erro.
540
+ 2. Chama `TriggerManager.compile(trigger)` — só valida sintaxe + AST (não roda).
541
+ 3. Se `CompilationFailedException` / `InstantiationException` / `IllegalAccessException`:
542
+ ```json
543
+ { "trigger": { ... }, "status": "ERROR", "message": "<msg>" }
544
+ ```
545
+ **HTTP status continua 201 Created** mesmo com erro de compilação.
546
+ 4. Se OK, chama `TriggerManager.add(trigger)`:
547
+ - Preenche `_id` se vazio (`Guid.shortTimeMillis()`).
548
+ - Preenche `creation` se null.
549
+ - **Sempre sobrescreve `updated = new Date()`**.
550
+ - `c.save(trigger)` — UPSERT por `_id`.
551
+ - Invalida cache local: `compiled.remove(id)` + `dates.remove(id)`.
552
+ 5. Retorna `{ "trigger": <persisted>, "status": "OK" }` com HTTP 201.
553
+
554
+ **Exemplo:**
175
555
 
176
- com.funifier.engine.mail.MailContext ctx = com.funifier.controller.Configuration.getCurrentConfiguration().getMailContext();
177
- MailerBuilder.withSMTPServer(ctx.hostName, ctx.smtpPort, ctx.authUser, ctx.authPassword)
178
- .buildMailer()
179
- .sendMail(email);
556
+ ```json
557
+ POST /v3/trigger
558
+ Authorization: Bearer eyJ...
559
+ Content-Type: application/json
560
+
561
+ {
562
+ "name": "After Register Action Log",
563
+ "entity": "action",
564
+ "event": "after_win",
565
+ "timeout": 10,
566
+ "script": "void trigger(event, entity, player, database) {\n println 'event:' + event + ', player:' + player\n}"
567
+ }
180
568
  ```
181
569
 
182
- > 📖 Para o pattern completo com templates editáveis e Mustache, ver `patterns.md` → **Email Template Pattern**
570
+ ### 4.5 `DELETE /v3/trigger/{id}`
571
+
572
+ | Aspecto | Detalhe |
573
+ |---|---|
574
+ | Finalidade | Remover trigger + invalidar cache. |
575
+ | Autenticação | Bearer token. |
576
+ | Resposta | `204 No Content`. |
577
+
578
+ **Comportamento real:** `mongo.remove({_id:#})` + `compiled.remove(id)` + `dates.remove(id)`. Se o id não existir, é no-op silencioso (Mongo não retorna erro).
579
+
580
+ ### 4.6 `PUT /v3/trigger/compile/{id}` — invalidar cache
581
+
582
+ | Aspecto | Detalhe |
583
+ |---|---|
584
+ | Finalidade | Forçar recompilação **na próxima execução**. |
585
+ | Autenticação | Bearer token. |
586
+ | Valor especial `id` | `"all"` itera todos os triggers e chama `clear` em cada um. |
587
+
588
+ **Comportamento real:** apesar do nome, **não compila nada agora** — apenas remove o `id` dos mapas `compiled` e `dates`. A próxima execução real é que vai recompilar.
183
589
 
184
- ## Validações e Testes
590
+ **Resposta:**
591
+
592
+ ```json
593
+ { "_id": "all" }
594
+ ```
595
+
596
+ ou
597
+
598
+ ```json
599
+ { "_id": "5f3a1b..." }
600
+ ```
601
+
602
+ Se o id passado não existir, `findById` retorna `null`, o trecho `if(trigger != null)` é falso e o endpoint ainda devolve `200 OK` com `_id` ecoado. **Não retorna 404.**
603
+
604
+ ### 4.7 Endpoints inexistentes (esperados em REST convencional, ausentes aqui)
605
+
606
+ - Não há `PUT /v3/trigger/{id}` para update — usar `POST` com `_id` no body.
607
+ - Não há `GET /v3/trigger/{id}/logs` — para ver logs, consultar a coleção `trigger_log` via `/v3/database/trigger_log`.
608
+ - Não há paginação cursor-based — apenas `max_results` clamped a 100 por default.
609
+
610
+ ---
611
+
612
+ ## 5. Regras de Negócio
613
+
614
+ - **`(entity, event)` é a chave de roteamento.** Não há regra adicional de tenant filter — todos os triggers daquela combinação rodam.
615
+ - **Ordem de execução não é determinística.** `findByEntityAndEvent` itera o cursor Mongo na ordem natural (geralmente ordem de inserção em RocksDB/WiredTiger, mas não garantida).
616
+ - **Isolamento por tenant é via `apiKey`.** Cada `ManagerFactory` tem sua própria conexão Mongo (banco do tenant) → `findByEntityAndEvent` só vê triggers daquele tenant.
617
+ - **Limite diário (`dailyTriggerExecutionsLimit`)**:
618
+ - Vem de `GamificationLimits.dailyTriggerExecutions` (coleção `game_limits`).
619
+ - Se `0`, **bloqueia tudo** (porque `1 <= 0` é falso).
620
+ - Quando excedido, a execução é **silenciosamente pulada** — `System.out.println` de erro no stdout do servidor; nenhum `trigger_log` é gravado.
621
+ - Contagem é **in-memory**: se o serviço reinicia, o contador zera.
622
+ - **`before_*` e `after_*` rodam em volta da mutação principal**, **na mesma thread chamadora**. Logo, um `before_create` lento atrasa o `insert` HTTP. Um `before_create` que joga exception é **engolido** (`e.printStackTrace`) e a operação continua — não é uma forma de cancelar a operação.
623
+ - **Não há rollback transacional** entre o script e o save do entity. Se o `after_create` falha, o documento já está persistido.
624
+ - **`script` recebido na API é compilado mas não é checado para a assinatura `trigger(event, entity, player, database)`.** Se faltar essa função, o `invokeMethod("trigger", ...)` lança `MissingMethodException` apenas em runtime, registrada em `trigger_log.err`.
625
+ - **Triggers customizadas podem disparar outros eventos**: se um script chama `manager.getActionManager().trackSynchonous(...)`, isso dispara novas triggers — **risco de loop infinito**, limitado apenas pelos timeouts e pelo `dailyTriggerExecutionsLimit`.
626
+ - **`techniques`, `style`, `references` são apenas metadados de Studio** — runtime ignora.
627
+
628
+ ---
629
+
630
+ ## 6. Comportamentos Automáticos
631
+
632
+ | Comportamento | Trigger (gatilho) | Impacto | Persistência |
633
+ |---|---|---|---|
634
+ | Atribuição de `_id` | `add(trigger)` sem `_id` | Gera `Guid.shortTimeMillis()`. | Sim — gravado. |
635
+ | Atribuição de `creation` | `add(trigger)` sem `creation` | `new Date()`. | Sim. |
636
+ | Atualização de `updated` | Todo `add`/`update` | Sobrescreve com `new Date()`. | Sim. Não respeita valor enviado pelo cliente. |
637
+ | Auto-save no primeiro run | `TriggerRunner.run` quando `trigger.updated == null` | Persiste o trigger no Mongo. | Sim — efeito colateral. |
638
+ | Invalidação de cache | `add` / `update` / `delete` / `clear` | `compiled.remove(id)` + `dates.remove(id)` apenas no processo local. | Não — só memória. |
639
+ | Recompilação por staleness | `run` detecta `trigger.updated > dates.get(id)` | Recompila e re-cacheia. | Não. |
640
+ | Log de execução | Toda chamada de `TriggerRunner.run` (independente de sucesso) | Insert em `trigger_log` com `out`, `err`, `ms`. | Sim, sempre. |
641
+ | Estatística | Toda chamada de `TriggerManager.execute` antes do run | Incremento de contadores em memória + decisão de limite. | Não direto — `StatisticManager` flushea em ciclos próprios. |
642
+ | Println capturado | Toda chamada de `println` no script | Adiciona a `output` (lista interna). | Sim, vai para `trigger_log.out`. |
643
+ | Wrapping de classes | Toda execução, antes da compilação | Adiciona ~40 imports + classe `FunifierTrigger` + setters. | Não — só em memória. |
644
+ | Reset diário/horário/mensal | `newTriggerExecution` em qualquer execução | Quando o `Calendar.HOUR_OF_DAY` / `DAY_OF_YEAR` / `MONTH` muda, o contador correspondente vira `1`. | Não diretamente. |
645
+
646
+ ### 6.1 Diagrama — Behaviors encadeados em `POST /v3/trigger`
647
+
648
+ ```mermaid
649
+ flowchart LR
650
+ A[POST /v3/trigger] --> B[TriggerRest.insert]
651
+ B --> C[manager.compile<br/>valida AST]
652
+ C -->|fail| F[status=ERROR<br/>HTTP 201 retorna]
653
+ C -->|OK| D[manager.add]
654
+ D --> E1[_id := Guid.shortTimeMillis<br/>se vazio]
655
+ D --> E2[creation := now<br/>se null]
656
+ D --> E3[updated := now<br/>sempre]
657
+ D --> E4[c.save trigger<br/>UPSERT por _id]
658
+ D --> E5[compiled.remove id<br/>dates.remove id]
659
+ E5 --> G[HTTP 201 + status=OK]
660
+ ```
661
+
662
+ ---
663
+
664
+ ## 7. Suportado vs NÃO Suportado
665
+
666
+ ### ✅ Suportado
667
+
668
+ - CRUD básico via `/v3/trigger` (criar, listar, buscar, deletar).
669
+ - Listagem com `$regex` em `name`/`script` e `$match` livre via `q`.
670
+ - Compilação `SecureASTCustomizer` com whitelist de tokens (operadores aritméticos, comparações, lógicos, `[`/`]`).
671
+ - Bloqueio de classes perigosas (`System`, `ProcessBuilder`, `File`, `GroovyShell`, `GroovyObject`) e expressões (`.execute`, `.getDB`, `.getMongo`, `.dropDatabase`) via `TriggerExpressionChecker`.
672
+ - Cache local de classes Groovy compiladas (per processo, invalidado por update/delete).
673
+ - Timeout configurável por trigger (efetivo até 200s).
674
+ - Endpoint de execução para debug (`POST /v3/trigger/execute/{id}`).
675
+ - Endpoint para forçar invalidação de cache (`PUT /v3/trigger/compile/{id}`, `compile/all`).
676
+ - Captura de `println` em `trigger_log.out`.
677
+ - Captura de exceções em `trigger_log.err`.
678
+ - Disparo via 29 managers/REST distintos, em todos os eventos `before_*` / `after_*` documentados em 3.1.
679
+ - `TriggerContext.extra["achievements"]` como canal de "saída" do script para o manager chamador (especialmente `AchievementManager`).
680
+ - Estatísticas in-memory de execução por hora/dia/mês (`TriggerStatistics`).
681
+ - Limite diário aplicado por tenant (`GamificationLimits.dailyTriggerExecutions`).
682
+
683
+ ### ❌ NÃO Suportado / Comportamento limitado
684
+
685
+ - **Sem `PUT /v3/trigger/{id}`** — atualização é feita via `POST` com `_id` no body (mesmo endpoint do create). O endpoint `PUT /v3/trigger/compile/{id}` não é um PUT de update, é apenas invalidação de cache.
686
+ - **404 não é retornado** para id inexistente em `GET /v3/trigger/{id}` nem `PUT /v3/trigger/compile/{id}`.
687
+ - **HTTP status não reflete erro de compilação** — `POST /v3/trigger` devolve `201 Created` mesmo quando `compile` falha (o erro vem no body `status: "ERROR"`).
688
+ - **Sem rollback transacional** — se um `after_create` falha, a entidade já foi criada.
689
+ - **`techniques`, `style`, `references` não influenciam runtime** — são apenas metadata de Studio. Aceitos no schema, ignorados pelo `TriggerRunner`.
690
+ - **`errors` e `logs` em `Trigger` são campos legados** — o método `SaveTrigger` que os populava está comentado. Nunca são preenchidos no fluxo atual.
691
+ - **Sem cancelamento via exceção** — `throw` dentro de um `before_create` script é capturado pelo `executor` (`exceptions.add`), logado, e a operação principal **continua mesmo assim**.
692
+ - **Sem broadcast de invalidação de cache em cluster** — cada nó decide por si baseado em `trigger.updated` vs. `dates`. Há janela de inconsistência: enquanto o nó B não recebe nenhum evento que dispare o trigger, ele continua com a classe antiga.
693
+ - **`@TimedInterrupt(value = 200L, ...)` é fixo no código** (linha 164 de `TriggerRunner.getScript`). Não há como configurar via `trigger.timeout` para passar de 200s.
694
+ - **`InterruptedException` no executor não chama `future.cancel(true)`** (linhas 308–314 de `TriggerRunner`) — possível leak de thread.
695
+ - **`TriggerManager.execute` engole TODA exception** — `catch(Exception)` com `e.printStackTrace()`, sem propagar nem registrar em log persistente. Bugs em managers chamadores são invisíveis.
696
+ - **Limite diário usa contador in-memory** — restart do serviço zera. Não há quota persistida.
697
+ - **Limite é shared entre todos os triggers do tenant** — não há quota por `trigger.id`. Quando o teto é atingido, **nenhum** trigger executa até o próximo reset de dia.
698
+ - **Sem audit log nativo** — operações em `/v3/trigger` não passam pelo `AuditManager` (diferente de `/v3/database`).
699
+ - **Sem filtro de tenant nas estatísticas detalhadas** quando comparado via `triggers` map — funciona porque `StatisticManager` é por `apiKey`, mas o `triggers` map cresce sem bound (todo `trigger.id` que executou pelo menos uma vez nunca é removido em memória).
700
+ - **`Trigger.entity` e `Trigger.event` aceitam qualquer string** — não há enum-enforcement. Triggers escritos para `event="after_win"` errado (ex: `"afterwin"`) **nunca disparam** e não há aviso.
701
+ - **`POST /v3/trigger/execute/{id}` ignora limite diário** — útil para debug, perigoso em prod.
702
+ - **Limit `dailyTriggerExecutionsLimit` é inclusivo** (`<=`) — quando o uso bate exatamente no limite ainda executa; bloqueia só do próximo em diante.
703
+
704
+ ---
705
+
706
+ ## 8. Segurança e Permissões
707
+
708
+ ### 8.1 Autenticação
709
+
710
+ - Todos os endpoints exigem Bearer token (`@BeanParam AuthBean authBean` → `FrontController.getInstance(authBean.getApiKey())`).
711
+ - **Não há filtro de role/permission no `TriggerRest`** — qualquer principal autenticado com `apiKey` válido cria/lê/deleta/executa triggers daquele tenant.
712
+ - Isolamento multi-tenant é via `apiKey` → `ManagerFactory` próprio → `Jongo` próprio → coleção `trigger` do banco daquele tenant.
713
+
714
+ ### 8.2 Sandbox de scripts
715
+
716
+ - `SecureASTCustomizer` com `TriggerExpressionChecker.isAuthorized(...)`:
717
+ - Bloqueia tipos: `System`, `ProcessBuilder`, `File`, `GroovyShell`, `GroovyObject` (lista hard-coded em `TriggerExpressionChecker`).
718
+ - Bloqueia substrings em `.getText()`: `.execute`, `.getDB`, `.getMongo`, `.dropDatabase`.
719
+ - Whitelist de tokens (linhas 222–247 de `TriggerRunner.compile`): `=, +, -, *, /, %, **, ++, +=, --, ==, !=, <, <=, >, >=, ||, ||=, &&, &&=, [, ]`. **Note que `==` aparece duplicado** (linhas 234 e 238).
720
+
721
+ ### 8.3 Brechas conhecidas no sandbox
722
+
723
+ - **`.execute` é bloqueado por busca de substring**, não por análise semântica. Strings em métodos como `executeAggregate`, `executeQuery` (ex: chamadas do `Jongo`) também são bloqueadas — falso positivo.
724
+ - O `getText()` do AST verifica a **representação textual da expressão**, não o tipo resolvido. Variáveis dinâmicas (`def x = "exec" + "ute"; obj[x]()`) podem contornar.
725
+ - O bloqueio é **AST-time** — só pega o que aparece no código fonte. Reflection via classes permitidas (ex: `ManagerFactory.class.getDeclaredMethods()`) é livre.
726
+ - Outras configurações de hardening do `SecureASTCustomizer` (`receiversBlackList`, `importsBlacklist`, `methodDefinitionAllowed`, `closuresAllowed`) **não estão configuradas**, então closures, definições de método e imports adicionais são permitidos.
727
+ - Já há imports automáticos a `ManagerFactory` e a todas as classes de domínio — scripts têm **acesso completo ao runtime do tenant**, incluindo Players, Achievements, Catalog, etc.
728
+
729
+ ### 8.4 Injeção em queries
730
+
731
+ - `GET /v3/trigger?q=<raw>` injeta `<raw>` literalmente no `$match` do aggregate. O aggregate é fixado em `db.trigger.aggregate([...])` da coleção `trigger`, mas pode ser usado para **enumeração de scripts** (ex: regex em `script` para procurar credenciais hardcoded).
732
+ - `name` e `script` são `$regex` sem escape — possível DoS por regex catastrófico.
733
+
734
+ ### 8.5 Outras superfícies
735
+
736
+ - `TriggerExpressionChecker` é instanciado a cada `compile` — não há cache do customizer (impacto desprezível).
737
+ - Scripts têm acesso ao `ManagerFactory`, então podem chamar `manager.getPlayerManager().findAll()` etc. Não há controle interno de what-can-a-script-do.
738
+
739
+ ---
740
+
741
+ ## 9. Observabilidade e Troubleshooting
742
+
743
+ ### 9.1 Como verificar se um trigger executou
744
+
745
+ ```bash
746
+ # Buscar logs por trigger.id (item == trigger.id no fluxo atual)
747
+ GET /v3/database/trigger_log?q={"item":"<TRIGGER_ID>"}&orderby=time&reverse=true&max_results=20
748
+ ```
749
+
750
+ Cada log tem `out` (println), `err` (exceções) e `ms` (latência).
751
+
752
+ ### 9.2 Trigger não dispara
753
+
754
+ Checklist (em ordem):
755
+
756
+ 1. **`entity` e `event` batem com o ponto de chamada?** Conferir nos sites em `getTriggerManager().execute(<id>, <obj>, <entity>, <event>, <player>, <ctx>)`. Strings divergentes (`"action"` vs `"actions"`) silenciam.
757
+ 2. **Limite diário não está estourado?**
758
+ ```bash
759
+ GET /v3/limits
760
+ # Ver "trigger.max" e o atual consumido.
761
+ ```
762
+ 3. **`trigger.updated` é mais recente que o último `clear` no cluster?** Se você editou o trigger em um nó mas outro nó tem o cache antigo, dispare `PUT /v3/trigger/compile/all`.
763
+ 4. **Compilação falhou?** O `POST /v3/trigger` retorna `status:"ERROR"` no body se sim. Reler o response. Observação: o fluxo do `POST /v3/trigger` é `compile → add`; se `compile` falha, `add` **não é chamado** — o script com erro **não** é persistido.
764
+ 5. **`script` define `void trigger(event, entity, player, database)`?** Se não, `MissingMethodException` aparece em `trigger_log.err` na primeira execução.
765
+
766
+ ### 9.3 Trigger demora demais / atinge timeout
767
+
768
+ - O log gravado terá `err: ["Timeout after Xs (timeout allowed Ys)", "<message>"]`.
769
+ - Se o script faz I/O remoto (`Unirest`, `HttpClientFunifier`), aumentar `trigger.timeout` até 200. Acima disso, o `@TimedInterrupt` mata mesmo assim.
770
+
771
+ ### 9.4 Queries úteis
772
+
773
+ ```js
774
+ // Triggers ativos em (entity, event)
775
+ db.trigger.find({ entity: "action", event: "after_win" })
776
+
777
+ // Triggers com timeout customizado
778
+ db.trigger.find({ timeout: { $exists: true, $gt: 0 } })
779
+
780
+ // Últimas execuções com erro
781
+ db.trigger_log.find({ err: { $ne: null } }).sort({ time: -1 }).limit(20)
782
+
783
+ // p95 de execução por trigger (aggregate)
784
+ db.trigger_log.aggregate([
785
+ { $group: { _id: "$item", count: { $sum: 1 }, avg_ms: { $avg: "$ms" }, max_ms: { $max: "$ms" } } },
786
+ { $sort: { avg_ms: -1 } }
787
+ ])
788
+
789
+ // Tamanho do trigger_log (atenção: pode crescer indefinidamente)
790
+ db.trigger_log.count()
791
+ ```
792
+
793
+ ### 9.5 Erros comuns
794
+
795
+ | Sintoma | Causa provável | Onde olhar |
796
+ |---|---|---|
797
+ | Script "não roda" mas POST OK | Limite diário estourado | `GET /v3/limits` |
798
+ | Funciona em dev, falha em prod | Cache local de outro nó | `PUT /v3/trigger/compile/all` |
799
+ | `MissingMethodException: trigger()` | Script sem a função `trigger(event, entity, player, database)` | `trigger_log.err` |
800
+ | `SecurityException` | AST bloqueou — `.execute`, `System`, ou token não-whitelisted | `trigger_log.err` |
801
+ | `TimeoutException` | Script passou de `trigger.timeout` ou de 200s (`@TimedInterrupt`) | `trigger_log.err` |
802
+ | Operação principal não foi cancelada apesar de exception no `before_*` | É by design — exceptions são engolidas | seção 5 |
803
+ | Coleção `trigger_log` cresceu muito | Não há TTL nativo no log | Criar índice TTL manualmente em `time` |
804
+
805
+ ### 9.6 Diagnóstico via REST
806
+
807
+ ```bash
808
+ # Detalhe atual de um trigger
809
+ GET /v3/trigger/<id>
810
+
811
+ # Listar últimos triggers criados
812
+ GET /v3/trigger?orderby=creation&reverse=true&max_results=10
813
+
814
+ # Stats de execução (in-memory por instância)
815
+ GET /v3/limits
816
+ ```
817
+
818
+ ---
819
+
820
+ ## 10. Exemplos Práticos
821
+
822
+ ### 10.1 Mínimo funcional — log de evento
823
+
824
+ ```json
825
+ POST /v3/trigger
826
+ {
827
+ "name": "Log after_win action",
828
+ "entity": "action",
829
+ "event": "after_win",
830
+ "script": "void trigger(event, entity, player, database) {\n println 'event=' + event + ' player=' + player + ' actionId=' + entity.getActionId()\n}"
831
+ }
832
+ ```
833
+
834
+ Resultado esperado: cada `trackSynchonous(action)` deixa uma linha em `trigger_log.out`.
835
+
836
+ ### 10.2 Avançado — atribuir Achievement extra de bônus de fim de semana
837
+
838
+ ```json
839
+ POST /v3/trigger
840
+ {
841
+ "name": "Bônus de fim de semana",
842
+ "entity": "action",
843
+ "event": "before_win",
844
+ "timeout": 10,
845
+ "techniques": ["GT001"],
846
+ "references": [
847
+ { "_id": "weekend_bonus", "type": "action", "title": "Sell weekend", "way": "from" }
848
+ ],
849
+ "script": "void trigger(event, entity, player, database) {\n def cal = java.util.Calendar.getInstance()\n cal.setTime(entity.getTime())\n def dow = cal.get(java.util.Calendar.DAY_OF_WEEK)\n if (dow == java.util.Calendar.SATURDAY || dow == java.util.Calendar.SUNDAY) {\n def bonus = new com.funifier.engine.achievement.Achievement(\n com.funifier.engine.guid.Guid.newShortGuid(),\n player,\n 50.0,\n com.funifier.engine.achievement.Achievement.TYPE_POINT,\n 'xp',\n new java.util.Date()\n )\n // Empurra para a lista que o AchievementManager devolverá ao caller\n if (context != null && context.extra != null && context.extra['achievements'] != null) {\n context.extra['achievements'].add(bonus)\n }\n println 'weekend bonus +50 xp'\n }\n}"
850
+ }
851
+ ```
852
+
853
+ Pontos a observar:
854
+
855
+ - Usa `context.extra['achievements']` (canal estabelecido por `ActionManager.trackSynchonous`).
856
+ - O Achievement é entregue ao caller via lista mutável no `TriggerContext.extra` — o `ActionManager` ao retornar adiciona ela ao resultado final.
857
+ - `techniques` e `references` ficam apenas como metadata.
858
+
859
+ ### 10.3 Debug com `/execute`
860
+
861
+ ```http
862
+ POST /v3/trigger/execute/<id>?player=test@user.com
863
+ Authorization: Bearer eyJ...
864
+ Content-Type: application/json
865
+
866
+ { "actionId": "sell", "userId": "test@user.com", "attributes": { "value": 200 } }
867
+ ```
868
+
869
+ Resposta:
870
+
871
+ ```json
872
+ {
873
+ "trigger": { ... },
874
+ "exceptions": [],
875
+ "outputs": ["weekend bonus +50 xp"],
876
+ "millis": 19
877
+ }
878
+ ```
879
+
880
+ Observação: este endpoint **não respeita `dailyTriggerExecutionsLimit`** e passa `context = null`. Scripts que dependem de `context.extra` lançarão NPE — testar isso é parte do debug.
881
+
882
+ ### 10.4 Anti-pattern — exception como cancelamento
883
+
884
+ ❌ **Não fazer:**
885
+
886
+ ```groovy
887
+ // Tentativa de impedir o save de um action_log
888
+ void trigger(event, entity, player, database) {
889
+ if (entity.getAttributes() == null) {
890
+ throw new RuntimeException("attributes obrigatórios")
891
+ }
892
+ }
893
+ ```
894
+
895
+ Por quê: `TriggerManager.execute` faz `catch(Exception)` com `e.printStackTrace()` e segue. A action é gravada de qualquer jeito; o erro vai para o stdout do servidor (e em `trigger_log.err`), mas o caller não é notificado.
896
+
897
+ ✅ **Em vez disso:** se você precisa veto, mover a validação para os checks no próprio `ActionManager` ou usar um `before_purchase_validation`/`before_acquire_validation` que o caller específico realmente respeite (não os `before_create`/`before_win` genéricos).
898
+
899
+ ### 10.5 Anti-pattern — recompilação custosa em loop
900
+
901
+ ❌ **Não fazer:**
902
+
903
+ ```groovy
904
+ void trigger(event, entity, player, database) {
905
+ // Atualiza este próprio trigger a cada execução
906
+ def t = database.getCollection('trigger').findOne('{_id:#}', '<this trigger id>').as(com.funifier.engine.integration.trigger.Trigger.class)
907
+ t.updated = new Date()
908
+ manager.getTriggerManager().add(t)
909
+ }
910
+ ```
185
911
 
186
- - [ ] Trigger aparece na lista GET /v3/trigger
187
- - [ ] Evento é disparado corretamente
188
- - [ ] Script executa sem erros
189
- - [ ] Testar em staging antes de produção
912
+ Por quê: alterar `updated` invalida o cache local. Próxima execução fará `compile` (~10–50ms). Em alta-volume, isso vira o gargalo.
913
+
914
+ ---
915
+
916
+ ## Checklist de Configuração
917
+
918
+ - [ ] `entity` e `event` correspondem a uma combinação **realmente disparada** por algum manager — consultar seção 1 (lista de chamadores) e seção 3.1 (eventos extras ad-hoc).
919
+ - [ ] `script` define `void trigger(event, entity, player, database) { ... }`.
920
+ - [ ] `script` **não** usa `System.*`, `ProcessBuilder`, `File`, `GroovyShell`, `GroovyObject`, nem chama `.execute`/`.getDB`/`.getMongo`/`.dropDatabase` — senão `SecurityException` no AST.
921
+ - [ ] Se o script faz I/O remoto (HTTP, e-mail), setar `timeout` explícito (até **200 segundos** — acima disso o `@TimedInterrupt` mata mesmo).
922
+ - [ ] `GamificationLimits.dailyTriggerExecutions` do tenant comporta o volume esperado — limite zero bloqueia tudo silenciosamente.
923
+ - [ ] Após criar/atualizar via API em ambiente cluster: chamar `PUT /v3/trigger/compile/all` em cada nó (ou aguardar que `TriggerRunner` recompile sozinho via comparação `dates < updated`).
924
+ - [ ] **Armadilha (silencioso):** se você usar `entity:"action_log"` ou `entity:"achievement"`, **nenhum trigger dispara** — essas entidades foram comentadas em `Trigger.java` e os managers usam `Trigger.ENTITY_ACTION` (`"action"`) para o evento de gravação do `ActionLog`, e disparam por `Entity.POINT_CATEGORY.collection` / `Entity.CHALLENGE.collection` para Achievements (não pelo nome `"achievement"`).
925
+ - [ ] **Armadilha (legacy):** `Trigger.errors` e `Trigger.logs` no schema **não são populados** no runtime atual — consultar `trigger_log` em vez disso.
926
+ - [ ] **Armadilha (timeout duplo):** se um script demora mais de 200s independentemente do `timeout` configurado, é o `@TimedInterrupt` em `TriggerRunner.getScript` — não há configuração externa para mudar isso.
927
+ - [ ] **Armadilha (cancelamento):** lançar exceção em `before_create`/`before_win` **não cancela** a operação principal — o caller engole e segue.
928
+ - [ ] **Armadilha (debug ≠ produção):** `POST /v3/trigger/execute/{id}` passa `context = null` e ignora o limite diário. Scripts validados nesse endpoint podem quebrar em prod por NPE em `context.extra` ou por hit no limite.
929
+ - [ ] **Armadilha (sem 404):** `GET /v3/trigger/{id}` de um id inexistente retorna `null` com HTTP 200, não 404.
930
+ - [ ] **Armadilha (HTTP 201 com erro):** `POST /v3/trigger` retorna `201 Created` mesmo quando `status: "ERROR"` no body — sempre checar `status` antes de assumir sucesso.
931
+ - [ ] `Trigger.techniques`, `Trigger.style`, `Trigger.references` são apenas Studio metadata — não usar para lógica de runtime.