funifier-mcp 0.2.26 → 0.2.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/.cursor/rules/funifier.mdc +38 -41
  2. package/.github/copilot-instructions.md +38 -41
  3. package/AGENTS.md +56 -49
  4. package/README.md +40 -22
  5. package/datasource-funifier-docs/.coverage.json +326 -0
  6. package/datasource-funifier-docs/.validation.json +593 -0
  7. package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
  8. package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
  9. package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
  10. package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
  11. package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
  12. package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
  13. package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
  14. package/datasource-funifier-docs/knowledge/index.md +4 -1
  15. package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
  16. package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
  17. package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
  18. package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
  19. package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
  20. package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
  21. package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
  22. package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
  23. package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
  24. package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
  25. package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
  26. package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
  27. package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
  28. package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
  29. package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
  30. package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
  31. package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
  32. package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
  33. package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
  34. package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
  35. package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
  36. package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
  37. package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
  38. package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
  39. package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
  40. package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
  41. package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
  42. package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
  43. package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
  44. package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
  45. package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
  46. package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
  47. package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
  48. package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
  49. package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
  50. package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
  51. package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
  52. package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
  53. package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
  54. package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
  55. package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
  56. package/dist/cli/init.d.ts.map +1 -1
  57. package/dist/cli/init.js +42 -1
  58. package/dist/cli/init.js.map +1 -1
  59. package/dist/cli/init.test.js +74 -3
  60. package/dist/cli/init.test.js.map +1 -1
  61. package/dist/cli/persona.d.ts +3 -0
  62. package/dist/cli/persona.d.ts.map +1 -0
  63. package/dist/cli/persona.js +25 -0
  64. package/dist/cli/persona.js.map +1 -0
  65. package/dist/mcp/bundle.js +119 -93
  66. package/dist/mcp/check-update.d.ts +5 -0
  67. package/dist/mcp/check-update.d.ts.map +1 -1
  68. package/dist/mcp/check-update.js +21 -10
  69. package/dist/mcp/check-update.js.map +1 -1
  70. package/dist/mcp/check-update.test.d.ts +2 -0
  71. package/dist/mcp/check-update.test.d.ts.map +1 -0
  72. package/dist/mcp/check-update.test.js +33 -0
  73. package/dist/mcp/check-update.test.js.map +1 -0
  74. package/dist/mcp/index.js +2 -2
  75. package/dist/mcp/index.js.map +1 -1
  76. package/dist/mcp/prompts/templates.d.ts.map +1 -1
  77. package/dist/mcp/prompts/templates.js +35 -0
  78. package/dist/mcp/prompts/templates.js.map +1 -1
  79. package/dist/mcp/resources/documentation.d.ts +1 -1
  80. package/dist/mcp/resources/documentation.d.ts.map +1 -1
  81. package/dist/mcp/resources/documentation.js +39 -3
  82. package/dist/mcp/resources/documentation.js.map +1 -1
  83. package/dist/mcp/tools/connect.d.ts.map +1 -1
  84. package/dist/mcp/tools/connect.js +18 -8
  85. package/dist/mcp/tools/connect.js.map +1 -1
  86. package/dist/mcp/tools/database.d.ts.map +1 -1
  87. package/dist/mcp/tools/database.js +59 -47
  88. package/dist/mcp/tools/database.js.map +1 -1
  89. package/dist/mcp/tools/database.test.js +2 -2
  90. package/dist/mcp/tools/database.test.js.map +1 -1
  91. package/dist/mcp/tools/delete.d.ts.map +1 -1
  92. package/dist/mcp/tools/delete.js +13 -3
  93. package/dist/mcp/tools/delete.js.map +1 -1
  94. package/dist/mcp/tools/execute.d.ts.map +1 -1
  95. package/dist/mcp/tools/execute.js +20 -9
  96. package/dist/mcp/tools/execute.js.map +1 -1
  97. package/dist/mcp/tools/folder.d.ts.map +1 -1
  98. package/dist/mcp/tools/folder.js +22 -12
  99. package/dist/mcp/tools/folder.js.map +1 -1
  100. package/dist/mcp/tools/get.d.ts.map +1 -1
  101. package/dist/mcp/tools/get.js +16 -6
  102. package/dist/mcp/tools/get.js.map +1 -1
  103. package/dist/mcp/tools/index.d.ts +1 -1
  104. package/dist/mcp/tools/index.d.ts.map +1 -1
  105. package/dist/mcp/tools/index.js +28 -1
  106. package/dist/mcp/tools/index.js.map +1 -1
  107. package/dist/mcp/tools/list.d.ts.map +1 -1
  108. package/dist/mcp/tools/list.js +38 -14
  109. package/dist/mcp/tools/list.js.map +1 -1
  110. package/dist/mcp/tools/logs.d.ts.map +1 -1
  111. package/dist/mcp/tools/logs.js +15 -5
  112. package/dist/mcp/tools/logs.js.map +1 -1
  113. package/dist/mcp/tools/save.d.ts.map +1 -1
  114. package/dist/mcp/tools/save.js +14 -4
  115. package/dist/mcp/tools/save.js.map +1 -1
  116. package/dist/mcp/tools/save.test.js +3 -3
  117. package/dist/mcp/tools/save.test.js.map +1 -1
  118. package/dist/mcp/tools/search-docs.d.ts +3 -0
  119. package/dist/mcp/tools/search-docs.d.ts.map +1 -0
  120. package/dist/mcp/tools/search-docs.js +102 -0
  121. package/dist/mcp/tools/search-docs.js.map +1 -0
  122. package/package.json +6 -2
  123. package/skills/acquire-funifier-knowledge/SKILL.md +155 -0
  124. package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
  125. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
  126. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
  127. package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
  128. package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
  129. package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
  130. package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +86 -0
  131. package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
  132. package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
  133. package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
  134. package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
  135. package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
  136. package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
  137. package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
  138. package/skills/funifier/SKILL.md +88 -0
  139. package/skills/funifier/references/configure-security.md +96 -0
  140. package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
  141. package/skills/funifier/references/create-aggregate.md +144 -0
  142. package/skills/funifier/references/create-challenge.md +116 -0
  143. package/skills/funifier/references/create-competition.md +98 -0
  144. package/skills/funifier/references/create-crossword.md +574 -0
  145. package/skills/funifier/references/create-custom-object.md +91 -0
  146. package/skills/funifier/references/create-custom-page.md +135 -0
  147. package/skills/funifier/references/create-folder.md +104 -0
  148. package/skills/funifier/references/create-lastmile.md +643 -0
  149. package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
  150. package/skills/funifier/references/create-level.md +94 -0
  151. package/skills/funifier/references/create-lottery.md +913 -0
  152. package/skills/funifier/references/create-mystery.md +769 -0
  153. package/skills/funifier/references/create-notification.md +75 -0
  154. package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
  155. package/skills/funifier/references/create-quiz.md +98 -0
  156. package/skills/funifier/references/create-scheduler.md +141 -0
  157. package/skills/funifier/references/create-story.md +636 -0
  158. package/skills/funifier/references/create-swap.md +95 -0
  159. package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
  160. package/skills/funifier/references/create-virtual-good.md +96 -0
  161. package/skills/funifier/references/create-webhook.md +72 -0
  162. package/skills/funifier/references/create-websocket.md +71 -0
  163. package/skills/funifier/references/create-widget.md +76 -0
  164. package/skills/funifier/references/debug.md +87 -0
  165. package/skills/funifier/references/help.md +81 -0
  166. package/skills/funifier/references/implement-frontend.md +106 -0
  167. package/skills/funifier/references/import-csv.md +75 -0
  168. package/skills/funifier/references/manage-player.md +82 -0
  169. package/skills/funifier/references/manage-team.md +76 -0
  170. package/skills/funifier/references/upload-file.md +91 -0
  171. package/skills/funifier-create-aggregate/SKILL.md +0 -127
  172. package/skills/funifier-create-challenge/SKILL.md +0 -88
  173. package/skills/funifier-create-custom-page/SKILL.md +0 -127
  174. package/skills/funifier-create-level/SKILL.md +0 -87
  175. package/skills/funifier-create-quiz/SKILL.md +0 -87
  176. package/skills/funifier-create-scheduler/SKILL.md +0 -127
  177. package/skills/funifier-create-virtual-good/SKILL.md +0 -87
  178. package/skills/funifier-debug/SKILL.md +0 -92
  179. package/skills/funifier-help/SKILL.md +0 -86
  180. package/skills/funifier-implement-frontend/SKILL.md +0 -90
  181. package/skills/funifier-index/SKILL.md +0 -58
