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