funifier-mcp 0.2.24 → 0.2.25

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 (51) hide show
  1. package/.cursor/rules/funifier.mdc +1 -0
  2. package/.github/copilot-instructions.md +1 -0
  3. package/AGENTS.md +1 -0
  4. package/datasource-funifier-docs/.search-index.json +5588 -4475
  5. package/datasource-funifier-docs/.skills-map.json +3 -1
  6. package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +511 -0
  7. package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +1 -1
  8. package/datasource-funifier-docs/knowledge/modules/action.md +96 -39
  9. package/datasource-funifier-docs/knowledge/modules/challenge.md +285 -48
  10. package/datasource-funifier-docs/knowledge/modules/leaderboard.md +173 -71
  11. package/datasource-funifier-docs/knowledge/modules/point.md +108 -37
  12. package/datasource-funifier-docs/knowledge/modules/trigger.md +1 -1
  13. package/dist/cli/init.d.ts +1 -1
  14. package/dist/cli/init.d.ts.map +1 -1
  15. package/dist/cli/init.js +11 -4
  16. package/dist/cli/init.js.map +1 -1
  17. package/dist/core/api-client.d.ts.map +1 -1
  18. package/dist/core/api-client.js +1 -1
  19. package/dist/core/api-client.js.map +1 -1
  20. package/dist/core/constants.d.ts +1 -0
  21. package/dist/core/constants.d.ts.map +1 -1
  22. package/dist/core/constants.js +1 -0
  23. package/dist/core/constants.js.map +1 -1
  24. package/dist/mcp/bundle.js +68 -67
  25. package/dist/mcp/index.js +2 -1
  26. package/dist/mcp/index.js.map +1 -1
  27. package/dist/mcp/tools/get.d.ts.map +1 -1
  28. package/dist/mcp/tools/get.js +22 -8
  29. package/dist/mcp/tools/get.js.map +1 -1
  30. package/dist/mcp/tools/list.d.ts.map +1 -1
  31. package/dist/mcp/tools/list.js +9 -2
  32. package/dist/mcp/tools/list.js.map +1 -1
  33. package/dist/mcp/tools/save.d.ts.map +1 -1
  34. package/dist/mcp/tools/save.js +88 -7
  35. package/dist/mcp/tools/save.js.map +1 -1
  36. package/package.json +3 -2
  37. package/skills/funifier-create-action/SKILL.md +58 -18
  38. package/skills/funifier-create-aggregate/SKILL.md +1 -0
  39. package/skills/funifier-create-challenge/SKILL.md +1 -0
  40. package/skills/funifier-create-custom-page/SKILL.md +1 -0
  41. package/skills/funifier-create-leaderboard/SKILL.md +90 -18
  42. package/skills/funifier-create-level/SKILL.md +1 -0
  43. package/skills/funifier-create-point/SKILL.md +60 -18
  44. package/skills/funifier-create-quiz/SKILL.md +1 -0
  45. package/skills/funifier-create-scheduler/SKILL.md +1 -0
  46. package/skills/funifier-create-trigger/SKILL.md +110 -19
  47. package/skills/funifier-create-virtual-good/SKILL.md +1 -0
  48. package/skills/funifier-debug/SKILL.md +2 -0
  49. package/skills/funifier-help/SKILL.md +1 -0
  50. package/skills/funifier-implement-frontend/SKILL.md +1 -0
  51. package/skills/funifier-index/SKILL.md +9 -1
@@ -3,7 +3,8 @@
3
3
  "knowledge/modules/trigger.md",
4
4
  "knowledge/guides/triggers-guide.md",
5
5
  "knowledge/guides/java-entities.md",
6
- "knowledge/guides/java-managers.md"
6
+ "knowledge/guides/java-managers.md",
7
+ "knowledge/guides/trigger-examples.md"
7
8
  ],