@@ -1,132 +1,600 @@
1
- # Swap (Troca)
1
+ # `swap`
2
2
 
3
3
  **Acesso Studio:** `/studio/swap`
4
4
  **API Endpoint:** `/v3/swap`
5
+ **Coleções MongoDB:** `swap`, `swap_counter_offer`
5
6
 
6
- ## O que é
7
+ ---
7
8
 
8
- Negociação e troca de objetos ou pontos entre jogadores. Permite que jogadores façam ofertas públicas de troca, negociando objetos ou pontos. Outros jogadores podem aceitar ou propor novas condições (contra propostas), incentivando a colaboração e interação.
9
+ ## 1. Visão Geral
9
10
 
10
- ## Quando usar
11
+ O módulo `swap` implementa **troca peer-to-peer de saldos de gamificação entre jogadores (ou times)**. Um jogador (`seller`) publica uma oferta declarando o que entrega (`rewards`) e o que cobra em retorno (`requires`); outro jogador (`buyer`) adquire a troca pagando os `requires`. Há também um modo de **contra-proposta** (`acceptCounterOffer = true`): o vendedor publica apenas os `rewards` e deixa que interessados registrem contra-propostas (`offer`), escolhendo uma delas para fechar negócio.
11
12
 
12
- - Para criar mercados de troca (ex: figurinhas)
13
- - Para permitir negociação entre jogadores
14
- - Para incentivar interação social na gamificação
13
+ Não existe "carteira" persistida por jogador. Todo movimento de saldo é um lançamento na coleção `achievement` (o módulo `achievement` é a fonte da verdade dos totais). O `swap` apenas **gera lançamentos de débito e crédito** marcados com `extra.swap` / `extra.offer` / `extra.swap_role`, e delega o recálculo de saldo a `AchievementManager.updatePlayerStatus`.
15
14
 
16
- ## Dependências
15
+ Papel arquitetural:
17
16
 
18
- - **Virtual Good** e/ou **Point**: itens/pontos para troca devem existir
19
- - **Player**: jogadores devem estar cadastrados
17
+ - **Escrow via achievement negativo**: ao publicar uma troca, os `rewards` do vendedor são imediatamente debitados (lançamento `achievement` negativo marcado com `extra.swap`). Esse débito funciona como caução — se a troca for excluída antes de ser adquirida, os débitos são removidos (estorno).
18
+ - **Liquidação em duas etapas**: a troca não é atômica entre as duas partes. O comprador paga e recebe na operação `acquire`; o vendedor coleta seu pagamento em uma operação separada (`receive`). No modo contra-proposta o vendedor recebe no `accept` e o comprador coleta no `counter/offer/receive`.
19
+ - **Sem isolamento de identidade**: a camada REST resolve `me` para o jogador do token, mas **não exige** que o token seja o vendedor/comprador. Qualquer token autenticado pode operar trocas em nome de terceiros (apenas registra `extra.*_registered_by` quando o autor difere).
20
20
 
21
- ## Checklist de Configuração no Studio
21
+ Relação com outros módulos:
22
22
 
23
- - [ ] Verificar se jogadores possuem itens/pontos para troca
24
- - [ ] Configurar interface de marketplace para os jogadores
23
+ | Módulo | Uso real no código |
24
+ |---|---|
25
+ | `achievement` | Único destino de débitos/créditos. `evaluateRequirements` (elegibilidade) e `updatePlayerStatus` (recálculo de `player_status`). Manipulação direta da coleção via Jongo. |
26
+ | `player` / `team` | `PlayerManager.findById` e, em fallback, `TeamManager.findById` validam a existência de `seller`/`buyer`. Um time pode ser participante de uma troca. |
27
+ | `challenge` (`Requirement`) | `rewards`, `requires` e `offer` são listas de `Requirement` (mesma classe usada por desafios). |
28
+ | `trigger` | Eventos `before_create`/`after_create`, `before_delete`/`after_delete`, `before_win`/`after_win` e o evento customizado `before_acquire_validation`. |
25
29
 
26
- ## API Endpoints
30
+ O `swap` **não** dispara notificações nem webhooks próprios (diferente de `achievement`). As notificações só ocorrem se um `trigger` configurado as emitir.
27
31
 
28
- ### Listar Trocas
29
- **Método:** GET
30
- **Endpoint:** `/v3/database/swap`
32
+ ---
31
33
 
32
- ### Criar Troca
33
- **Método:** POST
34
- **Endpoint:** `/v3/swap`
34
+ ## 2. Arquitetura e Fluxos
35
35
 
