funifier-mcp 0.2.25 → 0.2.27
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 +5 -2
- 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 +1011 -77
- 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/core/api-client.d.ts +21 -1
- package/dist/core/api-client.d.ts.map +1 -1
- package/dist/core/api-client.js +154 -1
- package/dist/core/api-client.js.map +1 -1
- package/dist/core/constants.d.ts +14 -0
- package/dist/core/constants.d.ts.map +1 -1
- package/dist/core/constants.js +14 -0
- package/dist/core/constants.js.map +1 -1
- package/dist/core/types/Folder.d.ts +16 -0
- package/dist/core/types/Folder.d.ts.map +1 -0
- package/dist/core/types/Folder.js +3 -0
- package/dist/core/types/Folder.js.map +1 -0
- package/dist/core/types/FolderContent.d.ts +10 -0
- package/dist/core/types/FolderContent.d.ts.map +1 -0
- package/dist/core/types/FolderContent.js +3 -0
- package/dist/core/types/FolderContent.js.map +1 -0
- package/dist/core/types/FolderContentType.d.ts +10 -0
- package/dist/core/types/FolderContentType.d.ts.map +1 -0
- package/dist/core/types/FolderContentType.js +3 -0
- package/dist/core/types/FolderContentType.js.map +1 -0
- package/dist/core/types/FolderLog.d.ts +11 -0
- package/dist/core/types/FolderLog.d.ts.map +1 -0
- package/dist/core/types/FolderLog.js +3 -0
- package/dist/core/types/FolderLog.js.map +1 -0
- package/dist/core/types/index.d.ts +4 -0
- package/dist/core/types/index.d.ts.map +1 -1
- package/dist/core/types/index.js +4 -0
- package/dist/core/types/index.js.map +1 -1
- package/dist/mcp/bundle.js +121 -87
- package/dist/mcp/check-update.d.ts +2 -0
- package/dist/mcp/check-update.d.ts.map +1 -0
- package/dist/mcp/check-update.js +44 -0
- package/dist/mcp/check-update.js.map +1 -0
- package/dist/mcp/index.js +5 -2
- package/dist/mcp/index.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/_char-guard.js +1 -1
- package/dist/mcp/tools/_char-guard.js.map +1 -1
- package/dist/mcp/tools/_fetch-current.d.ts +1 -1
- package/dist/mcp/tools/_fetch-current.d.ts.map +1 -1
- package/dist/mcp/tools/_fetch-current.js +12 -0
- package/dist/mcp/tools/_fetch-current.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 +33 -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 +4 -0
- package/dist/mcp/tools/folder.d.ts.map +1 -0
- package/dist/mcp/tools/folder.js +68 -0
- package/dist/mcp/tools/folder.js.map +1 -0
- 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 +5 -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 +26 -4
- package/dist/mcp/tools/save.js.map +1 -1
- package/dist/mcp/tools/save.test.js +192 -1
- 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 +132 -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 +47 -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/datasource-funifier-docs/.search-index.json +0 -17318
- package/datasource-funifier-docs/.skills-map.json +0 -73
- 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,99 +1,546 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `virtual-good` (Loja Virtual / Catálogo)
|
|
2
2
|
|
|
3
3
|
**Acesso Studio:** `/studio/catalog`
|
|
4
4
|
**API Endpoint:** `/v3/virtualgoods`
|
|
5
|
+
**Coleções MongoDB:** `catalog` (catálogos), `catalog_item` (itens) e `achievement` (registros de compra — `type=2`)
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
> Documentação de engenharia reversa baseada no código real do projeto `funifier-service`:
|
|
8
|
+
> `rest/v3/rest/VirtualGoodsRest.java`, `engine/catalog/CatalogManager.java`, `engine/catalog/CatalogDaoMongo.java`,
|
|
9
|
+
> `engine/catalog/Catalog.java`, `engine/catalog/CatalogItem.java`, `engine/catalog/PurchaseAsyncProcessor.java`,
|
|
10
|
+
> `engine/challenge/Requirement.java`, `engine/action/Limit.java`, `engine/technique/GameTechniqueManager.java`.
|
|
7
11
|
|
|
8
|
-
|
|
12
|
+
---
|
|
9
13
|
|
|
10
|
-
##
|
|
14
|
+
## 1. Visão Geral
|
|
11
15
|
|
|
12
|
-
-
|
|
13
|
-
- Para monetizar pontos/moedas virtuais
|
|
14
|
-
- Para dar senso de propriedade e escolha ao jogador
|
|
15
|
-
- Exemplos: camisetas, gift cards, itens virtuais, acessórios de avatar
|
|
16
|
+
O módulo `virtual-good` implementa a **loja de troca** da gamificação: cadastra **catálogos** (agrupadores) e **itens** que o jogador pode adquirir gastando pontos/itens e recebendo recompensas. Ele **não possui modelo de compra próprio** — toda compra é gravada como um `Achievement` do tipo `catalog_item` (`type=2`) na coleção `achievement`. Em consequência, "estoque", "limite por período", "histórico de compras" e o campo `owned` são todos **calculados sobre a coleção `achievement`**, não sobre uma coleção de pedidos.
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
Papel arquitetural:
|
|
18
19
|
|
|
19
|
-
- **
|
|
20
|
+
- **Catálogo** (`catalog`) é puramente organizacional — agrupa itens por `catalogId`.
|
|
21
|
+
- **Item** (`catalog_item`) carrega o preço (`requires`), as recompensas (`rewards`), as regras de disponibilidade (`active`, `start`/`end`, `limit`, `amount`, `principals`) e a integração com gatilhos/notificações.
|
|
22
|
+
- **Compra** reaproveita o motor de `Achievement`: debita os `requires`, credita os `rewards` e o próprio item, dispara triggers e notificações, e recalcula o status do jogador.
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
Relação com outros módulos:
|
|
22
25
|
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
26
|
+
- **Point / Challenge / Catalog Item** — únicos tipos efetivamente debitados/creditados na compra (ver §6).
|
|
27
|
+
- **Achievement / Player Status** — toda compra recalcula o status do jogador (`updatePlayerStatus`).
|
|
28
|
+
- **Trigger** — três eventos disparados na entidade `catalog_item` (§2.2).
|
|
29
|
+
- **Notification** — notificações `EVENT_WIN` configuradas no item.
|
|
30
|
+
- **Tango Card** — integração externa quando `item.extra.provider == "tango"`.
|
|
31
|
+
- **Game Technique** — itens recebem a técnica `GT08` (virtual good) via backfill (§3.4).
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
---
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
**Método:** GET
|
|
34
|
-
**Endpoint:** `/v3/virtualgoods/catalog`
|
|
35
|
+
## 2. Arquitetura e Fluxos
|
|
35
36
|
|
|
36
|
-
###
|
|
37
|
-
**Método:** POST
|
|
38
|
-
**Endpoint:** `/v3/virtualgoods/catalog`
|
|
37
|
+
### 2.1 Pipeline principal de compra — `CatalogManager.purchase(Achievement, boolean async)`
|
|
39
38
|
|
|
40
|
-
|
|
39
|
+
A compra é processada pelo método **`purchase(...)`**, declarado `synchronized` (serializa compras por tenant — ver §5). O endpoint monta um `Achievement(guid, player, total, TYPE_CATALOG_ITEM, item, time)` e chama o manager.
|
|
40
|
+
|
|
41
|
+
Sequência exata (linhas reais de `CatalogManager.purchase`):
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
[entrada] purchase.time = now se nulo
|
|
45
|
+
[lookup item] findOne catalog_item {_id: item} → se null: restrictions += "item_does_not_exist"
|
|
46
|
+
[lookup player] findById(player) → se null: findById team; se ambos null: "player_does_not_exist"
|
|
47
|
+
[trigger] triggerManager.execute(item, "catalog_item", "before_purchase_validation")
|
|
48
|
+
[validação] async=true → isValid(...) (rápida, para na 1ª falha) → se falha: "restrictions"
|
|
49
|
+
async=false → checkRestrictions(...) (lista completa de restrições)
|
|
50
|
+
[se restrictions vazio]:
|
|
51
|
+
[tango] se item.extra.provider == "tango" → tangoCardManager.purchase(item, player)
|
|
52
|
+
→ extra.tango_order_id = order.referenceOrderID
|
|
53
|
+
[trigger] triggerManager.execute(item, "catalog_item", "before_win")
|
|
54
|
+
[se restrictions ainda vazio]:
|
|
55
|
+
[débito] para cada requires r: se r.operation==DEDUCT && r.total!=0 && r.type∈{0,1,2}
|
|
56
|
+
→ Achievement(total negativo × purchase.total, r.type, r.item) com extra.origin=purchase.id → save
|
|
57
|
+
[crédito] para cada rewards r: se r.total!=0 && r.type∈{0,1,2}
|
|
58
|
+
→ Achievement(total positivo × purchase.total, r.type, r.item) com extra.origin=purchase.id
|
|
59
|
+
→ trigger before_win/after_win na coleção do tipo → save
|
|
60
|
+
[item] save do Achievement da própria compra (type=2, item=itemId)
|
|
61
|
+
[trigger] triggerManager.execute(item, "catalog_item", "after_win")
|
|
62
|
+
[status] async=true → playerManager.updateUserStatus(player)
|
|
63
|
+
async=false → achievementManager.updatePlayerStatus(player) (achievements somados ao retorno)
|
|
64
|
+
[notif] para cada NotificationDefinition EVENT_WIN do item → notificationManager.send(...)
|
|
65
|
+
[owned] item.owned = count achievement {type:2, item:id} → catalog_item.save(item)
|
|
66
|
+
[retorno] { restrictions, achievements, milliseconds, status: "OK"|"UNAUTHORIZED" }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Característica importante: **só os tipos `0` (point), `1` (challenge) e `2` (catalog_item)** são debitados/creditados. Tipos `3`–`7`/`50` em `requires`/`rewards` são **silenciosamente ignorados** na movimentação (ver §6 e §7).
|
|
70
|
+
|
|
71
|
+
```mermaid
|
|
72
|
+
flowchart TD
|
|
73
|
+
A["POST /purchase — monta Achievement type=2"] --> B{"item existe?"}
|
|
74
|
+
B -- não --> R0["restrictions += item_does_not_exist"]
|
|
75
|
+
B -- sim --> C{"player/team existe?"}
|
|
76
|
+
C -- não --> R1["restrictions += player_does_not_exist"]
|
|
77
|
+
C -- sim --> T0["trigger: before_purchase_validation"]
|
|
78
|
+
T0 --> V{"async?"}
|
|
79
|
+
V -- "sim (quick)" --> Vq["isValid() — para na 1ª falha"]
|
|
80
|
+
V -- "não (full)" --> Vf["checkRestrictions() — lista completa"]
|
|
81
|
+
Vq --> D{"restrictions vazio?"}
|
|
82
|
+
Vf --> D
|
|
83
|
+
D -- não --> OUT["status = UNAUTHORIZED"]
|
|
84
|
+
D -- sim --> TG{"extra.provider == tango?"}
|
|
85
|
+
TG -- sim --> TC["tangoCardManager.purchase → extra.tango_order_id"]
|
|
86
|
+
TG -- não --> T1
|
|
87
|
+
TC --> T1["trigger: before_win"]
|
|
88
|
+
T1 --> DB["débito dos requires (type 0,1,2 & operation=DEDUCT)"]
|
|
89
|
+
DB --> CR["crédito dos rewards (type 0,1,2)"]
|
|
90
|
+
CR --> SV["save Achievement da compra"]
|
|
91
|
+
SV --> T2["trigger: after_win"]
|
|
92
|
+
T2 --> ST["updatePlayerStatus / updateUserStatus"]
|
|
93
|
+
ST --> NT["notificações EVENT_WIN"]
|
|
94
|
+
NT --> OW["recalcula item.owned = count(achievement type=2)"]
|
|
95
|
+
OW --> OK["status = OK"]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 2.2 Síncrono vs Assíncrono — `VirtualGoodsRest.insertPurchase`
|
|
99
|
+
|
|
100
|
+
O endpoint decide o modo de execução nesta ordem:
|
|
101
|
+
|
|
102
|
+
1. Se o **query param `async`** não foi informado, lê o **default do item** (`CatalogItem.async`); se o item tiver `async=true`, a compra vira assíncrona.
|
|
103
|
+
2. Se `async == "true"` → enfileira via `catalogManager.asyncPurchase(_purchase)` e retorna **imediatamente** `status: "ASYNC"`.
|
|
104
|
+
3. Caso contrário (default) → executa `catalogManager.purchase(_purchase, false)` e retorna o resultado processado.
|
|
105
|
+
|
|
106
|
+
```mermaid
|
|
107
|
+
sequenceDiagram
|
|
108
|
+
participant Cli as Cliente
|
|
109
|
+
participant Rest as VirtualGoodsRest
|
|
110
|
+
participant Mgr as CatalogManager
|
|
111
|
+
participant Q as PurchaseAsyncProcessor (thread/tenant)
|
|
112
|
+
Cli->>Rest: POST /purchase {item, player, total}
|
|
113
|
+
Rest->>Rest: async ausente? → lê CatalogItem.async
|
|
114
|
+
alt async == true
|
|
115
|
+
Rest->>Mgr: asyncPurchase(achievement)
|
|
116
|
+
Mgr->>Q: queue.offer(achievement) (fila em memória)
|
|
117
|
+
Rest-->>Cli: 200 { status: "ASYNC", achievements:[...] }
|
|
118
|
+
Q->>Mgr: purchase(achievement, true) (processa depois)
|
|
119
|
+
else síncrono (default)
|
|
120
|
+
Rest->>Mgr: purchase(achievement, false)
|
|
121
|
+
Mgr-->>Rest: { restrictions, achievements, status }
|
|
122
|
+
Rest-->>Cli: 200 { resultado processado }
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
> A fila assíncrona (`PurchaseAsyncProcessor`) é um `LinkedBlockingQueue` **em memória**, com **uma thread por tenant** iniciada no construtor do `CatalogManager`. Compras enfileiradas e ainda não processadas são **perdidas em caso de restart** (não há durabilidade). Ver §7.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 3. Estrutura dos Objetos
|
|
131
|
+
|
|
132
|
+
### 3.1 `Catalog` — documento raiz (coleção `catalog`)
|
|
133
|
+
|
|
134
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
135
|
+
| --------- | ----------------------------- | ------------- | ----------- | --------- |
|
|
136
|
+
| `_id` | String | auto (`Guid.shortTimeMillis()`) | — | Gerado se ausente no `add`. |
|
|
137
|
+
| `catalog` | String | — | Não (lógico) | Nome de exibição do catálogo. |
|
|
138
|
+
| `itens` | CatalogItem[] | — | Não | Ver comportamento híbrido abaixo. |
|
|
139
|
+
| `extra` | Map<String,Object> | `{}` | Não | Atributos customizados. |
|
|
140
|
+
| `created` | Date | `now` no `add`| — | Setado automaticamente se ausente. |
|
|
141
|
+
| `image` | Image | — | Não | Imagens (`small`/`medium`/`original`). |
|
|
142
|
+
| `i18n` | Map<String,Map<String,String>>| `{}` | Não | Internacionalização. |
|
|
143
|
+
|
|
144
|
+
**Comportamento do campo `itens`:**
|
|
145
|
+
- No `POST /catalog`, se `itens` vier preenchido, cada item é também persistido na coleção `catalog_item` (via `addItem`), **além** de o objeto `catalog` ser salvo inteiro (com o array embutido) por `c.save(catalog)`.
|
|
146
|
+
- No `GET /catalog/{id}` (`findById`), `itens` é **recalculado** a partir da coleção `catalog_item` (`findAllItemsByCatalogId`), sobrescrevendo o que estiver embutido.
|
|
147
|
+
- No `GET /catalog` (lista, via `aggregate`), `itens` retorna **a cópia embutida** no documento `catalog` (não recalculada) — pode divergir de `findById`. Ver §7.
|
|
148
|
+
|
|
149
|
+
### 3.2 `CatalogItem` — item (coleção `catalog_item`)
|
|
150
|
+
|
|
151
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
152
|
+
| --------------- | ----------------------------- | ------- | ----------- | --------- |
|
|
153
|
+
| `_id` | String | auto (`Guid.shortTimeMillis()`) | — | Gerado se ausente. |
|
|
154
|
+
| `catalogId` | String | `"default"` | Não | Catálogo dono. Se ausente vira `"default"`; se o catálogo não existir, é criado automaticamente. |
|
|
155
|
+
| `name` | String | — | Não (lógico) | Nome do item. |
|
|
156
|
+
| `description` | String | — | Não | Descrição. |
|
|
157
|
+
| `amount` | int | `0` | Não | Estoque global. `-1` = ilimitado. `0` ⇒ sempre esgotado (ver §6). |
|
|
158
|
+
| `active` | boolean | `false` | Não | Item desativado bloqueia a compra (`item_disabled`). |
|
|
159
|
+
| `start` | Date | — | Não | Início da janela de disponibilidade. |
|
|
160
|
+
| `end` | Date | — | Não | Fim da janela de disponibilidade. |
|
|
161
|
+
| `created` | Date | `now` no `add` | — | Setado se ausente. |
|
|
162
|
+
| `updated` | Date | — | — | **Declarado mas nunca preenchido** pelo código (ver §7). |
|
|
163
|
+
| `image` | Image | — | Não | Imagens do item. |
|
|
164
|
+
| `limit` | Limit | — | Não | Limite de compras por período (§3.3). |
|
|
165
|
+
| `extra` | Map<String,Object> | `{}` | Não | Atributos extras. `extra.provider="tango"` ativa integração Tango. |
|
|
166
|
+
| `requires` | List<Requirement> | `[]` | Não | "Preço" — débitos/verificações para comprar (§3.5). |
|
|
167
|
+
| `rewards` | List<Requirement> | `[]` | Não | Recompensas creditadas além do item (§3.5). |
|
|
168
|
+
| `notifications` | List<NotificationDefinition> | `[]` | Não | Notificações disparadas na compra (`EVENT_WIN`). |
|
|
169
|
+
| `restrictions` | List<String> | `null` | — | **Computado, não persistido** (ver abaixo). |
|
|
170
|
+
| `principals` | List<String> | `null` | Não | IDs de player/team com acesso restrito ao item. Vazio/`null` = liberado a todos. |
|
|
171
|
+
| `i18n` | Map<String,Map<String,String>>| `{}` | Não | Internacionalização. |
|
|
172
|
+
| `techniques` | List<String> | `null` | Não | Técnicas de jogo; backfill atribui `GT08` (§3.4). |
|
|
173
|
+
| `owned` | long | `0` | — | Total de compras do item — recalculado pós-compra. |
|
|
174
|
+
| `async` | Boolean | `null` | Não | Default de modo de compra (força compra assíncrona). |
|
|
175
|
+
|
|
176
|
+
**Campo computado (não persiste no documento de leitura/cadastro):**
|
|
177
|
+
- `restrictions` — só é preenchido em `GET /item` **quando o query param `player` é informado** (chama `checkRestrictions` por item). `GET /item/{id}` (por id) **nunca** preenche `restrictions`.
|
|
178
|
+
|
|
179
|
+
**Campos deprecated/legados aceitos mas ignorados** (comentados no código, `@JsonIgnoreProperties(ignoreUnknown=true)` faz a desserialização silenciosamente descartar qualquer campo desconhecido enviado):
|
|
180
|
+
- `points` (`RewardPoint[]`), `triggerURL`, `maxPerUser`, `levelId`, `real`, `statusPurchaseIntent` — todos comentados em `CatalogItem.java`. Enviá-los no JSON **não gera erro e não tem efeito**.
|
|
181
|
+
|
|
182
|
+
### 3.3 `Limit` — limite de compras por período (`engine/action/Limit.java`)
|
|
183
|
+
|
|
184
|
+
| Campo | Tipo | Descrição |
|
|
185
|
+
| ------- | --------------- | --------- |
|
|
186
|
+
| `total` | Object (Number ou String) | Quantidade máxima no período. Se `String`, é tratada como expressão Mustache + exp4j (ex.: `{{player.extra.max_buys}}`). |
|
|
187
|
+
| `per` | String | `"player"`, `"team"` ou `"gamification"`. |
|
|
188
|
+
| `every` | String | Janela de tempo (`1d`, `10h`, `1w`...). Ausente ⇒ janela de `10y` (praticamente "sempre"). |
|
|
189
|
+
| `query` | String | **Declarado mas NÃO usado** em `checkIsInLimit` (ver §7). |
|
|
190
|
+
|
|
191
|
+
> Unidades aceitas em `every` (via `DateUtil.fromKeyword`): `y` (ano), `M` (mês), `w` (semana), `d` (dia), `h` (hora), `m` (minuto).
|
|
192
|
+
> **`per: "team"` não é avaliado** — `checkIsInLimit` só trata `player` e `gamification`. Ver §7.
|
|
193
|
+
|
|
194
|
+
### 3.4 Técnicas de jogo (`techniques`)
|
|
195
|
+
|
|
196
|
+
| Código | Constante (`GameTechniqueManager`) | Significado |
|
|
197
|
+
| ------ | ---------------------------------- | ----------- |
|
|
198
|
+
| `GT08` | `VIRTUALGOOD_CODE` | Identifica o item como **Virtual Good**. |
|
|
199
|
+
|
|
200
|
+
Atribuição automática (`autoConfigureMissingTechniqueFields`): itens cujo `techniques` esteja **ausente ou vazio** (filtro Mongo `{$or:[{techniques:{$exists:true,$size:0}},{techniques:{$exists:false}}]}`) recebem `GT08` em backfill e são re-salvos. É um job de migração — não roda a cada compra.
|
|
201
|
+
|
|
202
|
+
### 3.5 `Requirement` — usado por `requires` e `rewards` (`engine/challenge/Requirement.java`)
|
|
203
|
+
|
|
204
|
+
| Campo | Tipo | Descrição |
|
|
205
|
+
| ----------- | ------------------ | --------- |
|
|
206
|
+
| `total` | int | Quantidade. No débito é forçado a negativo, no crédito a positivo, e multiplicado por `purchase.total`. |
|
|
207
|
+
| `type` | int | Tipo do achievement (tabela abaixo). |
|
|
208
|
+
| `item` | String | Id do ponto/desafio/item alvo. |
|
|
209
|
+
| `operation` | int | `0` verify, `1` deduct. **Só usado em `requires`** (em `rewards` é ignorado — sempre crédito). |
|
|
210
|
+
| `extra` | Map<String,Object> | Controle extra. |
|
|
211
|
+
| `period` | String | Expressão de período (legado de challenge). |
|
|
212
|
+
| `folder` | String | Filtro de catálogo (V4 — não usado no fluxo de compra). |
|
|
213
|
+
| `restrict` | boolean | (V4) só credita se item disponível — não usado no fluxo de compra. |
|
|
214
|
+
| `perPlayer` | boolean | (V4) — não usado no fluxo de compra. |
|
|
215
|
+
|
|
216
|
+
**Enum de `type` (compartilhado com `Achievement`):**
|
|
217
|
+
|
|
218
|
+
| Valor | Tipo | Debitável/Creditável na compra? |
|
|
219
|
+
| ----- | ---- | ------------------------------- |
|
|
220
|
+
| `0` | point | ✅ Sim |
|
|
221
|
+
| `1` | challenge | ✅ Sim |
|
|
222
|
+
| `2` | catalog_item | ✅ Sim |
|
|
223
|
+
| `3` | level | ❌ Ignorado |
|
|
224
|
+
| `4` | crown | ❌ Ignorado |
|
|
225
|
+
| `5` | lottery | ❌ Ignorado |
|
|
226
|
+
| `6` | mystery_box | ❌ Ignorado |
|
|
227
|
+
| `7` | character_star_stat | ❌ Ignorado |
|
|
228
|
+
| `8` | bonus | ❌ Ignorado |
|
|
229
|
+
| `50` | lottery_ticket | ❌ Ignorado |
|
|
230
|
+
| `99` | custom | ❌ Ignorado |
|
|
231
|
+
|
|
232
|
+
`operation`: `0` = verify (apenas valida, não debita), `1` = deduct (valida e debita).
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 4. Endpoints
|
|
237
|
+
|
|
238
|
+
Todos sob `@Path("v3/virtualgoods")`, autenticação **Bearer token** (`AuthBean`), respostas com `null` removido (`JsonUtil.toJsonRemoveNullFields`).
|
|
239
|
+
|
|
240
|
+
### 4.1 Catálogos
|
|
241
|
+
|
|
242
|
+
**`GET /v3/virtualgoods/catalog/{id}`** — `findCatalog`
|
|
243
|
+
Retorna o catálogo com `itens` recalculados a partir de `catalog_item`.
|
|
244
|
+
|
|
245
|
+
**`GET /v3/virtualgoods/catalog`** — `findAllCatalogs`
|
|
246
|
+
|
|
247
|
+
| Param | Tipo | Descrição |
|
|
248
|
+
| ----- | ---- | --------- |
|
|
249
|
+
| `id` | String | Filtro por `_id` exato. |
|
|
250
|
+
| `catalog` | String | Filtro por nome (`$regex`, case-insensitive). |
|
|
251
|
+
| `q` | String | **Concatenado cru** no `$match` do pipeline (ver §8). |
|
|
252
|
+
| `fields` | String | Projeção (`campo1,campo2`). |
|
|
253
|
+
| `published_min`/`published_max` | String (RFC 3339) | Faixa sobre `created`. |
|
|
254
|
+
| `orderby` | String | Campo de ordenação. |
|
|
255
|
+
| `reverse` | boolean | `true` = descendente. |
|
|
256
|
+
| `max_results` | int | Default/limite **100** quando ≤ 0. |
|
|
257
|
+
|
|
258
|
+
**`POST /v3/virtualgoods/catalog`** — `insertCatalog` — corpo: objeto `Catalog`. Retorna `201`. Gera `_id` e `created` se ausentes. Se `itens` vier preenchido, persiste cada item em `catalog_item`.
|
|
259
|
+
|
|
260
|
+
**`DELETE /v3/virtualgoods/catalog/{id}`** — `deleteCatalog`
|
|
261
|
+
**Cascade:** remove o catálogo **e todos** os `catalog_item` com `catalogId == id`.
|
|
262
|
+
|
|
263
|
+
### 4.2 Itens
|
|
264
|
+
|
|
265
|
+
**`GET /v3/virtualgoods/item/{id}`** — `findItem` — por id. Não preenche `restrictions`.
|
|
266
|
+
|
|
267
|
+
**`GET /v3/virtualgoods/item`** — `findAllItems`
|
|
268
|
+
|
|
269
|
+
| Param | Tipo | Descrição |
|
|
270
|
+
| ----- | ---- | --------- |
|
|
271
|
+
| `id` | String | Filtro por `_id`. |
|
|
272
|
+
| `catalog` | String | Filtro por `catalogId` (igualdade exata, apesar do nome). |
|
|
273
|
+
| `name` | String | `$regex` case-insensitive. |
|
|
274
|
+
| `description` | String | `$regex` case-insensitive. |
|
|
275
|
+
| `player` | String | Se informado, adiciona o array `restrictions` a cada item (avaliação de elegibilidade). `me` = jogador do token. |
|
|
276
|
+
| `q`, `fields`, `published_min/max`, `orderby`, `reverse`, `max_results` | — | Igual a catálogos (`max_results` default 100). |
|
|
277
|
+
|
|
278
|
+
**`POST /v3/virtualgoods/item`** — `insertItem` — corpo: objeto `CatalogItem`. Retorna `201`.
|
|
279
|
+
**Atenção:** é um `save` por `_id` (full replace). Reenviar um item com `_id` existente **substitui o documento inteiro**; campos ausentes voltam ao default — incluindo `owned` (→ `0`) e `created` (regerado). Ver checklist.
|
|
280
|
+
|
|
281
|
+
### 4.3 Compra
|
|
282
|
+
|
|
283
|
+
**`POST /v3/virtualgoods/purchase`** — `insertPurchase`
|
|
284
|
+
|
|
285
|
+
| Param | Tipo | Descrição |
|
|
286
|
+
| ----- | ---- | --------- |
|
|
287
|
+
| `async` (query) | String | `"true"` força modo assíncrono. Ausente ⇒ usa `CatalogItem.async`. |
|
|
288
|
+
|
|
289
|
+
Corpo (`Map`, não há DTO tipado):
|
|
290
|
+
|
|
291
|
+
| Campo | Tipo | Comportamento |
|
|
292
|
+
| ----- | ---- | ------------- |
|
|
293
|
+
| `player` | String | `me` resolve para o jogador do token. |
|
|
294
|
+
| `item` | String | Id do `catalog_item`. |
|
|
295
|
+
| `total` | int | **Mínimo forçado a 1**; valores < 1 ou não numéricos viram 1. Multiplica débitos e créditos. |
|
|
296
|
+
| `extra` | Map | Anexado ao achievement da compra. |
|
|
297
|
+
|
|
298
|
+
Resposta síncrona:
|
|
41
299
|
```json
|
|
42
300
|
{
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
301
|
+
"achievements": [ { "player":"tom", "total":-10, "type":0, "item":"points", "time":1700000000000, "extra":{"origin":"<purchaseId>"}, "_id":"..." }, { "player":"tom", "total":1, "type":2, "item":"DTj7lVn", "time":1700000000000, "_id":"<purchaseId>" } ],
|
|
302
|
+
"restrictions": [],
|
|
303
|
+
"milliseconds": { "trigger":2, "verify":5, "debits":1, "credits":1, "status":3, "notify":0, "init":0 },
|
|
304
|
+
"status": "OK"
|
|
47
305
|
}
|
|
48
306
|
```
|
|
49
307
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
308
|
+
Resposta assíncrona:
|
|
309
|
+
```json
|
|
310
|
+
{ "achievements": [ { "player":"tom", "item":"DTj7lVn", "type":2, "total":1 } ], "restrictions": [], "status": "ASYNC" }
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Resposta bloqueada:
|
|
314
|
+
```json
|
|
315
|
+
{ "achievements": [], "restrictions": ["insufficient_requirements","limit_exceeded"], "status": "UNAUTHORIZED" }
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**`POST /v3/virtualgoods/purchase_by_player/{player}`** — `insertPurchaseByPlayer` — idêntico ao `/purchase`, mas o `player` vem no path (corpo `player` é ignorado).
|
|
319
|
+
|
|
320
|
+
**`DELETE /v3/virtualgoods/purchase`** — `undoPurchase` — corpo `{ "_id": "<achievementId>" }`. Estorna a compra (ver §6).
|
|
321
|
+
|
|
322
|
+
**`GET /v3/virtualgoods/evaluate/{id}`** — `evaluate` — diagnóstico de elegibilidade (não compra).
|
|
323
|
+
|
|
324
|
+
| Param | Tipo | Descrição |
|
|
325
|
+
| ----- | ---- | --------- |
|
|
326
|
+
| `player` | String | Jogador a avaliar. |
|
|
327
|
+
| `time` | String | Instante de referência (ms, RFC 3339 ou keyword). Default: agora. |
|
|
328
|
+
|
|
329
|
+
Retorna `{ params, exists, limit_purchases, limit_amount, who, requirements, milliseconds }`.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## 5. Regras de Negócio
|
|
334
|
+
|
|
335
|
+
Regras que existem **no código** e não são óbvias pelo schema:
|
|
336
|
+
|
|
337
|
+
- **`total` mínimo = 1.** Frações ou valores ≤ 0 são normalizados para 1 (não há compra fracionada).
|
|
338
|
+
- **Compra serializada por tenant.** `purchase(...)` é `synchronized` no `CatalogManager`. Há um `CatalogManager` por `apiKey`, então compras concorrentes do mesmo tenant são processadas **uma de cada vez**.
|
|
339
|
+
- **Estoque é global e histórico.** `amount` é comparado contra `count(achievement {type:2, item:id})` — soma de **todas as compras de todos os jogadores, sem janela de tempo**. `amount = -1` ⇒ ilimitado. `amount = 0` ⇒ sempre esgotado.
|
|
340
|
+
- **Limite por período** (`limit`): `per:"player"` conta achievements do jogador; `per:"gamification"` conta de todos; **`per:"team"` não é avaliado** (nunca bloqueia).
|
|
341
|
+
- **Elegibilidade por principal** (`principals`): vazio/`null` libera a todos; senão exige que o `principalId` do jogador esteja na lista, ou que o jogador pertença a um team listado.
|
|
342
|
+
- **Tipos movimentados.** Apenas `type ∈ {0,1,2}` são debitados/creditados. Demais tipos são aceitos no cadastro mas ignorados na compra (ver §6/§7).
|
|
343
|
+
- **Consistência eventual no modo assíncrono.** A resposta `ASYNC` retorna antes do processamento; restrições/recompensas só são aplicadas quando a thread consome a fila.
|
|
344
|
+
- **Multi-tenant.** Todo acesso é resolvido por `FrontController.getInstance(apiKey)`; cada tenant tem suas coleções e sua thread de processamento assíncrono.
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## 6. Comportamentos Automáticos
|
|
349
|
+
|
|
350
|
+
| Comportamento | Trigger | Impacto | Persistência |
|
|
351
|
+
| ------------- | ------- | ------- | ------------ |
|
|
352
|
+
| Geração de `_id`/`created` | `add`/`addItem` sem `_id`/`created` | IDs `Guid.shortTimeMillis()`; `created=now` | `catalog`/`catalog_item` |
|
|
353
|
+
| Auto-criação de catálogo | `addItem` com `catalogId` inexistente | Cria `Catalog{_id=catalogId}` | `catalog` |
|
|
354
|
+
| Default `catalogId="default"` | `addItem` sem `catalogId` | Item vai para catálogo `default` | `catalog_item` |
|
|
355
|
+
| Cascade delete | `DELETE /catalog/{id}` | Remove todos os itens daquele catálogo | `catalog_item` |
|
|
356
|
+
| Débito dos `requires` | `purchase` (sem restrições) | Achievement negativo p/ type 0/1/2 com `operation=DEDUCT`, `extra.origin=purchaseId` | `achievement` |
|
|
357
|
+
| Crédito dos `rewards` | `purchase` | Achievement positivo p/ type 0/1/2, `extra.origin=purchaseId` | `achievement` |
|
|
358
|
+
| Crédito do item | `purchase` | Achievement `type=2` da compra | `achievement` |
|
|
359
|
+
| Triggers de compra | `purchase` | `before_purchase_validation`, `before_win`, `after_win` na entidade `catalog_item` | — |
|
|
360
|
+
| Integração Tango | `extra.provider=="tango"` | Cria pedido externo; grava `extra.tango_order_id` | `achievement` |
|
|
361
|
+
| Notificações | `purchase` | Envia `NotificationDefinition` `EVENT_WIN` do item | módulo notification |
|
|
362
|
+
| Recalcular `owned` | fim de `purchase` | `owned = count(achievement type=2, item)` | `catalog_item` |
|
|
363
|
+
| Recalcular status do jogador | `purchase` | `updatePlayerStatus` (sync) / `updateUserStatus` (async) | módulo player |
|
|
364
|
+
| Estorno | `DELETE /purchase` | Remove achievement da compra + achievements `extra.origin=id`; recalcula status | `achievement` |
|
|
365
|
+
|
|
366
|
+
```mermaid
|
|
367
|
+
flowchart LR
|
|
368
|
+
A[Avaliar item para jogador] --> B{active?}
|
|
369
|
+
B -- não --> RB[item_disabled]
|
|
370
|
+
B -- sim --> C{requires OK?}
|
|
371
|
+
C -- não --> RC[insufficient_requirements]
|
|
372
|
+
C -- sim --> D{dentro de start/end?}
|
|
373
|
+
D -- não --> RD[item_out_of_time]
|
|
374
|
+
D -- sim --> E{limit por periodo OK?}
|
|
375
|
+
E -- não --> RE[limit_exceeded]
|
|
376
|
+
E -- sim --> F{amount/estoque OK?}
|
|
377
|
+
F -- não --> RF[item_sold_out]
|
|
378
|
+
F -- sim --> G{principal permitido?}
|
|
379
|
+
G -- não --> RG[principal_not_allowed]
|
|
380
|
+
G -- sim --> OK[elegível]
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
> **Estorno não recalcula `owned`.** `undoPurchase` remove os achievements e atualiza o status do jogador, mas **não atualiza o campo `owned`** do item — ele fica defasado até a próxima compra recalculá-lo. As checagens de `amount`/`limit` usam `count()` ao vivo, então a disponibilidade real é correta; apenas o número exibido em `owned` pode divergir temporariamente.
|
|
384
|
+
|
|
385
|
+
> **Recompensas de tipo não suportado entram na resposta mas não são gravadas.** No laço de `rewards`, o `Achievement` é adicionado à lista `achievements` retornada **antes** do teste de coleção; o `save` só ocorre se o tipo for 0/1/2. Para tipos 3–7/50, a API responde como se a recompensa tivesse sido creditada, mas **nada é persistido**.
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## 7. Suportado vs NÃO Suportado
|
|
390
|
+
|
|
391
|
+
### ✅ Suportado
|
|
392
|
+
|
|
393
|
+
- CRUD de catálogos (`/catalog`) e itens (`/item`) com filtros, projeção, paginação e ordenação.
|
|
394
|
+
- Compra síncrona e assíncrona (`/purchase`, `/purchase_by_player/{player}`), com débito de `requires` e crédito de `rewards` para **point/challenge/catalog_item**.
|
|
395
|
+
- Restrições de disponibilidade: `active`, janela `start`/`end`, `limit` (`player`/`gamification`), `amount` (estoque global), `principals`.
|
|
396
|
+
- Estorno de compra (`DELETE /purchase`) com remoção dos achievements derivados.
|
|
397
|
+
- Diagnóstico de elegibilidade (`GET /evaluate/{id}`).
|
|
398
|
+
- Triggers (`before_purchase_validation`, `before_win`, `after_win`) e notificações `EVENT_WIN`.
|
|
399
|
+
- Integração Tango Card (`extra.provider="tango"`).
|
|
400
|
+
- Internacionalização (`i18n`) em catálogo e item.
|
|
401
|
+
|
|
402
|
+
### ❌ NÃO Suportado / Limitações confirmadas no código
|
|
403
|
+
|
|
404
|
+
- **`requires`/`rewards` de tipos 3–7/50** (level, crown, lottery, mystery_box, character_star_stat, lottery_ticket) — aceitos no cadastro, **ignorados** na movimentação. Em `rewards`, ainda aparecem na resposta sem serem gravados.
|
|
405
|
+
- **`limit.per = "team"`** — constante existe (`Limit.PER_TEAM`), mas `checkIsInLimit` só trata `player`/`gamification`; limite por team **nunca é aplicado**.
|
|
406
|
+
- **`limit.query`** — campo declarado, **nunca usado** em `checkIsInLimit`.
|
|
407
|
+
- **`CatalogItem.updated`** — campo declarado, **nunca preenchido** por `add`/`update`.
|
|
408
|
+
- **Fila assíncrona não-durável** — `PurchaseAsyncProcessor` é `LinkedBlockingQueue` em memória; compras pendentes são perdidas em restart.
|
|
409
|
+
- **`owned` defasado após estorno** — não recalculado em `undoPurchase`.
|
|
410
|
+
- **Divergência `findAll` vs `findById`** — lista de catálogos retorna `itens` embutidos; `findById` recalcula de `catalog_item`.
|
|
411
|
+
- **Item inexistente na compra** — `purchase` adiciona `item_does_not_exist`, porém `checkRestrictions(null,...)` desreferencia o item → tende a lançar `NullPointerException` (HTTP 500) antes de devolver a resposta `UNAUTHORIZED`. Valide a existência do item antes.
|
|
412
|
+
- **Campos legados de `CatalogItem`** (`points`, `triggerURL`, `maxPerUser`, `levelId`, `real`, `statusPurchaseIntent`) — comentados; ignorados silenciosamente se enviados.
|
|
413
|
+
- **Endpoints legados v2** — `2.0.0/catalog` (`CatalogRest`) e `2.0.0/catalog_item` (`CatalogItemRest`) usam a entidade `Purchase` (coleção `purchase`) e `PurchaseManager.buy`/`get_purchases`. **Não fazem parte do módulo `/v3/virtualgoods`** e não compartilham a coleção de compras (que para o v3 é `achievement`). A entidade `Purchase` também carrega campos comentados (`price`, `pointCategoryId`, `delivered`, `deliveredTime`).
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## 8. Segurança e Permissões
|
|
418
|
+
|
|
419
|
+
- **Autenticação:** Bearer token resolvido por `AuthBean` em todos os endpoints; o tenant é determinado por `apiKey` (`FrontController.getInstance`).
|
|
420
|
+
- **Isolamento multi-tenant:** coleções e thread de processamento assíncrono são por tenant.
|
|
421
|
+
- **Injeção NoSQL — superfície confirmada:** o parâmetro **`q`** de `GET /catalog` e `GET /item` é **concatenado diretamente** na string do estágio `$match` do pipeline de agregação (`query.append(", " + q)` em `findAllCatalogs`/`findAllItems`), sem sanitização. Idem para `orderby`, interpolado em `{$sort:{<orderby>:...}}`. Operadores Mongo arbitrários enviados em `q` são executados. Restrinja/valide esses parâmetros na borda (gateway) e nunca exponha-os a entrada não confiável.
|
|
422
|
+
- **`@JsonIgnoreProperties(ignoreUnknown=true)`** em todas as entidades faz campos desconhecidos serem descartados silenciosamente — não há rejeição de payload malformado.
|
|
423
|
+
|
|
424
|
+
---
|
|
53
425
|
|
|
54
|
-
|
|
55
|
-
**Método:** POST
|
|
56
|
-
**Endpoint:** `/v3/virtualgoods/item`
|
|
426
|
+
## 9. Observabilidade e Troubleshooting
|
|
57
427
|
|
|
58
|
-
**
|
|
428
|
+
**Diagnóstico de elegibilidade (sem comprar):**
|
|
429
|
+
```
|
|
430
|
+
GET /v3/virtualgoods/evaluate/{itemId}?player=tom
|
|
431
|
+
```
|
|
432
|
+
Retorna `exists`, `limit_purchases` (com `total_allowed`/`total_done`/`end_date`), `limit_amount`, `who` e `requirements` — cada um com `status: OK|UNAUTHORIZED`.
|
|
433
|
+
|
|
434
|
+
**Verificar restrições de um item para um jogador na listagem:**
|
|
435
|
+
```
|
|
436
|
+
GET /v3/virtualgoods/item?player=tom&q=_id:"DTj7lVn"
|
|
437
|
+
```
|
|
438
|
+
O array `restrictions` virá preenchido por item.
|
|
439
|
+
|
|
440
|
+
**Por que a compra retornou `UNAUTHORIZED`?** Consulte o array `restrictions`. Strings possíveis:
|
|
441
|
+
|
|
442
|
+
| Restrição | Causa |
|
|
443
|
+
| --------- | ----- |
|
|
444
|
+
| `item_does_not_exist` | `item` não existe (pode acompanhar erro 500 — ver §7). |
|
|
445
|
+
| `player_does_not_exist` | `player` não é player nem team. |
|
|
446
|
+
| `item_disabled` | `active=false`. |
|
|
447
|
+
| `insufficient_requirements` | jogador não atende aos `requires`. |
|
|
448
|
+
| `item_out_of_time` | fora da janela `start`/`end`. |
|
|
449
|
+
| `limit_exceeded` | excedeu `limit` no período. |
|
|
450
|
+
| `item_sold_out` | `count(achievement type=2,item) >= amount`. |
|
|
451
|
+
| `principal_not_allowed` | jogador/team fora de `principals`. |
|
|
452
|
+
| `restrictions` | falha genérica no modo assíncrono (validação rápida). |
|
|
453
|
+
| (mensagem Tango) | erro retornado pela integração Tango Card. |
|
|
454
|
+
|
|
455
|
+
**Investigar compras direto no banco (coleção `achievement`):**
|
|
456
|
+
```
|
|
457
|
+
// quantas vezes o item foi comprado (= owned real)
|
|
458
|
+
db.achievement.count({ type: 2, item: "DTj7lVn" })
|
|
459
|
+
// compras de um jogador
|
|
460
|
+
db.achievement.find({ type: 2, item: "DTj7lVn", player: "tom" }).sort({ time: -1 })
|
|
461
|
+
// movimentações derivadas de uma compra (débitos/créditos)
|
|
462
|
+
db.achievement.find({ "extra.origin": "<purchaseId>" })
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
**Erros comuns:**
|
|
466
|
+
- "Comprei mas não debitou pontos" → o `requires` está com `operation=0` (verify) em vez de `1` (deduct), ou o `type` não é 0/1/2.
|
|
467
|
+
- "Recompensa não apareceu no saldo" → `reward.type` fora de {0,1,2} (ignorado), ou `total=0`.
|
|
468
|
+
- "Limite por equipe não funciona" → `limit.per="team"` não é avaliado.
|
|
469
|
+
- "Estoque não bate com `owned`" → houve estorno; `owned` só recalcula na próxima compra (use `count()` ao vivo).
|
|
470
|
+
- "Item sumiu ao re-salvar" → `POST /item` com `_id` existente faz full replace; reenvie todos os campos.
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## 10. Exemplos Práticos
|
|
475
|
+
|
|
476
|
+
### 10.1 Mínimo funcional
|
|
477
|
+
|
|
478
|
+
Criar item gratuito e comprá-lo:
|
|
479
|
+
```json
|
|
480
|
+
// POST /v3/virtualgoods/item
|
|
481
|
+
{ "_id": "badge_basic", "name": "Badge Básico", "active": true, "amount": -1 }
|
|
482
|
+
```
|
|
483
|
+
```json
|
|
484
|
+
// POST /v3/virtualgoods/purchase
|
|
485
|
+
{ "player": "tom", "item": "badge_basic" }
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### 10.2 Avançado (todos os campos relevantes)
|
|
489
|
+
|
|
490
|
+
Item pago com requisito, recompensa, limite, janela e notificação:
|
|
59
491
|
```json
|
|
492
|
+
// POST /v3/virtualgoods/item
|
|
60
493
|
{
|
|
494
|
+
"_id": "DTj7lVn",
|
|
61
495
|
"catalogId": "gifts",
|
|
62
|
-
"name": "
|
|
63
|
-
"description": "
|
|
64
|
-
"amount": -1,
|
|
496
|
+
"name": "Caneca",
|
|
497
|
+
"description": "Caneca da campanha",
|
|
65
498
|
"active": true,
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
499
|
+
"amount": 100,
|
|
500
|
+
"start": 1709251200000,
|
|
501
|
+
"end": 1735689600000,
|
|
502
|
+
"limit": { "total": 1, "per": "player", "every": "1d" },
|
|
503
|
+
"requires": [ { "total": 50, "type": 0, "item": "coins", "operation": 1 } ],
|
|
504
|
+
"rewards": [ { "total": 1, "type": 2, "item": "badge_basic" } ],
|
|
505
|
+
"notifications": [ { "event": 0, "type": 0, "scope": 99, "content": "Você comprou: {{item.name}}" } ],
|
|
71
506
|
"techniques": ["GT08"],
|
|
72
|
-
"
|
|
507
|
+
"extra": { "provider": "wecare" }
|
|
73
508
|
}
|
|
74
509
|
```
|
|
510
|
+
```json
|
|
511
|
+
// POST /v3/virtualgoods/purchase?async=true
|
|
512
|
+
{ "player": "me", "item": "DTj7lVn", "total": 2, "extra": { "endereco": "casa" } }
|
|
513
|
+
```
|
|
75
514
|
|
|
76
|
-
###
|
|
77
|
-
**Método:** POST
|
|
78
|
-
**Endpoint:** `/v3/virtualgoods/purchase`
|
|
515
|
+
### 10.3 Anti-pattern (o que NÃO fazer)
|
|
79
516
|
|
|
80
|
-
**Exemplo de Body:**
|
|
81
517
|
```json
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
"item": "
|
|
85
|
-
|
|
86
|
-
|
|
518
|
+
// ❌ ERRADO — recompensa de tipo não suportado: aparece na resposta mas NÃO é gravada
|
|
519
|
+
{ "_id": "x", "name": "x", "active": true, "amount": -1,
|
|
520
|
+
"rewards": [ { "total": 1, "type": 3, "item": "level_master" } ] } // type=3 (level) é ignorado
|
|
521
|
+
|
|
522
|
+
// ❌ ERRADO — limite por equipe nunca é aplicado
|
|
523
|
+
"limit": { "total": 5, "per": "team", "every": "1w" }
|
|
524
|
+
|
|
525
|
+
// ❌ ERRADO — requisito como "verify" não debita o jogador
|
|
526
|
+
"requires": [ { "total": 50, "type": 0, "item": "coins", "operation": 0 } ] // operation=0 não debita
|
|
527
|
+
|
|
528
|
+
// ❌ ERRADO — re-POST sem 'owned'/'created' faz full replace e zera esses campos
|
|
529
|
+
{ "_id": "DTj7lVn", "name": "Caneca (novo nome)" } // perde amount, requires, rewards, owned...
|
|
87
530
|
```
|
|
88
531
|
|
|
89
|
-
|
|
90
|
-
**Método:** DELETE
|
|
91
|
-
**Endpoint:** `/v3/virtualgoods/purchase`
|
|
532
|
+
---
|
|
92
533
|
|
|
93
|
-
##
|
|
534
|
+
## Checklist de Configuração
|
|
94
535
|
|
|
95
|
-
- [ ]
|
|
96
|
-
- [ ]
|
|
97
|
-
- [ ]
|
|
98
|
-
- [ ]
|
|
99
|
-
- [ ]
|
|
536
|
+
- [ ] Item com `active: true` (senão `item_disabled`).
|
|
537
|
+
- [ ] `amount` definido corretamente: `-1` para ilimitado; lembre-se que `0` ⇒ sempre esgotado.
|
|
538
|
+
- [ ] `requires` com `operation: 1` (deduct) para realmente debitar; `type` ∈ {0,1,2}.
|
|
539
|
+
- [ ] `rewards` com `type` ∈ {0,1,2} — outros tipos são ignorados (e não persistidos).
|
|
540
|
+
- [ ] Itens em `requires`/`rewards` (point category, challenge, catalog_item) existem antes de criar o item.
|
|
541
|
+
- [ ] Se usar `limit`, escolha `per: "player"` ou `"gamification"` — **não** `"team"`.
|
|
542
|
+
- [ ] Janela `start`/`end` coerente com o fuso/relógio do servidor.
|
|
543
|
+
- [ ] **Armadilha:** `POST /item` com `_id` existente substitui o documento inteiro — reenvie todos os campos (incl. `owned` se quiser preservar).
|
|
544
|
+
- [ ] **Armadilha:** modo assíncrono (`async=true` ou `CatalogItem.async=true`) retorna `ASYNC` sem garantir o resultado; não confie na resposta para confirmar débito/crédito.
|
|
545
|
+
- [ ] **Armadilha:** após `DELETE /purchase`, `owned` fica defasado até a próxima compra.
|
|
546
|
+
- [ ] **Segurança:** nunca exponha o parâmetro `q`/`orderby` a entrada não confiável (injeção no pipeline Mongo).
|