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,45 +1,584 @@
1
- # LastMile (Última Milha)
1
+ # `lastmile`
2
2
 
3
3
  **Acesso Studio:** `/studio/lastmile`
4
- **API Endpoint:** `/v3/lastmile`
4
+ **API Endpoint:** `/v3/lastmile` (mensagens) · `/v3/bonus` (motor de cálculo acoplado)
5
+ **Coleção MongoDB:** `last_mile` · `bonus`
5
6
 
6
- ## O que é
7
+ > Engenharia reversa do pacote `com.funifier.engine.lastmile` em `funifier-service`
8
+ > (`LastMile`, `LastMileManager`, `LastMileRest`, `LastMileLog`, `Bonus`, `BonusManager`, `BonusRest`, `BonusRule`, `BonusReward`, `Progress`).
9
+ > Esta documentação descreve o comportamento **real do código** — divergências em relação ao schema e comportamentos silenciosos estão explicitados.
7
10
 
8
- Envio de mensagens motivacionais quando o jogador está próximo de uma meta. Permite configurar mensagens automáticas para engajar e lembrar os jogadores sobre metas e desafios que estão prestes a serem concluídos, aumentando a taxa de finalização.
11
+ ---
9
12
 
10
- ## Quando usar
13
+ ## 1. Visão Geral
11
14
 
12
- - Para lembrar jogadores que estão perto de completar desafios
13
- - Para aumentar taxa de conclusão de metas
14
- - Para enviar mensagens motivacionais personalizadas
15
- - Para engajar jogadores que estão a 80-90% de uma meta
15
+ **LastMile (Last Mile Drive / "última milha")** entrega mensagens de incentivo a jogadores cujo progresso está dentro de uma **faixa percentual semi-aberta `[start, ends)`** de uma meta. A mensagem funciona como um "coach": _"faltam 2 vendas para você atingir 100% e ganhar o bônus X"_.
16
16
 
17
- ## Dependências
17
+ O pacote `lastmile` contém **dois motores acoplados**, persistidos em coleções distintas e expostos por REST distintos:
18
18
 
19
- - **Challenge**: desafios devem existir para monitorar progresso
19
+ | Motor | Coleção | REST | Papel |
20
+ |-------|---------|------|-------|
21
+ | `LastMile` | `last_mile` | `/v3/lastmile` | Define a mensagem, a faixa de disparo, o alvo e o agendamento |
22
+ | `Bonus` | `bonus` | `/v3/bonus` | Calcula progresso e recompensas do jogador; **é quem produz o texto** das mensagens lastmile do tipo bônus |
20
23
 
21
- ## Checklist de Configuração no Studio
24
+ LastMile **não é autossuficiente** no modo principal (`type = TYPE_BONUS`): a mensagem só é gerada por dentro de `BonusManager.calculate()`, que mede o progresso real do jogador. Por isso ambos os motores estão documentados aqui.
22
25
 
23
- - [ ] Definir condição de disparo (% de progresso)
24
- - [ ] Definir mensagem motivacional
25
- - [ ] Vincular ao desafio ou meta específica
26
+ **Dependências de runtime (via `ManagerFactory`):**
26
27
 
27
- ## API Endpoints
28
+ - `CrowningManager.calculatePlayerLeaderHelper` — calcula o resultado real do jogador por período/operação.
29
+ - `SchedulerManager` / `SchedulerRunner` — agenda e dispara a geração das mensagens (coleção `scheduler`).
30
+ - `NotificationManager` — entrega as mensagens como notificações privadas (coleção `notification`).
31
+ - `MustacheUtils` + Groovy (`GroovyClassLoader` + `SecureASTCustomizer`) — interpolação e preparação de variáveis da mensagem.
32
+ - `exp4j` (`ExpressionBuilder`) — avalia fórmulas de `goal` e `total`.
33
+ - `GameTechniqueManager` — atribui o código de técnica `GT53` ao LastMile.
28
34
 
29
- ### Listar LastMile
30
- **Método:** GET
31
- **Endpoint:** `/v3/lastmile`
35
+ ---
32
36
 
33
- ### Criar LastMile
34
- **Método:** POST
35
- **Endpoint:** `/v3/lastmile`
37
+ ## 2. Arquitetura e Fluxos
36
38
 
37
- ### Deletar LastMile
38
- **Método:** DELETE
39
- **Endpoint:** `/v3/lastmile/:id`
39
+ ### 2.1 Dois caminhos de geração (divergência runtime importante)
40
40
 
41
- ## Validações e Testes
41
+ Existem **dois caminhos distintos** que produzem mensagens lastmile, e eles têm efeitos colaterais diferentes:
42
42
 