8
9
  "funifier-create-scheduler": [
9
10
  "knowledge/modules/scheduler.md",
@@ -61,6 +62,7 @@
61
62
  "knowledge/guides/java-libraries.md",
62
63
  "knowledge/modules/trigger.md",
63
64
  "knowledge/guides/triggers-guide.md",
65
+ "knowledge/guides/trigger-examples.md",
64
66
  "knowledge/modules/scheduler.md",
65
67
  "knowledge/guides/aggregates.md",
66
68
  "knowledge/modules/public.md"
@@ -0,0 +1,511 @@
1
+ # Trigger Examples — Padrões de Produção
2
+
3
+ Referência prática de eventos, entidades, constantes e exemplos reais para criação de triggers no Funifier.
4
+
5
+ ---
6
+
7
+ ## Eventos Oficiais (dropdown do Studio)
8
+
9
+ | Evento | Quando é acionado |
10
+ |--------|-------------------|
11
+ | `before_win` | Antes de registrar uma conquista (ponto, desafio, item) |
12
+ | `after_win` | Depois de registrar uma conquista |
13
+ | `before_lose` | Antes de remover uma conquista |
14
+ | `after_lose` | Depois de remover uma conquista |
15
+ | `before_create` | Antes de persistir um novo documento |
16
+ | `after_create` | Depois de persistir um novo documento |
17
+ | `before_update` | Antes de atualizar um documento existente |
18
+ | `after_update` | Depois de atualizar um documento |
19
+ | `before_delete` | Antes de deletar um documento |
20
+ | `after_delete` | Depois de deletar um documento |
21
+
22
+ ---
23
+
24
+ ## Eventos Estendidos (free-text only — não aparecem no dropdown)
25
+
26
+ | Evento | Quando é acionado |
27
+ |--------|-------------------|
28
+ | `before_bulk` | Antes de um insert/update em lote via API |
29
+ | `after_bulk` | Depois de um insert/update em lote via API |
30
+ | `csv_before_bulk` | Antes de processar um import CSV |
31
+ | `csv_after_bulk` | Depois de processar um import CSV |
32
+
33
+ > ⚠ Nesses eventos a variável `entity` recebe uma **List** (não um único documento). Itere com `for (Object item : entity)`.
34
+
35
+ ---
36
+
37
+ ## Entidades Disponíveis
38
+
39
+ **Dropdown hardcoded do Studio:**
40
+ `action`, `catalog`, `catalog_item`, `crown`, `leaderboard`, `level`, `player`, `team`, `challenge`, `point_category`, `character_star_stats_level`, `upload`, `mystery_box`, `lottery`
41
+
42
+ **Entidades dinâmicas** (carregadas via `/v3/database/collections`):
43
+ `achievement` e todas as coleções customizadas (`__c`) do projeto aparecem aqui.
44
+ Excluídas: `action_log`, `challenge_progress`, `player_status`, `security`, `technique_field`, `join_log`.
45
+
46
+ **Free-text**: o formulário do Studio permite digitar qualquer nome de entidade/evento não listado.
47
+
48
+ ---
49
+
50
+ ## Constantes de Achievement
51
+
52
+ | Constante Java | Valor int | Descrição |
53
+ |----------------|-----------|-----------|
54
+ | `Achievement.TYPE_POINT` | `0` | Ponto acumulado |
55
+ | `Achievement.TYPE_CHALLENGE` | `1` | Desafio completado |
56
+ | `Achievement.TYPE_VIRTUAL_GOOD` | `2` | Item de catálogo comprado |
57
+ | `Achievement.TYPE_LEVEL` | `3` | Nível alcançado |
58
+ | `Achievement.TYPE_LOTTERY` | `5` | Loteria |
59
+ | `Achievement.TYPE_COMPETITION` | `9` | Competição |
60
+
61
+ ---
62
+
63
+ ## Campos das Entidades Java nos Triggers
64
+
65
+ ### ActionLog (`entity` em triggers de `action`)
66
+ ```java
67
+ entity.actionId // String — ID da ação (ex: "complete_task")
68
+ entity.userId // String — ID do jogador que executou
69
+ entity.attributes // Map<String, Object> — atributos da ação
70
+ entity.time // Date — momento da execução
71
+ entity.id // String — alias para entity._id
72
+ entity._id // String — ID do registro de log
73
+ ```
74
+
75
+ ### Player (`entity` em triggers de `player`)
76
+ ```java
77
+ entity._id / entity.id // String — ID do jogador
78
+ entity.name // String
79
+ entity.email // String
80
+ entity.extra // Map<String, Object> — campos customizados
81
+ entity.teams // List<String> — IDs das equipes
82
+ entity.image // ImageSet — getOriginal().getUrl(), getMedium(), getSmall()
83
+ ```
84
+
85
+ ### Achievement (`entity` em triggers de `achievement` / `point_category`)
86
+ ```java
87
+ entity._id / entity.id // String
88
+ entity.player // String — ID do jogador
89
+ entity.total // double — quantidade
90
+ entity.type // int — ver tabela de constantes acima
91
+ entity.item // String — ID do ponto/desafio/item
92
+ entity.time // Date
93
+ entity.extra // Map<String, Object>
94
+ ```
95
+
96
+ ---
97
+
98
+ ## context.extra — Campos Disponíveis
99
+
100
+ Disponível nos eventos `before_win`, `before_create`, `after_create` quando a trigger é acionada por uma cadeia de conquistas:
101
+
102
+ ```java
103
+ context.extra.parentAchievement // Achievement que gerou este evento
104
+ context.extra.parentAchievement.id // String
105
+ context.extra.parentAchievement.type // int (usar constantes acima)
106
+ context.extra.parentAchievement.item // String
107
+ context.extra.parentAchievement.player // String
108
+ context.extra.parentAchievement.total // double
109
+
110
+ context.extra.parentActionLog // ActionLog que originou a cadeia
111
+ context.extra.parentActionLog.id // String
112
+ context.extra.parentActionLog.actionId // String
113
+ context.extra.parentActionLog.userId // String
114
+ context.extra.parentActionLog.attributes // Map
115
+
116
+ context.extra.originActionLog // ActionLog raiz (ponta da cadeia)
117
+ ```
118
+
119
+ ---
120
+
121
+ ## ⚠ Regra Crítica: Triggers NÃO impedem persistência via throw
122
+
123
+ O Funifier **sempre salva o documento** antes de executar o trigger. Não é possível usar `throw` para cancelar a persistência.
124
+
125
+ **Padrão correto para rejeitar uma criação:**
126
+ 1. Use `after_create` (não `before_create`)
127
+ 2. Valide no trigger; se inválido:
128
+ ```java
129
+ database.delete(entity._id, "collection__c");
130
+ entity.clear();
131
+ entity.put("message", "Motivo da rejeição");
132
+ return;
133
+ ```
134
+ 3. Para lógica complexa, use uma `BusinessException` interna para centralizar o cleanup:
135
+ ```java
136
+ // A exceção é capturada dentro do próprio trigger — não propaga para a plataforma
137
+ try {
138
+ if (condicaoInvalida) throw new BusinessException("Motivo");
139
+ // ... mais validações ...
140
+ } catch (BusinessException e) {
141
+ database.delete(entity._id, "collection__c");
142
+ entity.clear();
143
+ entity.put("message", e.getMessage());
144
+ }
145
+
146
+ public class BusinessException extends Exception {
147
+ private String message;
148
+ public BusinessException(String message) { super(message); this.message = message; }
149
+ public String getMessage() { return this.message; }
150
+ }
151
+ ```
152
+
153
+ **`before_create` é para enriquecimento apenas:** normalizar strings, preencher defaults, adicionar metadados.
154
+
155
+ ---
156
+
157
+ ## Exemplos Anotados
158
+
159
+ ### Exemplo 1 — `action | before_win`: Atualizar coleção relacionada
160
+
161
+ Padrão: ler atributos da ActionLog, atualizar um documento em coleção customizada.
162
+
163
+ ```json
164
+ {
165
+ "name": "Update Task Status on Action Win",
166
+ "entity": "action",
167
+ "event": "before_win",
168
+ "active": true,
169
+ "script": "void trigger(event, entity, player, database) {\n if (!entity.actionId.equals(\"complete_task\")) return;\n\n String taskId = String.valueOf(entity.attributes.get(\"taskId\"));\n if (taskId == null || taskId.equals(\"null\")) return;\n\n HashMap<String, Object> task = manager.getJongoConnection()\n .getCollection(\"task__c\")\n .findOne(\"{_id:#}\", taskId).as(HashMap.class);\n\n if (task != null) {\n task.put(\"status\", \"done\");\n task.put(\"completedAt\", new Date());\n manager.getJongoConnection().getCollection(\"task__c\").save(task);\n entity.attributes.put(\"status\", \"processed\");\n println(\"task updated: \" + JsonUtil.toJson(task));\n }\n}\n"
170
+ }
171
+ ```
172
+
173
+ ```groovy
174
+ void trigger(event, entity, player, database) {
175
+ if (!entity.actionId.equals("complete_task")) return;
176
+
177
+ String taskId = String.valueOf(entity.attributes.get("taskId"));
178
+ if (taskId == null || taskId.equals("null")) return;
179
+
180
+ HashMap<String, Object> task = manager.getJongoConnection()
181
+ .getCollection("task__c")
182
+ .findOne("{_id:#}", taskId).as(HashMap.class);
183
+
184
+ if (task != null) {
185
+ task.put("status", "done");
186
+ task.put("completedAt", new Date());
187
+ manager.getJongoConnection().getCollection("task__c").save(task);
188
+ entity.attributes.put("status", "processed");
189
+ println("task updated: " + JsonUtil.toJson(task));
190
+ }
191
+ }
192
+ ```
193
+
194
+ ---
195
+
196
+ ### Exemplo 2 — `achievement | before_create`: Enriquecer com origem
197
+
198
+ Padrão: ler `context.extra` para saber qual action log ou achievement gerou este registro.
199
+
200
+ ```json
201
+ {
202
+ "name": "Tag Achievement with Origin",
203
+ "entity": "achievement",
204
+ "event": "before_create",
205
+ "active": true,
206
+ "script": "void trigger(event, entity, player, database) {\n if (entity.type != Achievement.TYPE_POINT) return;\n\n if (entity.extra == null) entity.extra = new HashMap<>();\n if (context.extra == null) return;\n\n if (context.extra.parentAchievement != null &&\n context.extra.parentAchievement.type == Achievement.TYPE_CHALLENGE) {\n entity.extra.put(\"originChallenge\", context.extra.parentAchievement.id);\n }\n\n if (context.extra.parentActionLog != null) {\n entity.extra.put(\"originAction\", context.extra.parentActionLog.id);\n entity.extra.put(\"originActionId\", context.extra.parentActionLog.actionId);\n }\n\n println(\"entity: \" + JsonUtil.toJson(entity));\n}\n"
207
+ }
208
+ ```
209
+
210
+ ```groovy
211
+ void trigger(event, entity, player, database) {
212
+ if (entity.type != Achievement.TYPE_POINT) return;
213
+
214
+ if (entity.extra == null) entity.extra = new HashMap<>();
215
+ if (context.extra == null) return;
216
+
217
+ if (context.extra.parentAchievement != null &&
218
+ context.extra.parentAchievement.type == Achievement.TYPE_CHALLENGE) {
219
+ entity.extra.put("originChallenge", context.extra.parentAchievement.id);
220
+ }
221
+
222
+ if (context.extra.parentActionLog != null) {
223
+ entity.extra.put("originAction", context.extra.parentActionLog.id);
224
+ entity.extra.put("originActionId", context.extra.parentActionLog.actionId);
225
+ }
226
+
227
+ println("entity: " + JsonUtil.toJson(entity));
228
+ }
229
+ ```
230
+
231
+ ---
232
+
233
+ ### Exemplo 3 — `action | after_win`: Criar Achievement programaticamente
234
+
235
+ Padrão: creditar pontos para outro jogador como efeito colateral de uma ação.
236
+
237
+ ```json
238
+ {
239
+ "name": "Credit Referrer Points on Invite Accept",
240
+ "entity": "action",
241
+ "event": "after_win",
242
+ "active": true,
243
+ "script": "void trigger(event, entity, player, database) {\n if (!entity.actionId.equals(\"accept_invite\")) return;\n\n String referrerId = String.valueOf(entity.attributes.get(\"referrer\"));\n if (referrerId == null || referrerId.equals(\"null\")) return;\n\n Achievement credit = new Achievement();\n credit.id = Guid.newShortGuid();\n credit.type = Achievement.TYPE_POINT;\n credit.player = referrerId;\n credit.item = \"xp\";\n credit.total = 50;\n credit.time = new Date();\n credit.extra = new HashMap();\n credit.extra.put(\"origin\", entity.id);\n credit.extra.put(\"originAction\", entity.actionId);\n\n manager.getAchievementManager().addAchievement(credit);\n println(\"credited: \" + JsonUtil.toJson(credit));\n}\n"
244
+ }
245
+ ```
246
+
247
+ ```groovy
248
+ void trigger(event, entity, player, database) {
249
+ if (!entity.actionId.equals("accept_invite")) return;
250
+
251
+ String referrerId = String.valueOf(entity.attributes.get("referrer"));
252
+ if (referrerId == null || referrerId.equals("null")) return;
253
+
254
+ Achievement credit = new Achievement();
255
+ credit.id = Guid.newShortGuid();
256
+ credit.type = Achievement.TYPE_POINT;
257
+ credit.player = referrerId;
258
+ credit.item = "xp";
259
+ credit.total = 50;
260
+ credit.time = new Date();
261
+ credit.extra = new HashMap();
262
+ credit.extra.put("origin", entity.id);
263
+ credit.extra.put("originAction", entity.actionId);
264
+
265
+ manager.getAchievementManager().addAchievement(credit);
266
+ println("credited: " + JsonUtil.toJson(credit));
267
+ }
268
+ ```
269
+
270
+ ---
271
+
272
+ ### Exemplo 4 — `player | after_create`: Inicializar dados do jogador
273
+
274
+ Padrão: ao criar um jogador, buscar dados relacionados em coleção customizada e preencher `extra`.
275
+
276
+ ```json
277
+ {
278
+ "name": "Initialize Player from Employee Data",
279
+ "entity": "player",
280
+ "event": "after_create",
281
+ "active": true,
282
+ "script": "void trigger(event, entity, player, database) {\n println(\"new player: \" + JsonUtil.toJson(entity));\n\n HashMap<String, Object> employee = manager.getJongoConnection()\n .getCollection(\"employee__c\")\n .findOne(\"{email:#}\", entity.email).as(HashMap.class);\n\n if (employee == null) return;\n\n Player p = manager.getPlayerManager().findById(entity._id);\n if (p == null) return;\n\n if (p.extra == null) p.extra = new HashMap();\n p.extra.put(\"department\", employee.get(\"department\"));\n p.extra.put(\"role\", employee.get(\"role\"));\n p.extra.put(\"employeeId\", employee.get(\"_id\"));\n\n manager.getPlayerManager().insert(p);\n println(\"player initialized: \" + p._id);\n}\n"
283
+ }
284
+ ```
285
+
286
+ ```groovy
287
+ void trigger(event, entity, player, database) {
288
+ println("new player: " + JsonUtil.toJson(entity));
289
+
290
+ HashMap<String, Object> employee = manager.getJongoConnection()
291
+ .getCollection("employee__c")
292
+ .findOne("{email:#}", entity.email).as(HashMap.class);
293
+
294
+ if (employee == null) return;
295
+
296
+ Player p = manager.getPlayerManager().findById(entity._id);
297
+ if (p == null) return;
298
+
299
+ if (p.extra == null) p.extra = new HashMap();
300
+ p.extra.put("department", employee.get("department"));
301
+ p.extra.put("role", employee.get("role"));
302
+ p.extra.put("employeeId", employee.get("_id"));
303
+
304
+ manager.getPlayerManager().insert(p);
305
+ println("player initialized: " + p._id);
306
+ }
307
+ ```
308
+
309
+ ---
310
+
311
+ ### Exemplo 5a — `custom__c | before_create`: Normalizar campos antes de persistir
312
+
313
+ Padrão: `before_create` **não rejeita** — apenas enriquece/normaliza o `entity` antes de salvar.
314
+
315
+ ```json
316
+ {
317
+ "name": "Normalize Employee Fields",
318
+ "entity": "employee__c",
319
+ "event": "before_create",
320
+ "active": true,
321
+ "script": "void trigger(event, entity, player, database) {\n entity.put(\"email\", normalizeEmail(entity.get(\"email\")));\n entity.put(\"name\", normalizeName(entity.get(\"name\")));\n entity.put(\"employeeId\", extractNumeric(entity.get(\"employeeId\")));\n}\n\nString normalizeEmail(Object value) {\n if (value == null) return \"\";\n String s = String.valueOf(value).replaceAll(\"\\\\s\", \"\");\n return s.equals(\"null\") ? \"\" : s.toLowerCase();\n}\n\nString normalizeName(Object value) {\n if (value == null) return \"\";\n String s = String.valueOf(value).trim();\n return s.equals(\"null\") ? \"\" : s;\n}\n\nString extractNumeric(Object value) {\n if (value == null) return \"\";\n try {\n return String.valueOf(Long.parseLong(String.valueOf(value).replaceAll(\"\\\\s\", \"\")));\n } catch (NumberFormatException e) {\n return \"\";\n }\n}\n"
322
+ }
323
+ ```
324
+
325
+ ```groovy
326
+ void trigger(event, entity, player, database) {
327
+ entity.put("email", normalizeEmail(entity.get("email")));
328
+ entity.put("name", normalizeName(entity.get("name")));
329
+ entity.put("employeeId", extractNumeric(entity.get("employeeId")));
330
+ }
331
+
332
+ String normalizeEmail(Object value) {
333
+ if (value == null) return "";
334
+ String s = String.valueOf(value).replaceAll("\\s", "");
335
+ return s.equals("null") ? "" : s.toLowerCase();
336
+ }
337
+
338
+ String normalizeName(Object value) {
339
+ if (value == null) return "";
340
+ String s = String.valueOf(value).trim();
341
+ return s.equals("null") ? "" : s;
342
+ }
343
+
344
+ String extractNumeric(Object value) {
345
+ if (value == null) return "";
346
+ try {
347
+ return String.valueOf(Long.parseLong(String.valueOf(value).replaceAll("\\s", "")));
348
+ } catch (NumberFormatException e) {
349
+ return "";
350
+ }
351
+ }
352
+ ```
353
+
354
+ ---
355
+
356
+ ### Exemplo 5b — `custom__c | after_create`: Validar e rejeitar criação
357
+
358
+ Padrão: document já foi salvo — se inválido, deletar e retornar erro no `entity`.
359
+
360
+ ```json
361
+ {
362
+ "name": "Validate Order on Create",
363
+ "entity": "order__c",
364
+ "event": "after_create",
365
+ "active": true,
366
+ "script": "void trigger(event, entity, player, database) {\n try {\n if (player == null || player.isEmpty()) {\n throw new BusinessException(\"Player obrigatório.\");\n }\n\n HashMap<String, Object> schedule = manager.getJongoConnection()\n .getCollection(\"schedule__c\")\n .findOne(\"{status:#}\", \"open\").as(HashMap.class);\n\n if (schedule == null) {\n throw new BusinessException(\"Nenhuma janela de cadastro aberta.\");\n }\n\n long existing = manager.getJongoConnection().getCollection(\"order__c\")\n .count('{\"_id\":{\"$ne\":#},\"owner\":#,\"status\":{\"$ne\":\"cancelled\"}}',\n entity._id, player);\n\n if (existing >= 3) {\n throw new BusinessException(\"Limite de cadastros atingido nesta janela.\");\n }\n\n } catch (BusinessException e) {\n database.delete(entity._id, \"order__c\");\n entity.clear();\n entity.put(\"message\", e.getMessage());\n println(\"rejected: \" + e.getMessage());\n return;\n }\n}\n\npublic class BusinessException extends Exception {\n private String message;\n public BusinessException(String msg) { super(msg); this.message = msg; }\n public String getMessage() { return this.message; }\n}\n"
367
+ }
368
+ ```
369
+
370
+ ```groovy
371
+ void trigger(event, entity, player, database) {
372
+ try {
373
+ if (player == null || player.isEmpty()) {
374
+ throw new BusinessException("Player obrigatório.");
375
+ }
376
+
377
+ HashMap<String, Object> schedule = manager.getJongoConnection()
378
+ .getCollection("schedule__c")
379
+ .findOne("{status:#}", "open").as(HashMap.class);
380
+
381
+ if (schedule == null) {
382
+ throw new BusinessException("Nenhuma janela de cadastro aberta.");
383
+ }
384
+
385
+ long existing = manager.getJongoConnection().getCollection("order__c")
386
+ .count('{"_id":{"$ne":#},"owner":#,"status":{"$ne":"cancelled"}}',
387
+ entity._id, player);
388
+
389
+ if (existing >= 3) {
390
+ throw new BusinessException("Limite de cadastros atingido nesta janela.");
391
+ }
392
+
393
+ } catch (BusinessException e) {
394
+ database.delete(entity._id, "order__c");
395
+ entity.clear();
396
+ entity.put("message", e.getMessage());
397
+ println("rejected: " + e.getMessage());
398
+ return;
399
+ }
400
+ }
401
+
402
+ public class BusinessException extends Exception {
403
+ private String message;
404
+ public BusinessException(String msg) { super(msg); this.message = msg; }
405
+ public String getMessage() { return this.message; }
406
+ }
407
+ ```
408
+
409
+ ---
410
+
411
+ ### Exemplo 6 — `custom__c | after_bulk`: Processar import em lote
412
+
413
+ Padrão: `entity` é uma **List** — itere e processe cada item.
414
+
415
+ ```json
416
+ {
417
+ "name": "Process Imported Records",
418
+ "entity": "import__c",
419
+ "event": "after_bulk",
420
+ "active": true,
421
+ "script": "void trigger(event, entity, player, database) {\n if (entity == null || entity.isEmpty()) return;\n\n println(\"bulk size: \" + entity.size());\n\n for (Object raw : entity) {\n HashMap<String, Object> item = (HashMap<String, Object>) raw;\n if (item == null) continue;\n\n String employeeId = String.valueOf(item.get(\"employeeId\"));\n if (employeeId == null || employeeId.equals(\"null\")) continue;\n\n HashMap<String, Object> employee = manager.getJongoConnection()\n .getCollection(\"employee__c\")\n .findOne(\"{_id:#}\", employeeId).as(HashMap.class);\n\n if (employee != null) {\n item.put(\"department\", employee.get(\"department\"));\n item.put(\"processed\", true);\n manager.getJongoConnection().getCollection(\"import__c\").save(item);\n }\n }\n\n println(\"bulk processed: \" + entity.size() + \" records\");\n}\n"
422
+ }
423
+ ```
424
+
425
+ ```groovy
426
+ void trigger(event, entity, player, database) {
427
+ if (entity == null || entity.isEmpty()) return;
428
+
429
+ println("bulk size: " + entity.size());
430
+
431
+ for (Object raw : entity) {
432
+ HashMap<String, Object> item = (HashMap<String, Object>) raw;
433
+ if (item == null) continue;
434
+
435
+ String employeeId = String.valueOf(item.get("employeeId"));
436
+ if (employeeId == null || employeeId.equals("null")) continue;
437
+
438
+ HashMap<String, Object> employee = manager.getJongoConnection()
439
+ .getCollection("employee__c")
440
+ .findOne("{_id:#}", employeeId).as(HashMap.class);
441
+
442
+ if (employee != null) {
443
+ item.put("department", employee.get("department"));
444
+ item.put("processed", true);
445
+ manager.getJongoConnection().getCollection("import__c").save(item);
446
+ }
447
+ }
448
+
449
+ println("bulk processed: " + entity.size() + " records");
450
+ }
451
+ ```
452
+
453
+ ---
454
+
455
+ ### Exemplo 7 — Registrar uma ação programaticamente (`ActionManager.track`)
456
+
457
+ Padrão: criar e disparar um ActionLog de dentro de um trigger, acionando as regras de gamificação normalmente.
458
+
459
+ ```json
460
+ {
461
+ "name": "Track Secondary Action on Win",
462
+ "entity": "action",
463
+ "event": "after_win",
464
+ "active": true,
465
+ "script": "void trigger(event, entity, player, database) {\n if (!entity.actionId.equals(\"complete_task\")) return;\n\n ActionLog log = new ActionLog();\n log.id = Guid.newShortGuid();\n log.userId = entity.userId;\n log.actionId = \"task_completed_by_team\";\n log.time = new Date();\n log.attributes = new HashMap();\n log.attributes.put(\"taskId\", entity.attributes.get(\"taskId\"));\n log.attributes.put(\"origin\", entity.id);\n manager.getActionManager().track(log);\n println(\"tracked async: \" + JsonUtil.toJson(log));\n}\n"
466
+ }
467
+ ```
468
+
469
+ ```groovy
470
+ void trigger(event, entity, player, database) {
471
+ if (!entity.actionId.equals("complete_task")) return;
472
+
473
+ // Async: dispara e não bloqueia — conquistas processadas em background
474
+ ActionLog log = new ActionLog();
475
+ log.id = Guid.newShortGuid(); // .id, não ._id
476
+ log.userId = entity.userId;
477
+ log.actionId = "task_completed_by_team";
478
+ log.time = new Date();
479
+ log.attributes = new HashMap();
480
+ log.attributes.put("taskId", entity.attributes.get("taskId"));
481
+ log.attributes.put("origin", entity.id);
482
+ manager.getActionManager().track(log);
483
+ println("tracked async: " + JsonUtil.toJson(log));
484
+ }
485
+ ```
486
+
487
+ Variante síncrona — use quando precisar saber quais conquistas foram geradas:
488
+
489
+ ```groovy
490
+ void trigger(event, entity, player, database) {
491
+ // Add action filter: if (!entity.actionId.equals("your_action")) return;
492
+
493
+ ActionLog log = new ActionLog();
494
+ log.id = Guid.newShortGuid();
495
+ log.userId = entity.userId;
496
+ log.actionId = "referral_bonus";
497
+ log.time = new Date();
498
+ log.attributes = new HashMap();
499
+
500
+ // trackSynchonous bloqueia e retorna as conquistas geradas
501
+ List<Achievement> earned = manager.getActionManager().trackSynchonous(log);
502
+ if (earned != null) {
503
+ println("earned: " + earned.size() + " achievements");
504
+ for (Achievement a : earned) {
505
+ println("achievement: type=" + a.type + " item=" + a.item + " total=" + a.total);
506
+ }
507
+ }
508
+ }
509
+ ```
510
+
511
+ > ⚠ Nunca chame `track` ou `trackSynchonous` dentro de um trigger `before_win` / `after_win` para a **mesma entidade/ação** que disparou o trigger — causa loop infinito. Use uma action diferente ou verifique com `entity.actionId` antes.
@@ -109,7 +109,7 @@ manager.getPlayerManager().delete("john");
109
109
  ```java
110
110
  Action a = manager.getActionManager().findActionById("sell");
111
111
  manager.getActionManager().track(actionLog);
112
- List<Achievement> results = manager.getActionManager().trackSynchronous(actionLog);
112
+ List<Achievement> results = manager.getActionManager().trackSynchonous(actionLog);
113
113
  ```
114
114
 
115
115
  ### CatalogManager