36
- **Exemplo de Body:**
37
- ```json
38
- {
39
- "seller": "tom",
40
- "rewards": [
41
- {
42
- "total": 1,
43
- "type": 2,
44
- "item": "DTj7lVn"
36
+ ### 2.1 Classes envolvidas
37
+
38
+ | Classe | Caminho | Papel |
39
+ |---|---|---|
40
+ | `SwapRest` | `com.funifier.rest.v3.rest.SwapRest` | Controller REST v3 (`/v3/swap`). 8 métodos. |
41
+ | `SwapManager` | `com.funifier.engine.swap.SwapManager` | Toda a lógica de negócio. Métodos `synchronized`. Único consumidor: `SwapRest` (via `ManagerFactory.getSwapManager()`). |
42
+ | `Swap` | `com.funifier.engine.swap.Swap` | Documento raiz, coleção `swap`. |
43
+ | `SwapCounterOffer` | `com.funifier.engine.swap.SwapCounterOffer` | Documento de contra-proposta, coleção `swap_counter_offer`. |
44
+ | `Requirement` | `com.funifier.engine.challenge.Requirement` | Item de troca (`total`/`type`/`item`/`operation`/`period`). |
45
+ | `Achievement` | `com.funifier.engine.achievement.Achievement` | Lançamento de saldo gerado pelas operações. |
46
+
47
+ Não há `Repository`/`Dao` dedicado. A persistência é feita por Jongo direto sobre as coleções `swap`, `swap_counter_offer` e `achievement`. Não há scheduler/job que processe trocas — todo o fluxo é síncrono e disparado por chamadas REST.
48
+
49
+ ### 2.2 Os dois fluxos do módulo
50
+
51
+ Existem **dois fluxos mutuamente exclusivos**, decididos pelo campo `acceptCounterOffer` no momento do `insert`:
52
+
53
+ - **Fluxo direto** (`acceptCounterOffer` ausente/`false`): vendedor define `requires` (preço fixo). Comprador paga via `acquire`. Usa o campo `acquired`.
54
+ - **Fluxo de contra-proposta** (`acceptCounterOffer = true`): vendedor **não** define `requires`. Compradores registram `offer`. Vendedor escolhe uma via `accept`. Usa os campos `accepted` / `counterOffer`, **nunca** `acquired`.
55
+
56
+ #### Fluxo direto — pipeline
57
+
58
+ ```
59
+ [POST /v3/swap] → insert(swap, author)
60
+ ├─ valida seller/rewards/requires
61
+ ├─ before_create (trigger)
62
+ ├─ DEBITA rewards do seller (achievement negativo, extra.swap_role="seller")
63
+ ├─ salva swap
64
+ ├─ after_create (trigger)
65
+ └─ updatePlayerStatus(seller)
66
+
67
+ [POST /v3/swap/acquire] → buyerAcquire(id, buyer, author, extra)
68
+ ├─ valida swap não adquirido + buyer existe
69
+ ├─ before_acquire_validation (trigger)
70
+ ├─ evaluateRequirements(requires, buyer) → "insufficient_requirements" se faltar saldo
71
+ ├─ before_win (trigger)
72
+ ├─ DEBITA requires do buyer (extra.swap_role="buyer")
73
+ ├─ CREDITA rewards ao buyer (extra.swap_role="buyer")
74
+ ├─ salva swap (buyer, acquired)
75
+ ├─ after_win (trigger)
76
+ └─ updatePlayerStatus(buyer)
77
+
78
+ [POST /v3/swap/receive] → sellerReceive(id)
79
+ ├─ valida swap adquirido (acquired != null OU buyer != null)
80
+ ├─ CREDITA requires ao seller (extra.swap_role="seller")
81
+ ├─ before_delete (trigger)
82
+ ├─ REMOVE o documento swap
83
+ ├─ after_delete (trigger)
84
+ └─ updatePlayerStatus(seller)
85
+ ```
86
+
87
+ ### Fluxo direto — pipeline de troca
88
+
89
+ ```mermaid
90
+ flowchart LR
91
+ A[POST /v3/swap<br/>insert] -->|debita rewards do seller| B[(swap salvo)]
92
+ B --> C[POST /v3/swap/acquire<br/>buyerAcquire]
93
+ C -->|debita requires + credita rewards ao buyer| D[(swap.acquired set)]
94
+ D --> E[POST /v3/swap/receive<br/>sellerReceive]
95
+ E -->|credita requires ao seller| F[swap removido]
96
+ B -.->|antes de adquirir| G[DELETE /v3/swap/:id<br/>estorna débito do seller]
97
+ ```
98
+
99
+ #### Fluxo de contra-proposta — pipeline
100
+
101
+ ```
102
+ [POST /v3/swap (acceptCounterOffer:true)] → insert
103
+ └─ DEBITA rewards do seller (escrow). requires NÃO é exigido.
104
+
105
+ [POST /v3/swap/counter/offer] → counterOfferInsert(offer, author)
106
+ ├─ valida buyer/offer/swap
107
+ ├─ offer != rewards do swap → "swap_rewards_and_counter_offer_must_be_different"
108
+ ├─ evaluateRequirements(offer, buyer) → "insufficient_funds"
109
+ ├─ before_create (trigger, coleção swap_counter_offer)
110
+ ├─ DEBITA offer do buyer (extra.offer=offerId, extra.swap=swapId, extra.swap_role="buyer")
111
+ ├─ salva counter offer
112
+ ├─ after_create (trigger)
113
+ └─ updatePlayerStatus(buyer)
114
+
115
+ [POST /v3/swap/counter/offer/accept] → counterOfferSellerAccept(offerId, extra)
116
+ ├─ swap.requires := offer.offer ; swap.buyer := offer.buyer
117
+ ├─ swap.accepted := now ; swap.counterOffer := offerId
118
+ ├─ CREDITA requires(=offer) ao seller
119
+ ├─ ESTORNA débitos das OUTRAS contra-propostas (remove achievements extra.offer != aceito)
120
+ ├─ REMOVE todos os documentos counter offer do swap
121
+ ├─ salva swap
122
+ └─ updatePlayerStatus(seller) ⚠ nenhum trigger é disparado aqui
123
+
124
+ [POST /v3/swap/counter/offer/receive] → counterOfferBuyerReceive(swapId)
125
+ ├─ valida swap.accepted != null OU swap.buyer != null
126
+ ├─ CREDITA rewards ao buyer
127
+ ├─ before_delete (trigger)
128
+ ├─ REMOVE o documento swap
129
+ ├─ after_delete (trigger)
130
+ └─ updatePlayerStatus(buyer)
131
+ ```
132
+
133
+ ### Interação entre módulos — `acquire` (fluxo direto)
134
+
135
+ ```mermaid
136
+ sequenceDiagram
137
+ participant Buyer
138
+ participant SwapRest
139
+ participant SwapManager
140
+ participant AchievementManager
141
+ participant Mongo as MongoDB
142
+ Buyer->>SwapRest: POST /v3/swap/acquire {swap, buyer}
143
+ SwapRest->>SwapManager: buyerAcquire(id, buyer, author, extra)
144
+ SwapManager->>Mongo: find swap por _id
145
+ SwapManager->>AchievementManager: evaluateRequirements(requires, buyer)
146
+ alt saldo insuficiente
147
+ AchievementManager-->>SwapManager: false
148
+ SwapManager-->>SwapRest: restrictions=[insufficient_requirements]
149
+ SwapRest-->>Buyer: 401 UNAUTHORIZED
150
+ else saldo ok
151
+ SwapManager->>Mongo: salva achievements (debita requires, credita rewards)
152
+ SwapManager->>Mongo: salva swap (buyer, acquired)
153
+ SwapManager->>AchievementManager: updatePlayerStatus(buyer)
154
+ SwapManager-->>SwapRest: status=OK
155
+ SwapRest-->>Buyer: 200 OK
156
+ end
157
+ ```
158
+
159
+ ### 2.3 Características de concorrência e transação
160
+
161
+ - Todos os métodos de mutação de `SwapManager` são `synchronized` **a nível de instância**. Como existe uma instância de `SwapManager` por `ManagerFactory` (resolvida por `FrontController.getInstance(apiKey)`), a serialização é **por tenant** — operações de trocas de um mesmo tenant são serializadas; tenants diferentes correm em paralelo.
162
+ - **Não há transação MongoDB.** As operações salvam múltiplos `achievement` e o documento `swap` em chamadas separadas. Uma falha no meio (ex.: exceção após debitar mas antes de salvar o swap) deixa lançamentos órfãos. Não há rollback.
163
+
164
+ ---
165
+
166
+ ## 3. Estrutura dos Objetos
167
+
168
+ ### 3.1 `Swap` — documento raiz (coleção `swap`)
169
+
170
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
171
+ |---|---|---|---|---|
172
+ | `_id` | String | auto (`Guid.newShortGuid()`) | — | Gerado no servidor. Enviar `_id` no POST gera restrição `swap_id_not_allowed`. |
173
+ | `seller` | String | — | Sim | ID do jogador **ou** time que oferta. Aceita `"me"` (resolvido para o jogador do token). |
174
+ | `buyer` | String | null | Não (sistema) | Preenchido no `acquire` (direto) ou no `accept` (contra-proposta). |
175
+ | `rewards` | `Requirement[]` | `[]` | Sim | O que o vendedor entrega. Debitado do vendedor no `insert`. |
176
+ | `requires` | `Requirement[]` | `[]` | Sim no fluxo direto; **não exigido** se `acceptCounterOffer=true` | O que o comprador paga. No fluxo de contra-proposta é **sobrescrito** pela `offer` aceita. |
177
+ | `acceptCounterOffer` | Boolean | null | Não | `true` ativa o modo contra-proposta. |
178
+ | `extra` | Object | `{}` | Não | Atributos livres. O sistema grava `seller_registered_by` / `buyer_registered_by` quando o autor difere do participante. |
179
+ | `created` | Date | `now()` no insert | — | Data de criação. |
180
+ | `acquired` | Date | null | — | Data da aquisição (**somente fluxo direto**, set em `buyerAcquire`). |
181
+ | `accepted` | Date | null | — | Data do aceite de contra-proposta (**somente fluxo contra-proposta**, set em `counterOfferSellerAccept`). |
182
+ | `counterOffer` | String | null | — | ID da `SwapCounterOffer` aceita (set no `accept`). |
183
+
184
+ > Não há campos computados nem campos removidos silenciosamente no save de `Swap` — o documento é persistido como recebido (com `@JsonIgnoreProperties(ignoreUnknown=true)`, campos desconhecidos são descartados na desserialização). Não há campo de status textual: o "estado" é inferido pela presença de `acquired` / `accepted` / `buyer`.
185
+
186
+ ### 3.2 `SwapCounterOffer` — contra-proposta (coleção `swap_counter_offer`)
187
+
188
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
189
+ |---|---|---|---|---|
190
+ | `_id` | String | auto | — | Enviar `_id` gera `counter_offer_id_not_allowed`. |
191
+ | `swap` | String | — | Sim | ID do `swap` alvo. |
192
+ | `buyer` | String | — | Sim | Jogador que propõe. Aceita `"me"`. |
193
+ | `offer` | `Requirement[]` | `[]` | Sim | O que o comprador oferece pelos `rewards` do swap. Debitado no insert. |
194
+ | `created` | Date | `now()` | — | Data de criação. |
195
+ | `extra` | Object | `{}` | Não | Grava `buyer_registered_by` quando o autor difere. |
196
+
197
+ ### 3.3 `Requirement` — item de troca (subentidade de `rewards`/`requires`/`offer`)
198
+
199
+ | Campo | Tipo | Padrão | Usado pelo swap? | Descrição |
200
+ |---|---|---|---|---|
201
+ | `total` | int | 0 | Sim | Quantidade. `total == 0` é **ignorado** no débito/crédito. |
202
+ | `type` | int | 0 | Parcial | Ver enum abaixo. **Apenas 0, 1 e 2 são debitados/creditados.** |
203
+ | `item` | String | null | Sim | ID do ponto / item de catálogo / desafio. |
204
+ | `operation` | int | 0 | **Não** (ignorado pelo swap) | `0=VERIFY`, `1=DEDUCT`. Irrelevante no swap (ver §5). |
205
+ | `period` | String | null | Sim (na elegibilidade) | Expressão de período (ex.: `-5h;-0h`) aplicada ao calcular o saldo em `evaluateRequirements`. |
206
+ | `extra` | Object | `{}` | Não | Ignorado pelo swap. |
207
+ | `folder` | String | null | **Não** | Ignorado pelo swap. |
208
+ | `restrict` | boolean | false | **Não** | Ignorado pelo swap. |
209
+ | `perPlayer` | boolean | false | **Não** | Ignorado pelo swap. |
210
+
211
+ **Enum `type` (`Requirement` / `Achievement`):**
212
+
213
+ | Valor | Constante | Debitado/creditado pelo swap? |
214
+ |---|---|---|
215
+ | 0 | `TYPE_POINT` | ✅ Sim |
216
+ | 1 | `TYPE_CHALLENGE` | ✅ Sim |
217
+ | 2 | `TYPE_CATALOG_ITEM` | ✅ Sim |
218
+ | 3 | `TYPE_LEVEL` | ❌ Verificado na elegibilidade, mas **nunca** debitado/creditado |
219
+ | 4 | `TYPE_CROWN` | ❌ idem |
220
+ | 5 | `TYPE_LOTTERY` | ❌ idem |
221
+ | 6 | `TYPE_MYSTERY_BOX` | ❌ idem |
222
+ | 7 | `TYPE_CHARACTER_STAR_STAT` | ❌ idem |
223
+ | 50 | `TYPE_LOTTERY_TICKET` | ❌ idem |
224
+
225
+ > **Comportamento silencioso crítico:** os loops de débito/crédito em todos os métodos do `SwapManager` filtram por `type ∈ {0,1,2}`. Um `Requirement` de tipo 3–7/50 é **aceito** no documento e **conta na verificação de elegibilidade** (`evaluateRequirements` soma todos os tipos), mas **nenhum lançamento de saldo é gerado** para ele. Resultado: o jogador precisa possuir o item para passar na validação, porém não o perde nem o recebe na troca.
226
+
227
+ ### 3.4 Ciclo de vida do `Swap`
228
+
229
+ ```mermaid
230
+ stateDiagram-v2
231
+ [*] --> Publicado: insert (debita rewards do seller)
232
+ Publicado --> Removido: delete (estorna débito)
233
+
234
+ state "Fluxo direto" as Direto {
235
+ Publicado --> Adquirido: buyerAcquire (acquired set)
236
+ Adquirido --> Liquidado: sellerReceive (credita seller, remove swap)
45
237
  }
