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.
- package/.cursor/rules/funifier.mdc +38 -41
- package/.github/copilot-instructions.md +38 -41
- package/AGENTS.md +56 -49
- package/README.md +40 -22
- package/datasource-funifier-docs/.coverage.json +326 -0
- package/datasource-funifier-docs/.validation.json +593 -0
- package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
- package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
- package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
- package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
- package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
- package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
- package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
- package/datasource-funifier-docs/knowledge/index.md +4 -1
- package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
- package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
- package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
- package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
- package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
- package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
- package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
- package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
- package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
- package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
- package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
- package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
- package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
- package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
- package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
- package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
- package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
- package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
- package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
- package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
- package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
- package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
- package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
- package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
- package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
- package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
- package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
- package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
- package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
- package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
- package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
- package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
- package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
- package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
- package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
- package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
- package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
- package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
- package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
- package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
- package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +42 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/init.test.js +74 -3
- package/dist/cli/init.test.js.map +1 -1
- package/dist/cli/persona.d.ts +3 -0
- package/dist/cli/persona.d.ts.map +1 -0
- package/dist/cli/persona.js +25 -0
- package/dist/cli/persona.js.map +1 -0
- package/dist/mcp/bundle.js +119 -93
- package/dist/mcp/index.js +2 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/resources/documentation.d.ts +1 -1
- package/dist/mcp/resources/documentation.d.ts.map +1 -1
- package/dist/mcp/resources/documentation.js +39 -3
- package/dist/mcp/resources/documentation.js.map +1 -1
- package/dist/mcp/tools/connect.d.ts.map +1 -1
- package/dist/mcp/tools/connect.js +18 -8
- package/dist/mcp/tools/connect.js.map +1 -1
- package/dist/mcp/tools/database.d.ts.map +1 -1
- package/dist/mcp/tools/database.js +59 -47
- package/dist/mcp/tools/database.js.map +1 -1
- package/dist/mcp/tools/database.test.js +2 -2
- package/dist/mcp/tools/database.test.js.map +1 -1
- package/dist/mcp/tools/delete.d.ts.map +1 -1
- package/dist/mcp/tools/delete.js +13 -3
- package/dist/mcp/tools/delete.js.map +1 -1
- package/dist/mcp/tools/execute.d.ts.map +1 -1
- package/dist/mcp/tools/execute.js +20 -9
- package/dist/mcp/tools/execute.js.map +1 -1
- package/dist/mcp/tools/folder.d.ts.map +1 -1
- package/dist/mcp/tools/folder.js +22 -12
- package/dist/mcp/tools/folder.js.map +1 -1
- package/dist/mcp/tools/get.d.ts.map +1 -1
- package/dist/mcp/tools/get.js +16 -6
- package/dist/mcp/tools/get.js.map +1 -1
- package/dist/mcp/tools/index.d.ts +1 -1
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +3 -1
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/list.d.ts.map +1 -1
- package/dist/mcp/tools/list.js +38 -14
- package/dist/mcp/tools/list.js.map +1 -1
- package/dist/mcp/tools/logs.d.ts.map +1 -1
- package/dist/mcp/tools/logs.js +15 -5
- package/dist/mcp/tools/logs.js.map +1 -1
- package/dist/mcp/tools/save.d.ts.map +1 -1
- package/dist/mcp/tools/save.js +14 -4
- package/dist/mcp/tools/save.js.map +1 -1
- package/dist/mcp/tools/save.test.js +3 -3
- package/dist/mcp/tools/save.test.js.map +1 -1
- package/dist/mcp/tools/search-docs.d.ts +3 -0
- package/dist/mcp/tools/search-docs.d.ts.map +1 -0
- package/dist/mcp/tools/search-docs.js +102 -0
- package/dist/mcp/tools/search-docs.js.map +1 -0
- package/package.json +6 -2
- package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
- package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
- package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
- package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
- package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
- package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
- package/skills/funifier/SKILL.md +88 -0
- package/skills/funifier/references/configure-security.md +96 -0
- package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
- package/skills/funifier/references/create-aggregate.md +144 -0
- package/skills/funifier/references/create-challenge.md +116 -0
- package/skills/funifier/references/create-competition.md +98 -0
- package/skills/funifier/references/create-crossword.md +574 -0
- package/skills/funifier/references/create-custom-object.md +91 -0
- package/skills/funifier/references/create-custom-page.md +135 -0
- package/skills/funifier/references/create-folder.md +104 -0
- package/skills/funifier/references/create-lastmile.md +643 -0
- package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
- package/skills/funifier/references/create-level.md +94 -0
- package/skills/funifier/references/create-lottery.md +913 -0
- package/skills/funifier/references/create-mystery.md +769 -0
- package/skills/funifier/references/create-notification.md +75 -0
- package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
- package/skills/funifier/references/create-quiz.md +98 -0
- package/skills/funifier/references/create-scheduler.md +141 -0
- package/skills/funifier/references/create-story.md +636 -0
- package/skills/funifier/references/create-swap.md +95 -0
- package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
- package/skills/funifier/references/create-virtual-good.md +96 -0
- package/skills/funifier/references/create-webhook.md +72 -0
- package/skills/funifier/references/create-websocket.md +71 -0
- package/skills/funifier/references/create-widget.md +76 -0
- package/skills/funifier/references/debug.md +87 -0
- package/skills/funifier/references/help.md +81 -0
- package/skills/funifier/references/implement-frontend.md +106 -0
- package/skills/funifier/references/import-csv.md +75 -0
- package/skills/funifier/references/manage-player.md +82 -0
- package/skills/funifier/references/manage-team.md +76 -0
- package/skills/funifier/references/upload-file.md +91 -0
- package/skills/funifier-create-aggregate/SKILL.md +0 -127
- package/skills/funifier-create-challenge/SKILL.md +0 -88
- package/skills/funifier-create-custom-page/SKILL.md +0 -127
- package/skills/funifier-create-level/SKILL.md +0 -87
- package/skills/funifier-create-quiz/SKILL.md +0 -87
- package/skills/funifier-create-scheduler/SKILL.md +0 -127
- package/skills/funifier-create-virtual-good/SKILL.md +0 -87
- package/skills/funifier-debug/SKILL.md +0 -92
- package/skills/funifier-help/SKILL.md +0 -86
- package/skills/funifier-implement-frontend/SKILL.md +0 -90
- package/skills/funifier-index/SKILL.md +0 -58
|
@@ -1,132 +1,600 @@
|
|
|
1
|
-
#
|
|
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
|
-
|
|
7
|
+
---
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
## 1. Visão Geral
|
|
9
10
|
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
15
|
+
Papel arquitetural:
|
|
17
16
|
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
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
|
-
|
|
21
|
+
Relação com outros módulos:
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
**Método:** GET
|
|
30
|
-
**Endpoint:** `/v3/database/swap`
|
|
32
|
+
---
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
**Método:** POST
|
|
34
|
-
**Endpoint:** `/v3/swap`
|
|
34
|
+
## 2. Arquitetura e Fluxos
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
###
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
"
|
|
69
|
-
"
|
|
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
|
-
###
|
|
74
|
-
|
|
75
|
-
**
|
|
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
|
-
|
|
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
|
-
###
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
123
|
-
**Método:** POST
|
|
124
|
-
**Endpoint:** `/v3/swap/counter/offer/receive`
|
|
587
|
+
---
|
|
125
588
|
|
|
126
|
-
##
|
|
589
|
+
## Checklist de Configuração
|
|
127
590
|
|
|
128
|
-
- [ ]
|
|
129
|
-
- [ ]
|
|
130
|
-
- [ ]
|
|
131
|
-
- [ ]
|
|
132
|
-
- [ ]
|
|
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.
|