funifier-mcp 0.2.26 → 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.
Files changed (170) hide show
  1. package/.cursor/rules/funifier.mdc +38 -41
  2. package/.github/copilot-instructions.md +38 -41
  3. package/AGENTS.md +56 -49
  4. package/README.md +40 -22
  5. package/datasource-funifier-docs/.coverage.json +326 -0
  6. package/datasource-funifier-docs/.validation.json +593 -0
  7. package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
  8. package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
  9. package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
  10. package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
  11. package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
  12. package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
  13. package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
  14. package/datasource-funifier-docs/knowledge/index.md +4 -1
  15. package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
  16. package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
  17. package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
  18. package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
  19. package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
  20. package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
  21. package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
  22. package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
  23. package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
  24. package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
  25. package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
  26. package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
  27. package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
  28. package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
  29. package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
  30. package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
  31. package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
  32. package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
  33. package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
  34. package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
  35. package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
  36. package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
  37. package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
  38. package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
  39. package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
  40. package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
  41. package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
  42. package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
  43. package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
  44. package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
  45. package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
  46. package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
  47. package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
  48. package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
  49. package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
  50. package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
  51. package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
  52. package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
  53. package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
  54. package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
  55. package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
  56. package/dist/cli/init.d.ts.map +1 -1
  57. package/dist/cli/init.js +42 -1
  58. package/dist/cli/init.js.map +1 -1
  59. package/dist/cli/init.test.js +74 -3
  60. package/dist/cli/init.test.js.map +1 -1
  61. package/dist/cli/persona.d.ts +3 -0
  62. package/dist/cli/persona.d.ts.map +1 -0
  63. package/dist/cli/persona.js +25 -0
  64. package/dist/cli/persona.js.map +1 -0
  65. package/dist/mcp/bundle.js +119 -93
  66. package/dist/mcp/index.js +2 -2
  67. package/dist/mcp/index.js.map +1 -1
  68. package/dist/mcp/resources/documentation.d.ts +1 -1
  69. package/dist/mcp/resources/documentation.d.ts.map +1 -1
  70. package/dist/mcp/resources/documentation.js +39 -3
  71. package/dist/mcp/resources/documentation.js.map +1 -1
  72. package/dist/mcp/tools/connect.d.ts.map +1 -1
  73. package/dist/mcp/tools/connect.js +18 -8
  74. package/dist/mcp/tools/connect.js.map +1 -1
  75. package/dist/mcp/tools/database.d.ts.map +1 -1
  76. package/dist/mcp/tools/database.js +59 -47
  77. package/dist/mcp/tools/database.js.map +1 -1
  78. package/dist/mcp/tools/database.test.js +2 -2
  79. package/dist/mcp/tools/database.test.js.map +1 -1
  80. package/dist/mcp/tools/delete.d.ts.map +1 -1
  81. package/dist/mcp/tools/delete.js +13 -3
  82. package/dist/mcp/tools/delete.js.map +1 -1
  83. package/dist/mcp/tools/execute.d.ts.map +1 -1
  84. package/dist/mcp/tools/execute.js +20 -9
  85. package/dist/mcp/tools/execute.js.map +1 -1
  86. package/dist/mcp/tools/folder.d.ts.map +1 -1
  87. package/dist/mcp/tools/folder.js +22 -12
  88. package/dist/mcp/tools/folder.js.map +1 -1
  89. package/dist/mcp/tools/get.d.ts.map +1 -1
  90. package/dist/mcp/tools/get.js +16 -6
  91. package/dist/mcp/tools/get.js.map +1 -1
  92. package/dist/mcp/tools/index.d.ts +1 -1
  93. package/dist/mcp/tools/index.d.ts.map +1 -1
  94. package/dist/mcp/tools/index.js +3 -1
  95. package/dist/mcp/tools/index.js.map +1 -1
  96. package/dist/mcp/tools/list.d.ts.map +1 -1
  97. package/dist/mcp/tools/list.js +38 -14
  98. package/dist/mcp/tools/list.js.map +1 -1
  99. package/dist/mcp/tools/logs.d.ts.map +1 -1
  100. package/dist/mcp/tools/logs.js +15 -5
  101. package/dist/mcp/tools/logs.js.map +1 -1
  102. package/dist/mcp/tools/save.d.ts.map +1 -1
  103. package/dist/mcp/tools/save.js +14 -4
  104. package/dist/mcp/tools/save.js.map +1 -1
  105. package/dist/mcp/tools/save.test.js +3 -3
  106. package/dist/mcp/tools/save.test.js.map +1 -1
  107. package/dist/mcp/tools/search-docs.d.ts +3 -0
  108. package/dist/mcp/tools/search-docs.d.ts.map +1 -0
  109. package/dist/mcp/tools/search-docs.js +102 -0
  110. package/dist/mcp/tools/search-docs.js.map +1 -0
  111. package/package.json +6 -2
  112. package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
  113. package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
  114. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
  115. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
  116. package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
  117. package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
  118. package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
  119. package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
  120. package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
  121. package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
  122. package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
  123. package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
  124. package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
  125. package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
  126. package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
  127. package/skills/funifier/SKILL.md +88 -0
  128. package/skills/funifier/references/configure-security.md +96 -0
  129. package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
  130. package/skills/funifier/references/create-aggregate.md +144 -0
  131. package/skills/funifier/references/create-challenge.md +116 -0
  132. package/skills/funifier/references/create-competition.md +98 -0
  133. package/skills/funifier/references/create-crossword.md +574 -0
  134. package/skills/funifier/references/create-custom-object.md +91 -0
  135. package/skills/funifier/references/create-custom-page.md +135 -0
  136. package/skills/funifier/references/create-folder.md +104 -0
  137. package/skills/funifier/references/create-lastmile.md +643 -0
  138. package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
  139. package/skills/funifier/references/create-level.md +94 -0
  140. package/skills/funifier/references/create-lottery.md +913 -0
  141. package/skills/funifier/references/create-mystery.md +769 -0
  142. package/skills/funifier/references/create-notification.md +75 -0
  143. package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
  144. package/skills/funifier/references/create-quiz.md +98 -0
  145. package/skills/funifier/references/create-scheduler.md +141 -0
  146. package/skills/funifier/references/create-story.md +636 -0
  147. package/skills/funifier/references/create-swap.md +95 -0
  148. package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
  149. package/skills/funifier/references/create-virtual-good.md +96 -0
  150. package/skills/funifier/references/create-webhook.md +72 -0
  151. package/skills/funifier/references/create-websocket.md +71 -0
  152. package/skills/funifier/references/create-widget.md +76 -0
  153. package/skills/funifier/references/debug.md +87 -0
  154. package/skills/funifier/references/help.md +81 -0
  155. package/skills/funifier/references/implement-frontend.md +106 -0
  156. package/skills/funifier/references/import-csv.md +75 -0
  157. package/skills/funifier/references/manage-player.md +82 -0
  158. package/skills/funifier/references/manage-team.md +76 -0
  159. package/skills/funifier/references/upload-file.md +91 -0
  160. package/skills/funifier-create-aggregate/SKILL.md +0 -127
  161. package/skills/funifier-create-challenge/SKILL.md +0 -88
  162. package/skills/funifier-create-custom-page/SKILL.md +0 -127
  163. package/skills/funifier-create-level/SKILL.md +0 -87
  164. package/skills/funifier-create-quiz/SKILL.md +0 -87
  165. package/skills/funifier-create-scheduler/SKILL.md +0 -127
  166. package/skills/funifier-create-virtual-good/SKILL.md +0 -87
  167. package/skills/funifier-debug/SKILL.md +0 -92
  168. package/skills/funifier-help/SKILL.md +0 -86
  169. package/skills/funifier-implement-frontend/SKILL.md +0 -90
  170. package/skills/funifier-index/SKILL.md +0 -58