46
- ],
47
- "requires": [
48
- {
49
- "total": 2,
50
- "type": 0,
51
- "item": "coin"
238
+
239
+ state "Fluxo contra-proposta" as Counter {
240
+ Publicado --> Aceito: counterOfferSellerAccept (accepted+buyer set, credita seller)
241
+ Aceito --> Coletado: counterOfferBuyerReceive (credita buyer, remove swap)
52
242
  }
53
- ]
54
- }
243
+
244
+ Liquidado --> [*]
245
+ Coletado --> [*]
246
+ Removido --> [*]
55
247
  ```
56
248
 
57
- ### Deletar Troca
58
- **Método:** DELETE
59
- **Endpoint:** `/v3/swap/:id`
249
+ > O documento `swap` **deixa de existir** ao final de ambos os fluxos (`sellerReceive` / `counterOfferBuyerReceive` removem o documento). Não há estado "concluído" persistido — uma troca finalizada simplesmente some da coleção. O histórico permanece apenas nos lançamentos `achievement`.
250
+
251
+ ### 3.5 Marcadores gravados no `Achievement`
252
+
253
+ Cada débito/crédito gerado pelo swap grava no `Achievement.extra`:
254
+
255
+ | Marcador | Onde | Significado |
256
+ |---|---|---|
257
+ | `extra.swap` | Todos | ID do `swap` que originou o lançamento. Usado para estorno em `delete`. |
258
+ | `extra.offer` | Contra-proposta | ID da `SwapCounterOffer`. Usado para estorno em `counterOfferDelete` e na limpeza do `accept`. |
259
+ | `extra.swap_role` | Todos | `"seller"` ou `"buyer"` — identifica de qual lado foi o movimento. |
260
+
261
+ ---
262
+
263
+ ## 4. Endpoints
264
+
265
+ Todos exigem `Authorization: Bearer <token>`. Respostas seguem o mesmo envelope: `{ status, restrictions[], achievements[], swap|offer }`. `status="OK"` → HTTP 200; `status="UNAUTHORIZED"` (qualquer restrição) → HTTP 401. Campos `null` são removidos da resposta (`JsonUtil.toJsonRemoveNullFields`).
60
266
 
61
- ### Realizar Troca (Adquirir)
62
- **Método:** POST
63
- **Endpoint:** `/v3/swap/acquire`
267
+ ### `POST /v3/swap` — criar troca
268
+
269
+ | Aspecto | Detalhe |
270
+ |---|---|
271
+ | Finalidade | Publica uma oferta e debita os `rewards` do vendedor (escrow). |
272
+ | Body | Objeto `Swap`. `_id` proibido. |
273
+ | `me` | `seller="me"` → jogador do token. |
274
+
275
+ **Comportamento real:** valida que `seller`, `rewards` e (no fluxo direto) `requires` estão preenchidos; que `requires` não é idêntico a `rewards` (mesmo `item`+`total`+`type`); e que o vendedor possui os `rewards` ofertados (`evaluateRequirements`). Em seguida debita os `rewards`.
64
276
 
65
- **Exemplo de Body:**
66
277
  ```json