43
- - [ ] Configuração de LastMile aparece na lista
44
- - [ ] Mensagem é disparada ao atingir % configurado
45
- - [ ] Jogador recebe notificação corretamente
43
+ | Caminho | Disparado por | Envia notificação? | Saída |
44
+ |---------|---------------|--------------------|-------|
45
+ | **Cron** | `SchedulerRunner` `LastMileManager.schedulerCallback()` | **Sim** (`NotificationManager.send`) | Notificações privadas por jogador |
46
+ | **Inline** | `BonusManager.calculate()` (endpoints `/v3/bonus/.../calculate*`) | **Não** | Apenas `lastmiles[]` no corpo da resposta |
47
+
48
+ > O caminho inline (cálculo via API de bônus) **nunca envia notificações** — ele apenas devolve as mensagens calculadas no JSON. O envio é exclusivo do caminho cron.
49
+
50
+ ### 2.2 Pipeline principal — cron / `type = TYPE_BONUS`
51
+
52
+ ```
53
+ [Criação] LastMileRest.insert() → LastMileManager.insert()
54
+ └─ gera _id (Guid.newShortGuid) se ausente
55
+ └─ deleteAllByEntityItemEvent("last_mile", id, "notify") // limpa scheduler antigo
56
+ └─ se lastmile.scheduler != null:
57
+ clona SchedulerConfig (active=true, timezone, entity="last_mile",
58
+ event="notify", item=id, script="") → SchedulerManager.add()
59
+ └─ collection("last_mile").save(lastmile)
60
+ └─ compiled.remove(id) // invalida cache do script Groovy (nó local)
61
+
62
+ [Disparo] SchedulerRunner.run()
63
+ └─ StatisticManager.newSchedulerExecution(id) // checa limite diário do tenant
64
+ └─ runEntity() // pois entity/event/item != null
65
+
66
+ [Dispatch] ManagerFactory.getSchedulerCallback("last_mile") → LastMileManager
67
+ └─ se callback == null → adiciona exceção e DESATIVA o scheduler (active=false)
68
+
69
+ [Callback] LastMileManager.schedulerCallback()
70
+ └─ lastmile = find(item)
71
+ └─ if type == 8 (TYPE_BONUS) → generateLastmileDriveNotificationsForBonus()
72
+ └─ if type == 99 (TYPE_CUSTOM) → generateLastmileDriveNotificationsForCustom()
73
+ └─ (qualquer outro type → NADA acontece, silenciosamente)
74
+
75
+ [Alvo] findPlayersToLastMile(lastmile)
76
+ └─ se lastmile.to vazio/null → retorna LISTA VAZIA (ver §5 / §7)
77
+ └─ senão: distinct("_id") em player com filtro raw { _id:{$exists:true}, <to> }
78
+
79
+ [Cálculo] para cada player:
80
+ └─ BonusManager.calculate(bonus, player, params{player,global}, mile=lastmile)
81
+ └─ result.get("lastmiles") → List<LastMileLog>
82
+
83
+ [Mensagem] generateMessage(lastmile, params)
84
+ └─ se lastmile.script != "" → runScript() (Groovy seguro, timeout 5s)
85
+ └─ MustacheUtils.parse(lastmile.message, params)
86
+
87
+ [Notifica] para cada LastMileLog:
88
+ └─ NotificationDefinition(EVENT_WIN=0, TYPE_TEXT=0, SCOPE_PRIVATE=0, message)
89
+ └─ NotificationManager.send(notification) // efeito colateral persistente
90
+ ```
91
+
92
+ ### Fluxo de geração — `firelastmile` (cron, TYPE_BONUS)
93
+
94
+ ```mermaid
95
+ flowchart LR
96
+ A[POST /v3/lastmile] --> B[LastMileManager.insert]
97
+ B --> C{scheduler != null?}
98
+ C -- sim --> D[SchedulerManager.add\nentity=last_mile event=notify]
99
+ C -- não --> E[apenas save em last_mile]
100
+ D --> F[SchedulerRunner.run]
101
+ F --> G{limite diário ok?}
102
+ G -- não --> Z[exceção: limite excedido]
103
+ G -- sim --> H[getSchedulerCallback last_mile]
104
+ H --> I[schedulerCallback]
105
+ I --> J{type}
106
+ J -- 8 BONUS --> K[generateForBonus]
107
+ J -- 99 CUSTOM --> L[generateForCustom]
108
+ J -- outro --> X[nada]
109
+ K --> M[findPlayersToLastMile]
110
+ M --> N{to vazio?}
111
+ N -- sim --> Y[0 jogadores - silencioso]
112
+ N -- não --> O[BonusManager.calculate por player]
113
+ O --> P[generateMessage mustache+groovy]
114
+ P --> Q[NotificationManager.send]
115
+ ```
116
+
117
+ ### 2.3 Pipeline — cron / `type = TYPE_CUSTOM`
118
+
119
+ Só funciona se **`entity`** e **`aggregate`** estiverem preenchidos (método `generateLastmileDriveNotificationsForCustom`):
120
+
121
+ 1. `findPlayersToLastMile(lastmile)` — obtém ids dos jogadores (mesma regra do `to`).
122
+ 2. Substitui o token literal `FUNIFIER_PLAYER_IDS` no `aggregate` pela lista de ids serializada.
123
+ 3. Executa o pipeline de agregação na coleção indicada por `lastmile.entity`, materializando cada documento como um `Progress`.
124
+ 4. Para cada `Progress`: `percent = progress.getPercentCompleted()`. Se `start <= percent < ends` e há mensagem → `NotificationManager.send`.
125
+
126
+ > No modo custom o `Progress` vem **direto da agregação que o usuário escreve** — o engine não calcula progresso; ele confia no shape devolvido pelo pipeline.
127
+
128
+ ### 2.4 Algoritmo de recompensa e gap — `BonusManager.calculate()`
129
+
130
+ ```
131
+ percent = pro.getPercentCompleted() // ver §3.6 (média dos micro-progressos)
132
+
133
+ para cada reward r em bonus.rewards:
134
+ total = exp4j( mustache(r.total, params) )
135
+ se r.start <= percent < r.ends:
136
+ cria Achievement(type=r.type, item=r.item, total=(long)total, player) // NÃO PERSISTE
137
+ senão se percent < r.start: // jogador ainda não atingiu a faixa
138
+ p = clona Progress e reposiciona meta para r.start (moveGoalToSpecificPercentualPosition)
139
+ params["progress"] = p; params["reward"] = r; params["reward_total"] = total
140
+ para cada lastmile vinculado (findAllByItem(TYPE_BONUS, bonusId)):
141
+ se lastmile.start <= percent < lastmile.ends:
142
+ gera LastMileLog( message = generateMessage(lastmile, params),
143
+ image = lastmile.image, priority = lastmile.priority )
144
+ // se percent >= r.ends → faixa já ultrapassada → reward IGNORADA (sem achievement, sem lastmile)
145
+
146
+ retorna { achievements, progress, progress_rewards, lastmiles }
147
+ ```
148
+
149
+ Pontos não óbvios:
150
+
151
+ - **`calculate` não premia.** Os `Achievement` montados são devolvidos no JSON, mas **nunca persistidos** (não há chamada a `AchievementManager.add/insert/save`). A premiação real ocorre em outro fluxo (triggers/achievement). É uma API de **preview/cálculo**.
152
+ - A mensagem lastmile só nasce quando o jogador está **abaixo** da faixa de recompensa (`percent < r.start`) — é o "ainda falta" que o reward representa.
153
+ - Toda a execução está dentro de `try { } catch (Exception)` que devolve `{ "error": <mensagem> }` — **qualquer falha é engolida** (ver §9).
154
+
155
+ ### 2.5 Interação entre módulos (cron vs inline)
156
+
157
+ ```mermaid
158
+ sequenceDiagram
159
+ participant Cron as SchedulerRunner
160
+ participant LM as LastMileManager
161
+ participant BM as BonusManager
162
+ participant NM as NotificationManager
163
+ participant API as BonusRest
164
+
165
+ Note over Cron,NM: Caminho CRON (notifica)
166
+ Cron->>LM: schedulerCallback(scheduler)
167
+ LM->>BM: calculate(bonus, player, mile=lastmile)
168
+ BM-->>LM: result.lastmiles[]
169
+ LM->>NM: send(notification) // efeito colateral
170
+
171
+ Note over API,BM: Caminho INLINE (não notifica)
172
+ API->>BM: calculate(bonus, player) // GET /v3/bonus/:id/calculate
173
+ BM->>BM: findAllByItem(TYPE_BONUS, bonusId)
174
+ BM-->>API: { achievements, progress, lastmiles[] } // só JSON
175
+ ```
176
+
177
+ ---
178
+
179
+ ## 3. Estrutura dos Objetos
180
+
181
+ ### 3.1 `LastMile` — documento raiz (coleção `last_mile`)
182
+
183
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
184
+ |-------|------|--------|-------------|-----------|
185
+ | `_id` | String | `Guid.newShortGuid()` se ausente | — | Id curto gerado no insert |
186
+ | `title` | String | — | recomendado | Usado como `name` do scheduler |
187
+ | `type` | int | `0` | sim (lógico) | Tipo do objeto monitorado (ver enum abaixo). Só `8` e `99` têm tratamento |
188
+ | `item` | String | — | sim p/ `type=8` | Id do `bonus` monitorado |
189
+ | `start` | double | `0.0` | sim | Limite inferior da faixa de progresso (inclusivo) |
190
+ | `ends` | double | `0.0` | sim | Limite superior da faixa de progresso (exclusivo) |
191
+ | `priority` | double | `0.0` | não | Prioridade da mensagem (copiada para o `LastMileLog`) |
192
+ | `message` | String | — | sim | Template Mustache (`{{player.name}}`, `{{progress.gap}}`, `{{reward.title}}`…) |
193
+ | `image` | String | — | não | URL de imagem (copiada para o `LastMileLog`) |
194
+ | `script` | String | — | não | Script Groovy `void prepare(lastmile, params){…}` para preparar variáveis |
195
+ | `to` | String | — | **crítico p/ cron** | Filtro Mongo **raw** aplicado a `player` (ex.: `extra.role:'asven',active:true`). **Vazio = nenhum jogador** |
196
+ | `scheduler` | `SchedulerConfig` | `null` | não | Agenda a geração automática. Sem ele, não há disparo cron |
197
+ | `entity` | String | `null` | sim p/ `type=99` | Coleção onde o `aggregate` roda (somente custom) |
198
+ | `aggregate` | String | `null` | sim p/ `type=99` | Pipeline de agregação JSON; usa o token `FUNIFIER_PLAYER_IDS` |
199
+ | `techniques` | `List<String>` | `null` | não | Auto-preenchido com `["GT53"]` (ver §3.8). Ignorado na lógica de lastmile |
200
+
201
+ **Enum `type`** (`Achievement.TYPE_*`) — apenas dois são tratados no `schedulerCallback`:
202
+
203
+ | Valor | Constante | Tratamento no LastMile |
204
+ |-------|-----------|------------------------|
205
+ | `8` | `TYPE_BONUS` | ✅ `generateLastmileDriveNotificationsForBonus` |
206
+ | `99` | `TYPE_CUSTOM` | ✅ `generateLastmileDriveNotificationsForCustom` |
207
+ | `0,1,2,3,4,5,6,7,9,50` | POINT, CHALLENGE, CATALOG_ITEM, LEVEL, CROWN, LOTTERY, MYSTERY_BOX, CHARACTER_STAR_STAT, COMPETITION, LOTTERY_TICKET | ❌ Aceitos no schema, mas o callback **não faz nada** |
208
+
209
+ **Campos aceitos e silenciosamente ignorados:**
210
+ - `techniques` — não influencia geração de mensagem; serve apenas para catalogação de técnicas (`GT53`).
211
+ - Qualquer campo desconhecido — a classe é anotada com `@JsonIgnoreProperties(ignoreUnknown=true)`: campos extras enviados no POST são descartados sem erro.
212
+
213
+ ### 3.2 `SchedulerConfig` — subentidade `scheduler` (coleção `scheduler`)
214
+
215
+ Campos efetivamente usados pelo LastMile no `insert` (os demais são herdados do módulo de scheduler):
216
+
217
+ | Campo | Tipo | Comportamento no LastMile |
218
+ |-------|------|---------------------------|
219
+ | `cron` | String | Expressão cron de agendamento (informada pelo cliente) |
220
+ | `id` | String | **Sobrescrito** com `lastmile.id` |
221
+ | `name` | String | **Sobrescrito** com `lastmile.title` |
222
+ | `active` | boolean | **Forçado** para `true` |
223
+ | `timezone` | String | **Sobrescrito** com o timezone da organização (`SecurityManager.find().getTimeZone()`) |
224
+ | `entity` | String | **Forçado** para `"last_mile"` |
225
+ | `event` | String | **Forçado** para `"notify"` |
226
+ | `item` | String | **Forçado** para `lastmile.id` |
227
+ | `script` | String | **Forçado** para `""` (a lógica vem do callback, não do script genérico) |
228
+ | `timeout` | Long | Timeout em segundos da execução (default 30 no runner genérico) |
229
+
230
+ > Mesmo que o cliente envie valores em `id`, `name`, `active`, `entity`, `event`, `item`, `script`, eles são **substituídos** dentro de `LastMileManager.insert()`. Só `cron` (e demais campos de agendamento) são respeitados.
231
+
232
+ ### 3.3 `Bonus` — documento raiz (coleção `bonus`)
233
+
234
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
235
+ |-------|------|--------|-------------|-----------|
236
+ | `_id` | String | `Guid.newShortGuid()` se vazio | — | Id curto |
237
+ | `title` | String | — | recomendado | Título do bônus |
238
+ | `description` | String | — | não | Descrição |
239
+ | `tags` | `List<String>` | `[]` | não | Filtrável em `findAll` (`tags: {$all: […]}`) |
240
+ | `rules` | `List<BonusRule>` | `[]` | sim | Metas que compõem o progresso |
241
+ | `rewards` | `List<BonusReward>` | `[]` | sim | Faixas de recompensa por percentual |
242
+ | `principals` | `List<String>` | `[]` | não | Ids de player/team para os quais o bônus existe (com expansão de times) |
243
+ | `extra` | `Map` | `{}` | não | Campos adicionais livres |
244
+ | `to` | String | — | não | Filtro Mongo **raw** adicional sobre `player` |
245
+
246
+ **Campo removido/legado:** `scheduler` existe apenas **comentado** no código-fonte do `Bonus` — **o bônus não possui campo de agendamento ativo**. Agendamento só existe no `LastMile` (ver §7).
247
+
248
+ ### 3.4 `BonusRule` — subentidade (meta)
249
+
250
+ | Campo | Tipo | Descrição |
251
+ |-------|------|-----------|
252
+ | `id` | String | Identificador da regra (para correlacionar no progress) |
253
+ | `goal` | String | Meta: número, variável Mustache (`{{player.extra.meta}}`) ou fórmula — avaliada por `exp4j` |
254
+ | `operation` | `Operation` | Como calcular o resultado real do jogador |
255
+ | `period` | `Period` | Janela de tempo do cálculo |
256
+
257
+ **`Operation.type`:** `1` COUNT_ACTIONS · `2` SUM_ATTRIBUTE · `3` SUM_ACHIEVEMENTS · `4` AVG_ATTRIBUTE · `10` PREPARED_KPI. Campos: `achievement_type` (`-1` = NONE), `item`, `filters[{param, operator, value}]`, `sort`, `sub`.
258
+
259
+ **`Period.type`:** `0` `TYPE_VARIABLE` (usa `timeAmount` + `timeScale`, ex.: última semana) · `1` `TYPE_FIXED` (usa `startDate`/`endDate`). Alternativamente `expression` (string `"inicio;fim"` com palavras-chave de data) tem precedência quando preenchida.
260
+
261
+ ### 3.5 `BonusReward` — subentidade (faixa de recompensa)
262
+
263
+ | Campo | Tipo | Descrição |
264
+ |-------|------|-----------|
265
+ | `title` | String | Título da faixa (ex.: "Faixa 100%") |
266
+ | `start` | double | Percentual inicial da faixa (inclusivo) |
267
+ | `ends` | double | Percentual final da faixa (exclusivo) |
268
+ | `type` | int | Tipo de achievement gerado (`Achievement.TYPE_*`, ex.: `0` = ponto) |
269
+ | `item` | String | Item do achievement (ex.: `reais`) |
270
+ | `total` | String | Valor: número, variável Mustache ou fórmula — avaliado por `exp4j`; convertido para `long` |
271
+
272
+ ### 3.6 `Progress` — objeto computado (não é coleção própria)
273
+
274
+ Objeto de transporte/medição. Usado (a) no resultado de `calculate`, (b) como classe-alvo da agregação no modo custom. Seus principais valores são **calculados via getters**, não persistidos:
275
+
276
+ | Getter | Lógica |
277
+ |--------|--------|
278
+ | `getGoal()` | Sem `micro` → `goal`; 1 micro → goal do micro; N micros → **soma** |
279
+ | `getDone()` | Idem, **soma** dos micros |
280
+ | `getGap()` | `goal > done ? goal-done : 0`; N micros → **soma** dos gaps |
281
+ | `getPercentCompleted()` | Sem micro → `(done/goal)*100`; N micros → **média aritmética** dos percentuais |
282
+ | `getStart()` / `getEnd()` | Menor data entre os micros |
283
+ | `getTime()` | Mapa `{ unit:"DAYS", spent, total }` (dias decorridos vs total) |
284
+
285
+ Campos persistíveis/serializados: `_id`, `type`, `item`, `player`, `goal`, `done`, `operation`, `start`, `now`, `end`, `extra`, `micro[]`.
286
+ `moveGoalToSpecificPercentualPosition(percent, propagate)` recalcula `goal = percent*(goal/100)` e propaga aos micros.
287
+
288
+ ### 3.7 `LastMileLog` — objeto de transporte (não é coleção própria)
289
+
290
+ Gerado em memória dentro de `calculate`; vira notificação ou item de `lastmiles[]`. **Não há coleção `last_mile_log`.**
291
+
292
+ | Campo | Tipo | Descrição |
293
+ |-------|------|-----------|
294
+ | `_id` | String | (não setado no fluxo atual) |
295
+ | `player` | String | Jogador alvo (preenchido só no caminho legado comentado) |
296
+ | `message` | String | Mensagem já interpolada |
297
+ | `priority` | double | Copiado de `lastmile.priority` |
298
+ | `image` | String | Copiado de `lastmile.image` |
299
+
300
+ ### 3.8 Técnicas de jogo (`techniques`)
301
+
302
+ | Código | Significado | Atribuição |
303
+ |--------|-------------|------------|
304
+ | `GT53` | Last Mile | `GameTechniqueManager.autoConfigureMissingTechniqueFields()` adiciona `GT53` a todo `last_mile` cujo `techniques` esteja ausente ou vazio (`$size:0`) e salva o campo de técnica `(_id="last_mile", field="techniques", title="title")` |
305
+
306
+ > **`Bonus` não recebe código GT** — não está no auto-configure de técnicas. O bônus não é catalogado como técnica de jogo independente.
307
+
308
+ ---
309
+
310
+ ## 4. Endpoints
311
+
312
+ Autenticação: **OAuth 2.0 (`client_credentials`) / Bearer token** em todos os endpoints (conforme anotações apiDoc). Multi-tenant resolvido por `apiKey` (`FrontController.getInstance(apiKey)`).
313
+
314
+ ### 4.1 LastMile (`/v3/lastmile`)
315
+
316
+ **`GET /v3/lastmile/{id}`** — `LastMileManager.find(id)`. Retorna o documento (JSON com nulls).
317
+
318
+ **`GET /v3/lastmile`** — `findAll()`. Retorna **todos** os documentos `last_mile` (sem filtros de query).
319
+
320
+ **`POST /v3/lastmile`** — `insert(lastmile)`.
321
+
322
+ | Aspecto | Detalhe |
323
+ |---------|---------|
324
+ | Finalidade | Criar **ou substituir** uma configuração de lastmile |
325
+ | Full replace ou patch | **Full replace** (`MongoCollection.save`) — enviar `_id` existente sobrescreve o documento inteiro. **Não há PATCH** |
326
+ | Efeitos colaterais | (Re)cria scheduler `last_mile/notify`; remove scheduler antigo do item; invalida cache Groovy local |
327
+ | Resposta | `201 Created` com o objeto (`toJsonRemoveNullFields`) |
328
+
329
+ **`DELETE /v3/lastmile/{id}`** — `delete(id)`. Remove o documento, remove **todos** os schedulers do item (`deleteAllByEntityItem`) e limpa o cache Groovy local. Resposta `200`.
330
+
331
+ ### 4.2 Bonus (`/v3/bonus`)
332
+
333
+ **`GET /v3/bonus/{id}`** — `find(id)`.
334
+
335
+ **`GET /v3/bonus`** — `findAll(...)` via pipeline de agregação. Query params:
336
+
337
+ | Param | Tipo | Descrição |
338
+ |-------|------|-----------|
339
+ | `id` | String | Filtra por `_id` |
340
+ | `title` | String | Regex case-insensitive sobre `title` |
341
+ | `tags` | String | CSV → `tags: {$all: [...]}` |
342
+ | `to` | String | Player/Team id (ou `me`) — retorna bônus visíveis ao principal (campo `principals`) |
343
+ | `q` | String | Critério Mongo **raw** adicional (ex.: `extra.field:"x"`) |
344
+ | `fields` | String | CSV → projeção `$project` |
345
+ | `orderby` | String | Campo de ordenação (`_id`, `title`, `tags`) |
346
+ | `reverse` | boolean | `true` → ordem descendente |
347
+ | `max_results` | int | Limite (default **100** se ≤ 0) |
348
+
349
+ **`POST /v3/bonus`** — `insert(bonus)`. **Full replace** (`save`). Gera `_id` se vazio. `201`.
350
+
351
+ **`DELETE /v3/bonus/{id}`** — `delete(id)`. Remove o bônus e (defensivamente) schedulers do item. `200`.
352
+
353
+ **`GET /v3/bonus/{id}/calculate`** — `calculate(id, player, time, simulate)`. Preview do progresso/recompensas de **um jogador**. Params: `player` (ou `me`), `time` (referência temporal), `simulate` (substitui o resultado real — ver §5). **Não persiste, não notifica.**
354
+
355
+ **`GET /v3/bonus/{id}/calculateToAllPlayers`** — itera `findPlayersToBonus(id)` e calcula para todos. **Sem anotação apiDoc** (endpoint operacional). **Não persiste, não notifica.**
356
+
357
+ **`PUT /v3/bonus/calculateAll`** — corpo = lista de ids de bônus; calcula todos para um `player`; devolve `result`, `total` (somatório de achievements por item), `size`, `millis`.
358
+
359
+ **`PUT /v3/bonus/simulateAll`** — corpo = mapa `{ bonusId: valorSimulado }`; simula cada bônus com o valor informado.
360
+
361
+ ---
362
+
363
+ ## 5. Regras de Negócio
364
+
365
+ Regras presentes no código que não aparecem no schema:
366
+
367
+ - **Faixa semi-aberta `[start, ends)`** — a comparação é sempre `percent >= start && percent < ends`. Faixas adjacentes não devem se sobrepor; `ends` é exclusivo.
368
+ - **Targeting assimétrico entre os dois motores:**
369
+ - `Bonus.findPlayersToBonus` — se `principals` vazio **e** `to` nulo → **todos os jogadores**; senão filtra por `principals` (com expansão de `teams`) e/ou `to`.
370
+ - `LastMile.findPlayersToLastMile` — se `to` vazio/null → **lista vazia** (o fallback "todos" está **comentado**). Modelos de alvo diferentes no mesmo pacote.
371
+ - **Recompensa só quando jogador está abaixo da faixa** — a mensagem lastmile é gerada apenas no ramo `percent < reward.start`. Se o jogador já está dentro da faixa, ele recebe o achievement (em preview), não a mensagem de incentivo.
372
+ - **Faixa ultrapassada é ignorada** — rewards com `percent >= ends` não geram nada.
373
+ - **Single-rule colapsa o progresso** — se o bônus tem exatamente 1 regra, `pro` vira o próprio micro-progresso; com N regras, percentual é a **média** dos micros (não ponderada).
374
+ - **Modo `simulate`** — quando `simulate` é informado em `calculate`: as `rules` são esvaziadas (`bonus.rules = []`), `pro.done = parseDouble(simulate)` e `pro.goal` = soma dos goals das regras avaliados. **Não há medição real por regra** — o `progress` devolvido reflete o valor simulado, não o resultado do jogador.
375
+ - **`calculate` é preview** — não persiste achievements; premiação real ocorre fora deste fluxo.
376
+ - **Limite diário de execuções de scheduler por tenant** — `StatisticManager.newSchedulerExecution`; ao exceder, a execução é abortada com exceção registrada.
377
+ - **Multi-tenant** — toda operação é resolvida por `apiKey`; coleções e conexões Jongo são isoladas por tenant.
378
+
379
+ ---
380
+
381
+ ## 6. Comportamentos Automáticos
382
+
383
+ | Comportamento | Trigger | Impacto | Persistência |
384
+ |---------------|---------|---------|--------------|
385
+ | Geração de `_id` curto | `insert` (LastMile/Bonus) sem id | `_id = Guid.newShortGuid()` | Persistido |
386
+ | (Re)criação de scheduler | `LastMile.insert` com `scheduler != null` | Cria `SchedulerConfig` `last_mile/notify`, `active=true` | Persistido (coleção `scheduler`) |
387
+ | Remoção de scheduler antigo | `LastMile.insert` e `delete` | `deleteAll*` schedulers do item | Persistido |
388
+ | Invalidação do cache Groovy | `LastMile.insert`/`delete` | `compiled.remove(id)` | Runtime — **somente nó local** |
389
+ | Atribuição de técnica `GT53` | `autoConfigureMissingTechniqueFields()` | `techniques = ["GT53"]` se vazio | Persistido |
390
+ | Compilação/execução de script | `generateMessage` com `script != ""` | Compila Groovy seguro, cacheia, executa `prepare()` | Cache runtime (5s timeout) |
391
+ | Notificação de incentivo | callback cron (TYPE_BONUS/CUSTOM) | `NotificationManager.send` (EVENT_WIN/TEXT/PRIVATE) | Persistido (coleção `notification`) |
392
+ | Desativação de scheduler órfão | `runEntity` sem callback p/ a entity | `scheduler.active = false` | Persistido |
393
+
394
+ ### Encadeamento de behaviors no `insert`
395
+
396
+ ```mermaid
397
+ flowchart LR
398
+ I[insert lastmile] --> A[gera _id]
399
+ A --> B[deleteAllByEntityItemEvent\nlast_mile/notify]
400
+ B --> C{scheduler != null}
401
+ C -- sim --> D[clona + força campos\nentity/event/item/active/timezone]
402
+ D --> E[SchedulerManager.add]
403
+ C -- não --> F[skip scheduler]
404
+ E --> G[save last_mile]
405
+ F --> G
406
+ G --> H[compiled.remove id\ninvalida cache local]
407
+ ```
408
+
409
+ ---
410
+
411
+ ## 7. Suportado vs NÃO Suportado
412
+
413
+ ### ✅ Suportado
414
+
415
+ - CRUD de `LastMile` (`/v3/lastmile`) e `Bonus` (`/v3/bonus`).
416
+ - Geração de mensagens por **cron** para `type = TYPE_BONUS (8)` e `type = TYPE_CUSTOM (99)`.
417
+ - Cálculo/simulação de bônus (preview) por jogador, em lote e para todos os jogadores.
418
+ - Templates Mustache com variáveis `player`, `global`, `progress`, `reward`, `reward_total` + variáveis criadas via script Groovy.
419
+ - Script Groovy sandboxed (`prepare(lastmile, params)`) com timeout de 5s.
420
+ - Targeting por filtro Mongo (`to`) no LastMile; por `principals`/`to` no Bonus.
421
+
422
+ ### ❌ NÃO Suportado / Comportamento silencioso
423
+
424
+ - **`Bonus.scheduler` não existe** — o campo está **comentado** na entidade e no `BonusManager.insert`. Bônus **não se auto-agenda**; o agendamento vive apenas no `LastMile`.
425
+ - **`BonusManager` não é `SchedulerCallback`** — `schedulerCallback` e o registro em `getSchedulerCallback` estão **comentados**. Um `SchedulerConfig` com `entity="bonus"` **não tem callback** → o runner o **desativa** (`active=false`).
426
+ - **Tipos diferentes de `8`/`99`** no callback de LastMile → **nada acontece** (sem erro, sem log de negação).
427
+ - **LastMile com `scheduler` mas `to` vazio** → `findPlayersToLastMile` devolve `[]` → cron roda e atinge **zero jogadores**, silenciosamente.
428
+ - **`calculate` não premia** — achievements montados não são persistidos (preview-only).
429
+ - **Caminho inline não notifica** — endpoints de cálculo de bônus só devolvem `lastmiles[]` no JSON; nenhuma notificação é enviada.
430
+ - **Sem PATCH / atualização parcial** — POST com `_id` existente é **full replace** (`save`).
431
+ - **`techniques` ignorado na lógica** — aceito e auto-preenchido, mas não altera comportamento.
432
+ - **Campos desconhecidos descartados** — `@JsonIgnoreProperties(ignoreUnknown=true)` em todas as entidades.
433
+ - **Código legado comentado** (não executado): `BonusManager.calculateOLD`, `BonusManager.schedulerCallback`/`generateLastmileDriveNotificationsForAllPlayers`, `LastMileManager.executeScript`, e o fallback "todos os jogadores" em `findPlayersToLastMile`.
434
+
435
+ ---
436
+
437
+ ## 8. Segurança e Permissões
438
+
439
+ - **Autenticação:** OAuth 2.0 `client_credentials` / Bearer token (`AuthBean`); isolamento multi-tenant por `apiKey`.
440
+ - **Sandbox Groovy** (em `generateMessage`/`runScript`):
441
+ - `SecureASTCustomizer` + `TriggerExpressionChecker` + **whitelist de tokens** (apenas operadores aritméticos/lógicos/comparação e colchetes).
442
+ - Anotação `@TimedInterrupt(5s)` **e** `future.get(5, SECONDS)` em executor single-thread → dupla barreira de timeout; ao estourar, a execução é cancelada e a exceção registrada.
443
+ - **Superfícies de injeção (NoSQL) confirmadas no código:**
444
+ - `LastMile.to` e `Bonus.to`/`q` são **concatenados crus** na string de query Mongo (`query.append(", " + to)`), sem sanitização. Permite operadores Mongo arbitrários — superfície de injeção. Restrito a usuários autenticados/admin do tenant, mas não validado.
445
+ - **Modo custom**: `LastMile.aggregate` é um **pipeline JSON arbitrário** executado via `collection(lastmile.entity).aggregate(...)` na coleção informada pelo próprio documento → permite ler/agregar **qualquer coleção do tenant**.
446
+ - **Cache de scripts por nó:** `compiled` é um `HashMap` de instância do `LastMileManager`. A invalidação (`compiled.remove(id)`) ocorre **apenas no nó que processou o save** — em deploy clusterizado, outros nós podem continuar usando o script antigo até recompilar.
447
+
448
+ ---
449
+
450
+ ## 9. Observabilidade e Troubleshooting
451
+
452
+ ### Diagnóstico
453
+
454
+ - **Log de execução do scheduler:** cada `SchedulerRunner.run` grava um `SchedulerLog` (coleção `scheduler_log`) com `outputs`, `exceptions` e `millis`.
455
+ - **Output de negócio:** o callback adiciona `"lastmile generated for N players"` aos outputs — N=0 indica alvo vazio.
456
+ - **Notificações geradas:** verificáveis na coleção `notification` (`type=0` texto, `scope=0` privado).
457
+
458
+ ### Comandos úteis
459
+
460
+ ```
461
+ GET /v3/lastmile/{id} # configuração do lastmile
462
+ GET /v3/lastmile # todos os lastmiles
463
+ GET /v3/bonus/{id} # configuração do bônus
464
+ GET /v3/bonus/{id}/calculate?player=me # preview do progresso (sem notificar)
465
+ GET /v3/bonus/{id}/calculate?player=me&simulate=80 # preview forçando done=80
466
+ ```
467
+
468
+ Consultas Mongo de investigação:
469
+
470
+ ```js
471
+ db.last_mile.find({ item: "bonus_seguro_vida" })
472
+ db.bonus.find({ _id: "bonus_seguro_vida" })
473
+ db.scheduler.find({ entity: "last_mile", event: "notify", item: "<lastmileId>" })
474
+ db.scheduler_log.find().sort({ _id: -1 }).limit(5)
475
+ db.notification.find({ "player._id": "<playerId>" }).sort({ time: -1 })
476
+ ```
477
+
478
+ ### Erros comuns e causas
479
+
480
+ | Sintoma | Causa provável |
481
+ |---------|----------------|
482
+ | Nenhuma notificação enviada | `to` vazio (0 jogadores); `type` ≠ 8/99; faixa `[start,ends)` não casa com `percent`; scheduler inativo ou limite diário excedido |
483
+ | `{ "error": "..." }` no `calculate` | Exceção engolida: `goal`/`total` inválido para `exp4j`, regra sem `operation`/`period`, `bonus` inexistente |
484
+ | `SchedulerCallback does not exist for entity X` + scheduler desativado | Scheduler criado com `entity` sem callback (ex.: `bonus`) |
485
+ | Mensagem sem variáveis interpoladas | Variável Mustache ausente em `params`, ou script não preencheu o `params` esperado |
486
+ | Script "não roda" após edição em cluster | Cache `compiled` não invalidado no nó atual (invalidação é local) |
487
+
488
+ ---
489
+
490
+ ## 10. Exemplos Práticos
491
+
492
+ ### 10.1 Mínimo funcional (LastMile sobre bônus)
493
+
494
+ ```json
495
+ POST /v3/lastmile
496
+ {
497
+ "title": "Incentivo Seguro de Vida 95%",
498
+ "type": 8,
499
+ "item": "bonus_seguro_vida",
500
+ "start": 90,
501
+ "ends": 100,
502
+ "to": "active:true",
503
+ "message": "{{player.name}}, faltam {{progress.gap}} vendas para você bater a meta!"
504
+ }
505
+ ```
506
+
507
+ > Sem `scheduler`, a mensagem só é produzida sob demanda via `GET /v3/bonus/bonus_seguro_vida/calculate` (não notifica). Para envio automático, adicione `scheduler`.
508
+
509
+ ### 10.2 Avançado (cron + script + alvo segmentado)
510
+
511
+ ```json
512
+ POST /v3/lastmile
513
+ {
514
+ "title": "Coach Seguro Vida",
515
+ "type": 8,
516
+ "item": "bonus_seguro_vida",
517
+ "start": 80,
518
+ "ends": 100,
519
+ "priority": 10,
520
+ "to": "extra.role:'asven',active:true",
521
+ "image": "https://cdn/coach.png",
522
+ "message": "{{player.name}}, com mais {{gap}} vendas você alcança a {{reward.title}} e ganha {{reais}} — você tem {{days}} dias (até {{ends}}).",
523
+ "script": "void prepare(lastmile, params){\n int gap = new Double(params.progress.gap).intValue();\n String reais = FormatterUtil.numberCurrency(params.reward_total, \"BRL\", \"BR\", \"¤ #,##0.00\");\n int days = new Double((params.progress.end.getTime() - new Date().getTime())/(1000*60*60*24)).intValue();\n params.put(\"gap\", gap);\n params.put(\"reais\", reais);\n params.put(\"days\", days);\n params.put(\"ends\", DateUtil.format(params.progress.end, \"dd/MM/yyyy\"));\n}",
524
+ "scheduler": { "cron": "0 0 9 * * ?" }
525
+ }
526
+ ```
527
+
528
+ ### 10.3 Bônus completo (motor que alimenta o LastMile)
529
+
530
+ ```json
531
+ POST /v3/bonus
532
+ {
533
+ "_id": "bonus_seguro_vida",
534
+ "title": "Bônus Seguro Vida",
535
+ "tags": ["vendas"],
536
+ "rules": [
537
+ {
538
+ "goal": "{{player.extra.meta_mes}}",
539
+ "operation": { "type": 1, "achievement_type": -1, "item": "vender",
540
+ "filters": [ { "param": "produto", "operator": 1, "value": "seg_vida" } ],
541
+ "sort": 0, "sub": false },
542
+ "period": { "type": 0, "timeAmount": 1, "timeScale": 7 }
543
+ }
544
+ ],
545
+ "rewards": [
546
+ { "title": "Faixa 100%", "start": 100, "ends": 110, "type": 0, "item": "reais", "total": "0.025 * {{global.teto_rv}}" },
547
+ { "title": "Faixa 110%", "start": 110, "ends": 1000, "type": 0, "item": "reais", "total": "0.03 * {{global.teto_rv}}" }
548
+ ],
549
+ "principals": []
550
+ }
551
+ ```
552
+
553
+ ### 10.4 Anti-pattern (o que NÃO fazer)
554
+
555
+ ```json
556
+ // ❌ LastMile com scheduler mas SEM `to`: o cron dispara e atinge ZERO jogadores
557
+ {
558
+ "title": "Nunca chega a ninguém",
559
+ "type": 8,
560
+ "item": "bonus_seguro_vida",
561
+ "start": 90, "ends": 100,
562
+ "message": "...",
563
+ "scheduler": { "cron": "0 0 9 * * ?" }
564
+ }
565
+ ```
566
+
567
+ Outros anti-patterns:
568
+ - **Faixas sobrepostas** entre rewards/lastmiles (`[80,100)` e `[90,110)`) → comportamento ambíguo; mantenha faixas disjuntas e `ends` exclusivo.
569
+ - **Esperar que `calculate` premie** — ele é preview; não cria achievements persistidos.
570
+ - **Criar scheduler com `entity:"bonus"`** — não há callback; será desativado.
571
+ - **`type` ≠ 8/99 esperando notificação** — o callback ignora silenciosamente.
572
+
573
+ ---
574
+
575
+ ## Checklist de Configuração
576
+
577
+ - [ ] O `bonus` referenciado em `LastMile.item` existe **antes** de criar o LastMile (`type=8`).
578
+ - [ ] `LastMile.to` está preenchido — sem ele, o cron atinge **0 jogadores** (armadilha silenciosa).
579
+ - [ ] `start < ends` e a faixa do LastMile é coerente com as faixas (`reward.start`) do bônus.
580
+ - [ ] As variáveis usadas no `message` existem em `params` (`player`, `global`, `progress`, `reward`) ou são criadas no `script`.
581
+ - [ ] Para `type=99` (custom): `entity` e `aggregate` preenchidos; o `aggregate` usa o token `FUNIFIER_PLAYER_IDS` e devolve documentos no formato `Progress`.
582
+ - [ ] `scheduler.cron` válido — sem `scheduler`, não há disparo automático (apenas cálculo on-demand pelo `/v3/bonus`).
583
+ - [ ] Não criar scheduler com `entity:"bonus"` (sem callback → desativado).
584
+ - [ ] Em cluster: após editar `script`, lembrar que a invalidação de cache é por nó.