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.
- package/.cursor/rules/funifier.mdc +38 -41
- package/.github/copilot-instructions.md +38 -41
- package/AGENTS.md +56 -49
- package/README.md +40 -22
- package/datasource-funifier-docs/.coverage.json +326 -0
- package/datasource-funifier-docs/.validation.json +593 -0
- package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
- package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
- package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
- package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
- package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
- package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
- package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
- package/datasource-funifier-docs/knowledge/index.md +4 -1
- package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
- package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
- package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
- package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
- package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
- package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
- package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
- package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
- package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
- package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
- package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
- package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
- package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
- package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
- package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
- package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
- package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
- package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
- package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
- package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
- package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
- package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
- package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
- package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
- package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
- package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
- package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
- package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
- package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
- package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
- package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
- package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
- package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
- package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
- package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
- package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
- package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
- package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
- package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
- package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
- package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +42 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/init.test.js +74 -3
- package/dist/cli/init.test.js.map +1 -1
- package/dist/cli/persona.d.ts +3 -0
- package/dist/cli/persona.d.ts.map +1 -0
- package/dist/cli/persona.js +25 -0
- package/dist/cli/persona.js.map +1 -0
- package/dist/mcp/bundle.js +119 -93
- package/dist/mcp/check-update.d.ts +5 -0
- package/dist/mcp/check-update.d.ts.map +1 -1
- package/dist/mcp/check-update.js +21 -10
- package/dist/mcp/check-update.js.map +1 -1
- package/dist/mcp/check-update.test.d.ts +2 -0
- package/dist/mcp/check-update.test.d.ts.map +1 -0
- package/dist/mcp/check-update.test.js +33 -0
- package/dist/mcp/check-update.test.js.map +1 -0
- package/dist/mcp/index.js +2 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/prompts/templates.d.ts.map +1 -1
- package/dist/mcp/prompts/templates.js +35 -0
- package/dist/mcp/prompts/templates.js.map +1 -1
- package/dist/mcp/resources/documentation.d.ts +1 -1
- package/dist/mcp/resources/documentation.d.ts.map +1 -1
- package/dist/mcp/resources/documentation.js +39 -3
- package/dist/mcp/resources/documentation.js.map +1 -1
- package/dist/mcp/tools/connect.d.ts.map +1 -1
- package/dist/mcp/tools/connect.js +18 -8
- package/dist/mcp/tools/connect.js.map +1 -1
- package/dist/mcp/tools/database.d.ts.map +1 -1
- package/dist/mcp/tools/database.js +59 -47
- package/dist/mcp/tools/database.js.map +1 -1
- package/dist/mcp/tools/database.test.js +2 -2
- package/dist/mcp/tools/database.test.js.map +1 -1
- package/dist/mcp/tools/delete.d.ts.map +1 -1
- package/dist/mcp/tools/delete.js +13 -3
- package/dist/mcp/tools/delete.js.map +1 -1
- package/dist/mcp/tools/execute.d.ts.map +1 -1
- package/dist/mcp/tools/execute.js +20 -9
- package/dist/mcp/tools/execute.js.map +1 -1
- package/dist/mcp/tools/folder.d.ts.map +1 -1
- package/dist/mcp/tools/folder.js +22 -12
- package/dist/mcp/tools/folder.js.map +1 -1
- package/dist/mcp/tools/get.d.ts.map +1 -1
- package/dist/mcp/tools/get.js +16 -6
- package/dist/mcp/tools/get.js.map +1 -1
- package/dist/mcp/tools/index.d.ts +1 -1
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +28 -1
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/list.d.ts.map +1 -1
- package/dist/mcp/tools/list.js +38 -14
- package/dist/mcp/tools/list.js.map +1 -1
- package/dist/mcp/tools/logs.d.ts.map +1 -1
- package/dist/mcp/tools/logs.js +15 -5
- package/dist/mcp/tools/logs.js.map +1 -1
- package/dist/mcp/tools/save.d.ts.map +1 -1
- package/dist/mcp/tools/save.js +14 -4
- package/dist/mcp/tools/save.js.map +1 -1
- package/dist/mcp/tools/save.test.js +3 -3
- package/dist/mcp/tools/save.test.js.map +1 -1
- package/dist/mcp/tools/search-docs.d.ts +3 -0
- package/dist/mcp/tools/search-docs.d.ts.map +1 -0
- package/dist/mcp/tools/search-docs.js +102 -0
- package/dist/mcp/tools/search-docs.js.map +1 -0
- package/package.json +6 -2
- package/skills/acquire-funifier-knowledge/SKILL.md +155 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +86 -0
- package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
- package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
- package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
- package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
- package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
- package/skills/funifier/SKILL.md +88 -0
- package/skills/funifier/references/configure-security.md +96 -0
- package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
- package/skills/funifier/references/create-aggregate.md +144 -0
- package/skills/funifier/references/create-challenge.md +116 -0
- package/skills/funifier/references/create-competition.md +98 -0
- package/skills/funifier/references/create-crossword.md +574 -0
- package/skills/funifier/references/create-custom-object.md +91 -0
- package/skills/funifier/references/create-custom-page.md +135 -0
- package/skills/funifier/references/create-folder.md +104 -0
- package/skills/funifier/references/create-lastmile.md +643 -0
- package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
- package/skills/funifier/references/create-level.md +94 -0
- package/skills/funifier/references/create-lottery.md +913 -0
- package/skills/funifier/references/create-mystery.md +769 -0
- package/skills/funifier/references/create-notification.md +75 -0
- package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
- package/skills/funifier/references/create-quiz.md +98 -0
- package/skills/funifier/references/create-scheduler.md +141 -0
- package/skills/funifier/references/create-story.md +636 -0
- package/skills/funifier/references/create-swap.md +95 -0
- package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
- package/skills/funifier/references/create-virtual-good.md +96 -0
- package/skills/funifier/references/create-webhook.md +72 -0
- package/skills/funifier/references/create-websocket.md +71 -0
- package/skills/funifier/references/create-widget.md +76 -0
- package/skills/funifier/references/debug.md +87 -0
- package/skills/funifier/references/help.md +81 -0
- package/skills/funifier/references/implement-frontend.md +106 -0
- package/skills/funifier/references/import-csv.md +75 -0
- package/skills/funifier/references/manage-player.md +82 -0
- package/skills/funifier/references/manage-team.md +76 -0
- package/skills/funifier/references/upload-file.md +91 -0
- package/skills/funifier-create-aggregate/SKILL.md +0 -127
- package/skills/funifier-create-challenge/SKILL.md +0 -88
- package/skills/funifier-create-custom-page/SKILL.md +0 -127
- package/skills/funifier-create-level/SKILL.md +0 -87
- package/skills/funifier-create-quiz/SKILL.md +0 -87
- package/skills/funifier-create-scheduler/SKILL.md +0 -127
- package/skills/funifier-create-virtual-good/SKILL.md +0 -87
- package/skills/funifier-debug/SKILL.md +0 -92
- package/skills/funifier-help/SKILL.md +0 -86
- package/skills/funifier-implement-frontend/SKILL.md +0 -90
- package/skills/funifier-index/SKILL.md +0 -58
|
@@ -1,45 +1,584 @@
|
|
|
1
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
11
|
+
---
|
|
9
12
|
|
|
10
|
-
##
|
|
13
|
+
## 1. Visão Geral
|
|
11
14
|
|
|
12
|
-
|
|
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
|
-
|
|
17
|
+
O pacote `lastmile` contém **dois motores acoplados**, persistidos em coleções distintas e expostos por REST distintos:
|
|
18
18
|
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
- [ ] Definir mensagem motivacional
|
|
25
|
-
- [ ] Vincular ao desafio ou meta específica
|
|
26
|
+
**Dependências de runtime (via `ManagerFactory`):**
|
|
26
27
|
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
**Método:** GET
|
|
31
|
-
**Endpoint:** `/v3/lastmile`
|
|
35
|
+
---
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
**Método:** POST
|
|
35
|
-
**Endpoint:** `/v3/lastmile`
|
|
37
|
+
## 2. Arquitetura e Fluxos
|
|
36
38
|
|
|
37
|
-
###
|
|
38
|
-
**Método:** DELETE
|
|
39
|
-
**Endpoint:** `/v3/lastmile/:id`
|
|
39
|
+
### 2.1 Dois caminhos de geração (divergência runtime importante)
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
Existem **dois caminhos distintos** que produzem mensagens lastmile, e eles têm efeitos colaterais diferentes:
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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ó.
|