67
278
  {
68
- "buyer": "jerry",
69
- "swap": "650c67f88325771ffaa78a24"
279
+ "seller": "tom",
280
+ "rewards": [ { "total": 1, "type": 2, "item": "DTj7lVn" } ],
281
+ "requires": [ { "total": 2, "type": 0, "item": "coin" } ]
70
282
  }
71
283
  ```
72
284
 
73
- ### Receber Recompensas da Troca
74
- **Método:** POST
75
- **Endpoint:** `/v3/swap/receive`
285
+ ### `DELETE /v3/swap/{id}` excluir troca
286
+
287
+ Só remove se `acquired == null && buyer == null` (troca ainda não negociada); caso contrário retorna `swap_have_being_acquired`. Ao remover, **estorna** os achievements com `extra.swap == id` (incluindo o débito de escrow do vendedor) e dispara `before_delete`/`after_delete`.
288
+
289
+ ### `POST /v3/swap/acquire` — adquirir (fluxo direto)
290
+
291
+ | Aspecto | Detalhe |
292
+ |---|---|
293
+ | Body | `{ swap, buyer, extra? }` |
294
+ | `me` | `buyer="me"` → jogador do token. |
295
+
296
+ Valida que a troca existe e não foi adquirida, e que o comprador possui os `requires`. Debita `requires` do comprador, credita `rewards` ao comprador, marca `buyer`/`acquired`. Dispara `before_acquire_validation` → `before_win` → `after_win`.
76
297
 
77
- **Exemplo de Body:**
78
298
  ```json
79
- {
80
- "swap": "650c67f88325771ffaa78a24"
81
- }
299
+ { "buyer": "jerry", "swap": "650c67f88325771ffaa78a24" }
300
+ ```
301
+
302
+ ### `POST /v3/swap/receive` — vendedor coleta (fluxo direto)
303
+
304
+ | Aspecto | Detalhe |
305
+ |---|---|
306
+ | Body | `{ swap }` |
307
+
308
+ Credita os `requires` ao vendedor e **remove** o documento `swap`. ⚠ Não valida que o token é o vendedor. ⚠ Ver §7/§8 sobre uso indevido no fluxo de contra-proposta (duplo crédito).
309
+
310
+ ```json
311
+ { "swap": "650c67f88325771ffaa78a24" }
82
312
  ```
83
313
 
84
- ### Listar Contra Propostas
85
- **Método:** POST
86
- **Endpoint:** `/v3/database/swap_counter_offer/aggregate`
314
+ ### `POST /v3/swap/counter/offer` — criar contra-proposta
315
+
316
+ | Aspecto | Detalhe |
317
+ |---|---|
318
+ | Body | Objeto `SwapCounterOffer` (`{ swap, buyer, offer[] }`). |
319
+ | `me` | `buyer="me"` → jogador do token. |
87
320
 
88
- ### Criar Contra Proposta
89
- **Método:** POST
90
- **Endpoint:** `/v3/swap/counter/offer`
321
+ Valida `buyer`/`offer`/`swap`, que `offer` difere dos `rewards` do swap, e que o comprador possui a `offer`. Debita a `offer` do comprador. **Não verifica** se o swap tem `acceptCounterOffer=true` (ver §7).
91
322
 
92
- **Exemplo de Body:**
93
323
  ```json
94
324
  {
95
325
  "swap": "650c683c8325771ffaa78a3f",
96
326
  "buyer": "jerry",
97
- "offer": [
98
- {
99
- "total": 1,
100
- "type": 0,
101
- "item": "coin"
102
- }
103
- ]
327
+ "offer": [ { "total": 1, "type": 0, "item": "coin" } ]
104
328
  }
105
329
  ```
106
330
 
107
- ### Deletar Contra Proposta
108
- **Método:** DELETE
109
- **Endpoint:** `/v3/swap/counter/offer/:id`
331
+ ### `POST /v3/swap/counter/offer/accept` — vendedor aceita
332
+
333
+ | Aspecto | Detalhe |
334
+ |---|---|
335
+ | Body | `{ offer, extra? }` (`offer` = `_id` da contra-proposta) |
110
336
 
111
- ### Aceitar Contra Proposta
112
- **Método:** POST
113
- **Endpoint:** `/v3/swap/counter/offer/accept`
337
+ Sobrescreve `swap.requires` com a `offer` aceita, credita a `offer` ao vendedor, **estorna os débitos das demais contra-propostas**, remove todas as contra-propostas do swap e marca `accepted`/`buyer`/`counterOffer`. ⚠ **Não dispara nenhum trigger** e **não valida** que o token é o vendedor.
114
338
 
115
- **Exemplo de Body:**
116
339
  ```json