@@ -1,99 +1,546 @@
1
- # Virtual Good (Loja Virtual)
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
- ## O que é
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
- Cadastro e gestão de objetos e benefícios que podem ser adquiridos pelos jogadores. Permite cadastrar produtos físicos, virtuais ou benefícios, estipular preços, quantidades, limites de compra e atributos. Os jogadores trocam pontos ou itens por objetos na loja.
12
+ ---
9
13
 
10
- ## Quando usar
14
+ ## 1. Visão Geral
11
15
 
12
- - Para criar sistema de recompensas tangíveis
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
- ## Dependências
18
+ Papel arquitetural:
18
19
 
19
- - **Point**: moeda para compra deve existir
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
- ## Checklist de Configuração no Studio
24
+ Relação com outros módulos:
22
25
 
23
- - [ ] Criar catálogo (ex: "gifts", "rewards")
24
- - [ ] Criar itens dentro do catálogo
25
- - [ ] Definir preço (requires: tipo de ponto + quantidade)
26
- - [ ] Definir quantidade em estoque (amount, -1 = ilimitado)
27
- - [ ] Definir limite de compras por jogador
28
- - [ ] Ativar item (active: true)
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
- ## API Endpoints
33
+ ---
31
34
 
32
- ### Listar Catálogos
33
- **Método:** GET
34
- **Endpoint:** `/v3/virtualgoods/catalog`
35
+ ## 2. Arquitetura e Fluxos
35
36
 
36
- ### Criar Catálogo
37
- **Método:** POST
38
- **Endpoint:** `/v3/virtualgoods/catalog`
37
+ ### 2.1 Pipeline principal de compra — `CatalogManager.purchase(Achievement, boolean async)`
39
38
 
40
- **Exemplo de Body:**
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
- "catalog": "Gifts",
44
- "extra": {},
45
- "i18n": {},
46
- "_id": "gifts"
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
- ### Listar Itens
51
- **Método:** GET
52
- **Endpoint:** `/v3/virtualgoods/item`
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
- ### Criar Item
55
- **Método:** POST
56
- **Endpoint:** `/v3/virtualgoods/item`
426
+ ## 9. Observabilidade e Troubleshooting
57
427
 
58
- **Exemplo de Body:**
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": "Bike",
63
- "description": "Electric bike",
64
- "amount": -1,
496
+ "name": "Caneca",
497
+ "description": "Caneca da campanha",
65
498
  "active": true,
66
- "extra": {},
67
- "requires": [],
68
- "rewards": [],
69
- "notifications": [],
70
- "i18n": {},
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
- "_id": "DTj7lVn"
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
- ### Realizar Compra
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
- "player": "tom",
84
- "item": "DTj7lVn",
85
- "total": 1
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
- ### Excluir Compra
90
- **Método:** DELETE
91
- **Endpoint:** `/v3/virtualgoods/purchase`
532
+ ---
92
533
 
93
- ## Validações e Testes
534
+ ## Checklist de Configuração
94
535
 
95
- - [ ] Catálogo e itens aparecem nas respectivas listas
96
- - [ ] Jogador consegue comprar item com saldo suficiente
97
- - [ ] Compra debita pontos corretamente
98
- - [ ] Estoque é decrementado (se não ilimitado)
99
- - [ ] Limite de compras por jogador é respeitado
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).