340
+ { "offer": "650c69aa8325771ffaa78b1b" }
341
+ ```
342
+
343
+ ### `POST /v3/swap/counter/offer/receive` — comprador coleta (contra-proposta)
344
+
345
+ | Aspecto | Detalhe |
346
+ |---|---|
347
+ | Body | `{ swap }` (ID do **swap**, não da offer) |
348
+
349
+ Credita os `rewards` ao comprador (`swap.buyer`) e remove o documento `swap`. Dispara `before_delete`/`after_delete`.
350
+
351
+ ### `DELETE /v3/swap/counter/offer/{id}` — excluir contra-proposta
352
+
353
+ Estorna os achievements com `extra.offer == id` (devolve a `offer` debitada) e remove a contra-proposta. Dispara `before_delete`/`after_delete`. Não há verificação de que a troca já foi aceita.
354
+
355
+ ### Leitura (listagem)
356
+
357
+ **Não existe `GET /v3/swap`.** A leitura é feita pelos endpoints genéricos de banco:
358
+
359
+ - `GET /v3/database/swap` — lista trocas.
360
+ - `POST /v3/database/swap/aggregate` — agregação sobre trocas.
361
+ - `GET /v3/database/swap_counter_offer` / `POST /v3/database/swap_counter_offer/aggregate` — contra-propostas.
362
+
363
+ Estes endpoints são servidos pelo módulo `database` (CRUD genérico), não pelo `SwapManager`. Não há a filtragem por elegibilidade nem a randomização que o comentário legado do código menciona (ver §7).
364
+
365
+ ---
366
+
367
+ ## 5. Regras de Negócio
368
+
369
+ Regras presentes no código e ausentes do schema:
370
+
371
+ 1. **Escrow imediato.** Publicar uma troca debita os `rewards` do vendedor na hora (`insert`). O vendedor precisa possuí-los — senão `insufficient_rewards`.
372
+ 2. **`rewards` ≠ `requires`.** Se algum item coincidir em `item`+`total`+`type`, retorna `rewards_and_requires_must_be_different`. Análogo para contra-proposta vs `rewards` (`swap_rewards_and_counter_offer_must_be_different`).
373
+ 3. **Elegibilidade por soma de saldo.** `evaluateRequirements` calcula `sumTotalRewards(player, type, item, period)` e exige `saldo >= total` para **cada** requisito, **de todos os tipos** (não só 0/1/2). Aceita expressão de `period` por requisito.
374
+ 4. **`operation` é ignorada no swap.** O swap usa a sobrecarga `evaluateRequirements(all, player, jongo)`, que verifica saldo de todo requisito independentemente de `operation`. Marcar um item como `VERIFY` ou `DEDUCT` não muda nada — todo `requires`/`offer` é sempre debitado de fato (o débito real depende só de `type ∈ {0,1,2}` e `total != 0`).
375
+ 5. **`total == 0` é no-op.** Requisitos com `total` zero passam na validação mas não geram lançamento.
376
+ 6. **Normalização de sinal.** Débitos são forçados a negativo (`(total>0)?-total:total`); créditos forçados a positivo. Enviar `total` negativo em `rewards` não inverte a operação — o sinal é sempre normalizado pela etapa.
377
+ 7. **Participante pode ser time.** `seller`/`buyer` são validados primeiro como `Player`, depois como `Team`. Um time pode participar de trocas.
378
+ 8. **Exclusão só antes da negociação** (`delete`): `acquired == null && buyer == null`.
379
+ 9. **Contra-proposta sobrescreve `requires`.** No `accept`, `swap.requires := offer.offer`. A partir daí o "preço" do swap passa a ser a oferta aceita.
380
+ 10. **Estorno seletivo no aceite.** Ao aceitar uma contra-proposta, os débitos das **outras** contra-propostas são removidos (estorno) via `{extra.swap:#, extra.swap_role:"buyer", extra.offer:{$ne:#}}`. Mas só o `player_status` do **vendedor** é recalculado — os saldos dos demais compradores ficam consistentes apenas eventualmente (ver §9).
381
+ 11. **Multi-tenant por database.** O isolamento entre organizações é o database Jongo resolvido por `apiKey` (`FrontController.getInstance(apiKey)`). Não há campo de tenant nos documentos.
382
+
383
+ ---
384
+
385
+ ## 6. Comportamentos Automáticos
386
+
387
+ | Comportamento | Trigger / origem | Impacto | Persistência |
388
+ |---|---|---|---|
389
+ | Débito de escrow do vendedor | `insert` | Achievement negativo `extra.swap`, `swap_role="seller"` | Coleção `achievement` |
390
+ | Débito da contra-proposta | `counterOfferInsert` | Achievement negativo `extra.offer`+`extra.swap` | `achievement` |
391
+ | Débito do comprador + crédito de rewards | `buyerAcquire` | 2 lançamentos `swap_role="buyer"` | `achievement` |
392
+ | Crédito ao vendedor | `sellerReceive` / `counterOfferSellerAccept` | Achievement positivo `swap_role="seller"` | `achievement` |
393
+ | Crédito ao comprador | `counterOfferBuyerReceive` | Achievement positivo `swap_role="buyer"` | `achievement` |
394
+ | Estorno ao excluir | `delete` (`extra.swap`), `counterOfferDelete` (`extra.offer`) | Remove lançamentos | `achievement` |
395
+ | Estorno das demais ofertas | `counterOfferSellerAccept` | Remove débitos das contra-propostas não aceitas | `achievement` |
396
+ | Recálculo de `player_status` | `updatePlayerStatus(player)` ao fim de cada mutação bem-sucedida | Atualiza snapshot e pode gerar achievements derivados (ex.: level-up) | `player_status` + `achievement` |
397
+ | Triggers de domínio | Ver tabela abaixo | Permite hooks Groovy/Java; podem **adicionar restrições** que abortam a operação | Conforme o trigger |
398
+
399
+ ### Eventos de trigger por operação
400
+
401
+ | Operação | Eventos disparados (na ordem) |
402
+ |---|---|
403
+ | `insert` | `before_create` → (débitos) → `after_create` |
404
+ | `delete` | `before_delete` → (estorno) → `after_delete` |
405
+ | `buyerAcquire` | `before_acquire_validation` → `before_win` → (débito/crédito) → `after_win` |
406
+ | `sellerReceive` | (crédito) → `before_delete` → (remove) → `after_delete` |
407
+ | `counterOfferInsert` | `before_create` → (débito) → `after_create` |
408
+ | `counterOfferDelete` | `before_delete` → (estorno) → `after_delete` |
409
+ | `counterOfferSellerAccept` | **nenhum** |
410
+ | `counterOfferBuyerReceive` | (crédito) → `before_delete` → (remove) → `after_delete` |
411
+
412
+ > Os triggers `before_create` / `before_win` podem inserir entradas em `restrictions` (via `TriggerContext`). Se o fizerem, a operação é abortada antes da persistência. `before_acquire_validation` roda **antes** da checagem de saldo do comprador.
413
+
414
+ ### Fluxo de behaviors automáticos no `insert`
415
+
416
+ ```mermaid
417
+ flowchart TD
418
+ A[insert] --> B{validações de schema}
419
+ B -->|falha| Z[restrictions → 401]
420
+ B -->|ok| C[before_create trigger]
421
+ C --> D{trigger adicionou restrição?}
422
+ D -->|sim| Z
423
+ D -->|não| E[debita rewards do seller]
424
+ E --> F[salva swap]
425
+ F --> G[after_create trigger]
426
+ G --> H[updatePlayerStatus seller]
427
+ H --> I[200 OK]
428
+ ```
429
+
430
+ ---
431
+
432
+ ## 7. Suportado vs NÃO Suportado
433
+
434
+ ### ✅ Suportado
435
+
436
+ - Troca direta com preço fixo (`rewards` por `requires`).
437
+ - Modo contra-proposta (`acceptCounterOffer`).
438
+ - Débito de escrow, estorno na exclusão e estorno das ofertas perdedoras.
439
+ - Participação de **player ou team** como `seller`/`buyer`.
440
+ - Resolução de `"me"` para o jogador do token em `insert`/`acquire`/`counterOfferInsert`.
441
+ - Elegibilidade por saldo com expressão de `period`.
442
+ - Triggers de domínio (`before/after_create`, `before/after_delete`, `before/after_win`, `before_acquire_validation`).
443
+ - Tipos de saldo movimentados: **ponto (0), challenge (1), catalog_item (2)**.
444
+
445
+ ### ❌ NÃO Suportado / comportamento divergente
446
+
447
+ - **`GET /v3/swap` não existe.** O comentário legado no cabeçalho de `Swap.java`/`SwapManager.java` ("GET swap: filtrar por aqueles que o jogador tem requisitos para comprar, e trazer itens aleatórios") **nunca foi implementado**. Leitura só via `/v3/database/swap`.
448
+ - **Tipos 3–7 e 50** (`level`, `crown`, `lottery`, `mystery_box`, `character_star_stat`, `lottery_ticket`): aceitos no documento e **verificados** na elegibilidade, mas **nunca debitados nem creditados**. Trocar esses tipos não move saldo.
449
+ - **`Requirement.operation` é ignorado** no contexto de swap (todo requisito é tratado como verificação + débito incondicional por tipo).
450
+ - **`Requirement.folder` / `restrict` / `perPlayer`** são aceitos mas **ignorados**.
451
+ - **`counterOfferInsert` não valida `acceptCounterOffer`** — é possível criar contra-proposta contra uma troca de preço fixo (que tem `requires` definidos). A oferta seria debitada e ficaria "presa" até `accept`/`delete`.
452
+ - **`counterOfferSellerAccept` não dispara triggers** — diferente de toda outra mutação. Hooks `after_win`/notificações não rodam no aceite.
453
+ - **Sem atomicidade entre as partes.** A liquidação é em duas etapas separadas; não há garantia de que ambos os lados completem.
454
+ - **Sem transação MongoDB.** Falha no meio de uma operação deixa lançamentos órfãos sem rollback.
455
+ - **Bug de duplo crédito (`sellerReceive` no fluxo de contra-proposta):** após `accept`, o swap fica com `acquired == null` e `buyer != null`. O gate de `sellerReceive` é `acquired == null && buyer == null` (AND) → a restrição **não dispara**. Logo `sellerReceive` prossegue, credita o vendedor **uma segunda vez** com `requires` (= a `offer` já creditada no `accept`) e **remove o swap**, impedindo o comprador de coletar via `counter/offer/receive`. Não chame `receive` em trocas de contra-proposta.
456
+ - **NPE (HTTP 500) com entidades inexistentes**, em vez de restrição limpa:
457
+ - `sellerReceive(id)`: se o swap não existe, `swap.seller` é acessado sem guarda → NPE.
458
+ - `counterOfferInsert(offer)`: se `offer.swap` aponta para swap inexistente, `find()` retorna null e `swap.rewards` é acessado sem guarda → NPE.
459
+ - `counterOfferSellerAccept(id)`: se a offer não existe, `offer.swap` é acessado sem guarda → NPE.
460
+ - `counterOfferBuyerReceive(swapId)`: se o swap não existe, `swap.buyer` é acessado sem guarda → NPE.
461
+
462
+ ---
463
+
464
+ ## 8. Segurança e Permissões
465
+
466
+ - **Autenticação:** Bearer token em todos os endpoints. `getPlayerFromTokenIfExist()` extrai o jogador do token (pode ser `null` em tokens administrativos/sem player).
467
+ - **Autorização ausente entre participantes:**
468
+ - `acquire` **não exige** que o token seja o `buyer` — qualquer token pode adquirir em nome de outro jogador.
469
+ - `receive` **não exige** que o token seja o `seller`.
470
+ - `counterOfferSellerAccept` **não exige** que o token seja o vendedor do swap — qualquer token pode aceitar uma contra-proposta alheia.
471
+ - A única rastreabilidade é `extra.seller_registered_by` / `extra.buyer_registered_by`, gravado quando o autor difere do participante. **Não é um controle de acesso**, apenas auditoria.
472
+ - **Isolamento multi-tenant:** por database, via `apiKey` (`FrontController.getInstance(apiKey)`). Não há campo de tenant nos documentos; o isolamento depende inteiramente do roteamento de conexão.
473
+ - **Superfície de indisponibilidade (DoS leve):** os NPEs do §7 permitem que um cliente provoque HTTP 500 com IDs inexistentes. Não há injeção: as queries Jongo usam binding parametrizado (`"{_id:#}", id`), não concatenação de string.
474
+ - **Concorrência:** mutações são `synchronized` por instância (por tenant). Não há proteção contra reentrância entre tenants distintos (esperado).
475
+
476
+ ---
477
+
478
+ ## 9. Observabilidade e Troubleshooting
479
+
480
+ Como não há campo de status nem `GET /v3/swap`, o diagnóstico é feito sobre as coleções `swap`, `swap_counter_offer` e `achievement`.
481
+
482
+ **Estado de uma troca:**
483
+ ```
484
+ GET /v3/database/swap?strategy=AGGREGATE (ou)
485
+ POST /v3/database/swap/aggregate
486
+ [ { "$match": { "_id": "650c67f88325771ffaa78a24" } } ]
487
+ ```
488
+ Interprete:
489
+ - `acquired != null` → adquirida (fluxo direto), aguardando `receive` do vendedor.
490
+ - `accepted != null` → contra-proposta aceita, aguardando `counter/offer/receive` do comprador.
491
+ - `buyer == null && acquired == null && accepted == null` → publicada, ainda disponível.
492
+ - Documento ausente → troca finalizada (removida) **ou** nunca existiu.
493
+
494
+ **Lançamentos de uma troca (auditoria de saldo):**
495
+ ```
496
+ POST /v3/database/achievement/aggregate
497
+ [ { "$match": { "extra.swap": "650c67f88325771ffaa78a24" } },
498
+ { "$group": { "_id": { "player": "$player", "role": "$extra.swap_role", "item": "$item" },
499
+ "total": { "$sum": "$total" } } } ]
500
+ ```
501
+
502
+ **Contra-propostas de um swap:**
503
+ ```
504
+ POST /v3/database/swap_counter_offer/aggregate
505
+ [ { "$match": { "swap": "650c683c8325771ffaa78a3f" } } ]
506
+ ```
507
+
508
+ ### Erros comuns e causas
509
+
510
+ | Sintoma | Causa provável |
511
+ |---|---|
512
+ | `insufficient_rewards` no insert | Vendedor não possui o saldo dos `rewards` ofertados (escrow falha). |
513
+ | `insufficient_requirements` / `insufficient_funds` | Comprador sem saldo para `requires`/`offer`. |
514
+ | `rewards_and_requires_must_be_different` | `requires` idêntico a `rewards` (item+total+type). |
515
+ | `swap_have_being_acquired` no delete/acquire | Troca já tem `buyer`/`acquired`. |
516
+ | HTTP 500 sem `restrictions` | ID inexistente atingiu um dos NPEs do §7. Verifique se o `swap`/`offer` existe antes de chamar. |
517
+ | Saldo "sumiu" sem troca concluir | Escrow debitou no insert/counterOfferInsert e a troca segue pendente. Estorna ao excluir. |
518
+ | Comprador não consegue coletar contra-proposta | O vendedor chamou `receive` (não `counter/offer/receive`) e o swap foi removido — ver bug de duplo crédito (§7). |
519
+ | Saldo de comprador perdedor desatualizado após accept | `updatePlayerStatus` só roda para o vendedor no `accept`; o snapshot do perdedor atualiza eventualmente. |
520
+
521
+ ---
522
+
523
+ ## 10. Exemplos Práticos
524
+
525
+ ### Exemplo mínimo (troca direta)
526
+
527
+ ```http
528
+ POST /v3/swap
529
+ Authorization: Bearer <token>
530
+
117
531
  {
118
- "offer": "650c69aa8325771ffaa78b1b"
532
+ "seller": "tom",
533
+ "rewards": [ { "total": 1, "type": 2, "item": "card_dragon" } ],
534
+ "requires": [ { "total": 10, "type": 0, "item": "coin" } ]
119
535
  }
120
536
  ```
537
+ Em seguida o comprador adquire e o vendedor coleta:
538
+ ```http
539
+ POST /v3/swap/acquire { "buyer": "jerry", "swap": "<id>" }
540
+ POST /v3/swap/receive { "swap": "<id>" }
541
+ ```
542
+
543
+ ### Exemplo avançado (contra-proposta com `me` e período)
544
+
545
+ ```http
546
+ POST /v3/swap
547
+ Authorization: Bearer <token-do-tom>
548
+ {
549
+ "seller": "me",
550
+ "acceptCounterOffer": true,
551
+ "rewards": [ { "total": 1, "type": 2, "item": "card_phoenix" } ],
552
+ "extra": { "provider": "marketplace" }
553
+ }
554
+ ```
555
+ ```http
556
+ POST /v3/swap/counter/offer
557
+ Authorization: Bearer <token-do-jerry>
558
+ {
559
+ "swap": "<swapId>",
560
+ "buyer": "me",
561
+ "offer": [ { "total": 50, "type": 0, "item": "coin", "period": "-720h;-0h" } ]
562
+ }
563
+ ```
564
+ ```http
565
+ POST /v3/swap/counter/offer/accept { "offer": "<offerId>" } # vendedor aceita
566
+ POST /v3/swap/counter/offer/receive { "swap": "<swapId>" } # comprador coleta
567
+ ```
568
+
569
+ ### Anti-pattern (o que NÃO fazer)
570
+
571
+ ```http
572
+ # ❌ ERRADO: usar /receive no fluxo de contra-proposta
573
+ POST /v3/swap/counter/offer/accept { "offer": "<offerId>" } # vendedor já é creditado aqui
574
+ POST /v3/swap/receive { "swap": "<swapId>" } # credita o vendedor DE NOVO e
575
+ # remove o swap → comprador não coleta
576
+ ```
577
+ Por quê: no fluxo de contra-proposta o vendedor é creditado no `accept`. Chamar `receive` aciona o bug de duplo crédito (§7). Use sempre `counter/offer/receive`.
578
+
579
+ ```http
580
+ # ❌ ERRADO: trocar um nível esperando que mude o saldo
581
+ { "seller": "tom",
582
+ "rewards": [ { "total": 1, "type": 3, "item": "level_gold" } ],
583
+ "requires": [ { "total": 5, "type": 0, "item": "coin" } ] }
584
+ ```
585
+ Por quê: `type:3 (LEVEL)` não é debitado/creditado (§3.3). O vendedor precisa "ter" o nível para passar na validação do escrow, mas o comprador nunca o recebe.
121
586
 
122
- ### Receber Recompensas de Contra Proposta
123
- **Método:** POST
124
- **Endpoint:** `/v3/swap/counter/offer/receive`
587
+ ---
125
588
 
126
- ## Validações e Testes
589
+ ## Checklist de Configuração
127
590
 
128
- - [ ] Troca aparece na lista de swaps
129
- - [ ] Comprador consegue adquirir a troca
130
- - [ ] Vendedor recebe recompensas após troca
131
- - [ ] Contra proposta é criada e listada corretamente
132
- - [ ] Aceitar contra proposta funciona
591
+ - [ ] `seller` existe (player **ou** team) antes de criar.
592
+ - [ ] Vendedor possui os `rewards` ofertados (senão `insufficient_rewards`).
593
+ - [ ] `rewards` e `requires` usam apenas `type ∈ {0,1,2}` se você espera movimento real de saldo.
594
+ - [ ] `rewards` `requires` (item+total+type) senão a criação é recusada.
595
+ - [ ] Fluxo escolhido é consistente: **direto** usa `requires` + `acquire` + `receive`; **contra-proposta** usa `acceptCounterOffer:true` + `counter/offer` + `accept` + `counter/offer/receive`.
596
+ - [ ] ⚠ Nunca chamar `/v3/swap/receive` em troca de contra-proposta (duplo crédito + remoção).
597
+ - [ ] Validar IDs (`swap`/`offer`) no cliente antes de chamar — IDs inexistentes podem gerar HTTP 500 (§7).
598
+ - [ ] Lembrar do escrow: publicar uma troca já debita o vendedor; excluir antes da negociação estorna.
599
+ - [ ] Não confiar em `Requirement.operation`/`folder`/`restrict`/`perPlayer` — são ignorados pelo swap.
600
+ - [ ] Autorização de identidade (token = participante) deve ser garantida fora do módulo — o swap não a impõe.