Sistema de Swap - Troca de Itens
Documentacao completa do sistema de Swap do ProCases. O sistema possui dois modos de operacao independentes:
- Swap entre usuarios (User-to-User): Criacao, aceite, recusa e cancelamento de propostas de troca de itens entre jogadores.
- Swap com o site (Site Swap via Waxpeer): O usuario entrega itens do inventario e recebe um item do marketplace Waxpeer com preco em tempo real.
Indice
- Modelo de Dados
- Diagrama de Fluxo
- Status do Inventario
- Sistema de Locks
- Endpoints da API
- Regras de Negocio
- Calculo de Precos (Site Swap)
- Prevencao de Deadlocks
- Edge Cases e Cenarios Criticos
- Exemplos Numericos
- Arquivos do Sistema
- Debugging e Troubleshooting
Modelo de Dados
Enum SwapStatus
enum SwapStatus {
PENDING // Aguardando aceite do outro usuario
ACCEPTED // Troca concluida com sucesso
DECLINED // Recusada pelo acceptor
CANCELLED // Cancelada pelo creator
EXPIRED // Expirou apos 24 horas sem aceite
}Localizacao: prisma/schema.prisma (linha 697)
Enum InventoryItemStatus
enum InventoryItemStatus {
AVAILABLE // Disponivel para uso
LOCKED // Bloqueado (saque pendente)
SOLD // Vendido por saldo
WITHDRAWN // Sacado via Steam
USED_IN_UPGRADE // Consumido em upgrade
USED_IN_BATTLE // Consumido em batalha
IN_SWAP // Reservado em proposta de swap ativa
}Localizacao: prisma/schema.prisma (linha 414)
Tabela Swap (User-to-User)
model Swap {
id BigInt @id
creatorId BigInt @map("creator_id")
acceptorId BigInt? @map("acceptor_id")
status SwapStatus @default(PENDING)
creatorValueCents BigInt @map("creator_value_cents")
acceptorValueCents BigInt? @map("acceptor_value_cents")
isPublic Boolean @default(false) @map("is_public")
message String?
expiresAt DateTime @map("expires_at")
createdAt DateTime @default(now()) @map("created_at")
completedAt DateTime? @map("completed_at")
creator User @relation("SwapCreator", fields: [creatorId], references: [id])
acceptor User? @relation("SwapAcceptor", fields: [acceptorId], references: [id])
items SwapItem[]
@@map("swaps")
@@index([creatorId, status])
@@index([acceptorId, status])
@@index([status, createdAt])
@@index([isPublic, status])
@@index([expiresAt])
}Localizacao: prisma/schema.prisma (linha 705)
Campos importantes:
| Campo | Tipo | Descricao |
|---|---|---|
id | BigInt (Snowflake) | Identificador unico gerado pelo SnowflakeService |
creatorId | BigInt | ID do usuario que criou a proposta |
acceptorId | BigInt? | ID do acceptor designado (null = swap publico) |
status | SwapStatus | Estado atual do swap |
creatorValueCents | BigInt | Soma dos valores dos itens do creator em centavos BRL |
acceptorValueCents | BigInt? | Soma dos valores dos itens do acceptor em centavos BRL (preenchido no aceite) |
isPublic | Boolean | Se true, qualquer usuario pode aceitar |
message | String? | Mensagem opcional do creator |
expiresAt | DateTime | Data de expiracao (createdAt + 24 horas) |
completedAt | DateTime? | Timestamp de quando o swap foi aceito/concluido |
Tabela SwapItem
model SwapItem {
id BigInt @id
swapId BigInt @map("swap_id")
inventoryItemId BigInt @map("inventory_item_id")
isCreatorItem Boolean @map("is_creator_item")
itemValueCents BigInt @map("item_value_cents")
swap Swap @relation(fields: [swapId], references: [id], onDelete: Cascade)
inventoryItem UserInventory @relation(fields: [inventoryItemId], references: [id])
@@map("swap_items")
@@index([swapId])
@@index([inventoryItemId])
}Localizacao: prisma/schema.prisma (linha 730)
Campos importantes:
| Campo | Tipo | Descricao |
|---|---|---|
id | BigInt (Snowflake) | Identificador unico |
swapId | BigInt | FK para o swap |
inventoryItemId | BigInt | FK para o UserInventory |
isCreatorItem | Boolean | true = item do creator, false = item do acceptor |
itemValueCents | BigInt | Snapshot do valor em centavos BRL no momento da criacao |
O campo isCreatorItem e fundamental para distinguir de qual lado da troca o item pertence. Na criacao do swap, os itens sao registrados com isCreatorItem = true. No aceite, os itens do acceptor sao registrados com isCreatorItem = false.
Tabela SiteSwap (Waxpeer)
model SiteSwap {
id BigInt @id
userId BigInt @map("user_id")
targetItemId BigInt @map("target_item_id")
targetItemValueCents BigInt @map("target_item_value_cents")
totalOfferedValueCents BigInt @map("total_offered_value_cents")
createdAt DateTime @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id])
targetItem Item @relation(fields: [targetItemId], references: [id])
items SiteSwapItem[]
@@map("site_swaps")
@@index([userId])
@@index([createdAt])
@@index([targetItemValueCents])
}Localizacao: prisma/schema.prisma (linha 745)
Campos importantes:
| Campo | Tipo | Descricao |
|---|---|---|
id | BigInt (Snowflake) | Identificador unico |
userId | BigInt | ID do usuario que fez o swap |
targetItemId | BigInt | FK para o Item que o usuario recebeu |
targetItemValueCents | BigInt | Valor do item alvo em centavos BRL (com margem dinamica via PriceMarginService) |
totalOfferedValueCents | BigInt | Soma dos itens oferecidos pelo usuario em centavos BRL |
Tabela SiteSwapItem
model SiteSwapItem {
id BigInt @id
siteSwapId BigInt @map("site_swap_id")
userInventoryId BigInt @map("user_inventory_id")
itemId BigInt @map("item_id")
itemValueCents BigInt @map("item_value_cents")
createdAt DateTime @default(now()) @map("created_at")
siteSwap SiteSwap @relation(fields: [siteSwapId], references: [id], onDelete: Cascade)
userInventory UserInventory @relation(fields: [userInventoryId], references: [id])
item Item @relation(fields: [itemId], references: [id])
@@map("site_swap_items")
@@index([siteSwapId])
@@index([userInventoryId])
}Localizacao: prisma/schema.prisma (linha 763)
Campos importantes:
| Campo | Tipo | Descricao |
|---|---|---|
id | BigInt (Snowflake) | Identificador unico |
siteSwapId | BigInt | FK para o SiteSwap |
userInventoryId | BigInt | FK para o item do inventario do usuario |
itemId | BigInt | FK para o Item base |
itemValueCents | BigInt | Snapshot do valor em centavos BRL |
Diagrama de Fluxo
Swap User-to-User
CRIACAO ACEITE
------- ------
Creator Backend Acceptor
| | |
|-- POST /swap --------->| |
| |-- Lock user:balance |
| |-- Valida itens |
| |-- Calcula totalValue |
| |-- Marca itens IN_SWAP |
| |-- Cria Swap PENDING |
| |-- Cria SwapItems |
|<-- Swap criado --------| |
| | |
| |<-- PUT /swap/:id/accept-|
| | |
| |-- Lock swap:{id} |
| |-- Lock user:balance (menor ID primeiro)
| |-- Lock user:balance (maior ID segundo)
| | |
| |-- Re-fetch swap (PENDING?)
| |-- Valida itens acceptor |
| |-- Calcula totalValue |
| | |
| |-- TRANSACTION BEGIN |
| | setAcceptor() |
| | Marca itens IN_SWAP |
| | Cria SwapItems |
| | Transfer creator->acceptor
| | Transfer acceptor->creator
| | Status = ACCEPTED |
| |-- TRANSACTION COMMIT |
| | |
| |-- Swap aceito --------->|Swap com Site (Waxpeer)
Usuario Backend Waxpeer
| | |
|-- POST /swap/site ---->| |
| |-- getItemPrice() ------>|
| |<-- priceCentsUsd -------|
| | |
| |-- Aplica margem dinamica|
| |-- Converte USD -> BRL |
| | |
| |-- Lock user:balance |
| |-- TRANSACTION BEGIN |
| | Valida itens |
| | Calcula total user |
| | Valida range valor |
| | Find/create item DB |
| | Marca itens IN_SWAP |
| | Cria inventory AVAILABLE
| | Cria SiteSwap |
| | Cria SiteSwapItems |
| | Credita refund (diff) |
| |-- TRANSACTION COMMIT |
| | |
| |-- setTimeout(3s) |
| | emitLiveDrop() |
| | |
|<-- resultado -----------| |Status do Inventario
Transicoes de Status dos Itens durante Swaps
SWAP USER-TO-USER:
Criar Swap:
Creator items: AVAILABLE --> IN_SWAP
Aceitar Swap:
Acceptor items: AVAILABLE --> IN_SWAP
Creator items: IN_SWAP --> AVAILABLE (novo dono: acceptor)
Acceptor items: IN_SWAP --> AVAILABLE (novo dono: creator)
Recusar Swap (Decline):
Creator items: IN_SWAP --> AVAILABLE (devolvidos ao creator)
Acceptor items: (nao afetados, nunca foram commitados)
Cancelar Swap (Cancel):
Creator items: IN_SWAP --> AVAILABLE (devolvidos ao creator)
Acceptor items: IN_SWAP --> AVAILABLE (devolvidos ao acceptor, se existirem)
SWAP COM SITE (WAXPEER):
Perform Site Swap:
User items: AVAILABLE --> IN_SWAP (consumidos)
Novo item: AVAILABLE (criado, acquiredFrom = 'Swap')Diferenca critica entre Cancel e Decline:
- Cancel: Executado pelo creator. Devolve itens de AMBOS os lados (creator e acceptor), caso o acceptor ja tenha adicionado itens.
- Decline: Executado pelo acceptor (ou qualquer usuario em swaps publicos). Devolve APENAS os itens do creator. O acceptor nunca chega a commitar itens no decline.
Sistema de Locks
O sistema de Swap utiliza distributed locks via Redlock para prevenir race conditions.
Configuracao do Redlock
// src/infrastructure/locks/lock.service.ts
this.redlock = new Redlock([client], {
driftFactor: 0.01,
retryCount: 10,
retryDelay: 200, // ms
retryJitter: 200, // ms
automaticExtensionThreshold: 500, // ms
});| Parametro | Valor | Descricao |
|---|---|---|
retryCount | 10 | Numero maximo de tentativas para adquirir o lock |
retryDelay | 200ms | Tempo base entre tentativas |
retryJitter | 200ms | Variacao aleatoria adicionada ao delay |
automaticExtensionThreshold | 500ms | Extensao automatica se o lock esta proximo de expirar |
Locks por Operacao
| Operacao | Lock(s) | TTL | Descricao |
|---|---|---|---|
| Create Swap | lock:user:balance:{userId} | 10s | Previne operacoes concorrentes no inventario do creator |
| Accept Swap | lock:swap:{swapId} + lock:user:balance:{firstId} + lock:user:balance:{secondId} | 5s + 10s + 10s | Lock triplo com ordenacao para prevenir deadlocks |
| Site Swap | lock:user:balance:{userId} | 10s | Previne operacoes concorrentes no inventario do usuario |
| Cancel Swap | Nenhum (Prisma transaction) | - | Apenas transaction do Prisma, sem distributed lock |
| Decline Swap | Nenhum (Prisma transaction) | - | Apenas transaction do Prisma, sem distributed lock |
Ordenacao de Locks no Accept (Prevencao de Deadlocks)
O accept-swap adquire tres locks em sequencia rigorosa. A ordenacao dos user balance locks e feita pelo userId em ordem crescente (ASC) para garantir que dois swaps simultaneos entre os mesmos usuarios nunca causem deadlock:
// src/application/use-cases/swap/accept-swap.use-case.ts
const [firstUserId, secondUserId] = [userId, swap.creatorId].sort((a, b) =>
a < b ? -1 : a > b ? 1 : 0,
);
return this.lockService.withLock(`swap:${swapId}`, async () => {
return this.lockService.withUserBalanceLock(firstUserId, async () => {
return this.lockService.withUserBalanceLock(secondUserId, async () => {
// operacao segura aqui
});
});
});Exemplo de prevencao:
Cenario: Usuario A (id=100) e Usuario B (id=200) tentam aceitar swaps um do outro ao mesmo tempo.
Sem ordenacao (DEADLOCK):
Thread 1: lock(user:100) --> tentando lock(user:200) [BLOCKED]
Thread 2: lock(user:200) --> tentando lock(user:100) [BLOCKED]
= DEADLOCKCom ordenacao (SEGURO):
Thread 1: lock(swap:X) --> lock(user:100) --> lock(user:200) [OK]
Thread 2: lock(swap:Y) --> tentando lock(user:100) [ESPERA] --> lock(user:200) [OK]
= SEM DEADLOCK (Thread 2 espera Thread 1 terminar)A regra e: sempre adquirir locks de usuario na ordem crescente de userId.
Endpoints da API
Todos os endpoints estao sob o prefixo /swap e requerem autenticacao (exceto GET /swap/targets).
Controller: src/presentation/controllers/swap.controller.ts
1. POST /swap - Criar Swap
Cria uma nova proposta de troca de itens. O swap pode ser privado (direcionado a um usuario especifico) ou publico (qualquer usuario pode aceitar).
Autenticacao: Obrigatoria (AuthGuard)
Use Case: src/application/use-cases/swap/create-swap.use-case.ts
Request
POST /swap
Content-Type: application/json
Cookie: sessionId=abc123
{
"inventoryItemIds": ["839201847382", "839201847383"],
"acceptorId": "738291038271",
"isPublic": false,
"message": "Troca justa, aceita ai"
}| Campo | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
inventoryItemIds | string[] | Sim | IDs dos itens do inventario (1 a 10 itens) |
acceptorId | string | Condicional | Obrigatorio se isPublic = false |
isPublic | boolean | Sim | Se true, qualquer usuario pode aceitar |
message | string | Nao | Mensagem opcional para o acceptor |
Validacoes
- Quantidade de itens: Minimo 1, maximo 10.
- Swap privado: Se
isPublic = false, o campoacceptorIde obrigatorio. - Auto-swap: O
acceptorIdnao pode ser igual aouserIddo creator. - Existencia dos itens: Todos os
inventoryItemIdsdevem existir no banco. - Propriedade: Todos os itens devem pertencer ao
userIdautenticado. - Disponibilidade: Todos os itens devem ter status
AVAILABLE.
Logica de Execucao
// create-swap.use-case.ts (simplificado)
async execute(userId, inventoryItemIds, acceptorId?, isPublic, message?) {
// 1. Validacoes de input
if (inventoryItemIds.length === 0 || inventoryItemIds.length > 10) {
throw new BadRequestException('Must select between 1 and 10 items');
}
if (!isPublic && !acceptorId) {
throw new BadRequestException('Private swaps must specify an acceptor');
}
if (acceptorId && acceptorId === userId) {
throw new BadRequestException('Cannot create swap with yourself');
}
// 2. Adquire lock do usuario
return this.lockService.withUserBalanceLock(userId, async () => {
// 3. Busca e valida cada item
const items = await Promise.all(
inventoryItemIds.map((id) => this.inventoryRepository.findById(id)),
);
for (const item of items) {
if (!item) throw new NotFoundException('One or more items not found');
if (item.userId !== userId) throw new BadRequestException('You do not own all selected items');
if (item.status !== InventoryItemStatus.AVAILABLE) {
throw new BadRequestException('One or more items are not available');
}
}
// 4. Calcula valor total em centavos BRL
const totalValueCents = items.reduce(
(sum, item) => sum + item.item.valueCentsBrl,
BigInt(0),
);
// 5. Define expiracao (24 horas)
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + 24);
// 6. Cria o swap no banco
const swap = await this.swapRepository.create({
creatorId: userId,
acceptorId,
creatorValueCents: totalValueCents,
isPublic,
message,
expiresAt,
});
// 7. Marca itens como IN_SWAP e registra SwapItems
for (const item of items) {
await this.inventoryRepository.updateStatus(item.id, InventoryItemStatus.IN_SWAP);
await this.swapRepository.addItem({
swapId: swap.id,
inventoryItemId: item.id,
isCreatorItem: true,
itemValueCents: item.item.valueCentsBrl,
});
}
// 8. Retorna swap completo com relations
return this.swapRepository.findById(swap.id);
});
}Response (200 OK)
{
"success": true,
"swap": {
"id": "839201847500",
"creatorId": "738291038270",
"acceptorId": "738291038271",
"status": "PENDING",
"creatorValueCents": "125000",
"acceptorValueCents": null,
"isPublic": false,
"message": "Troca justa, aceita ai",
"expiresAt": "2026-02-12T14:30:00.000Z",
"createdAt": "2026-02-11T14:30:00.000Z",
"completedAt": null,
"creator": {
"id": "738291038270",
"username": "SkinMaster",
"avatarUrl": "https://..."
},
"acceptor": {
"id": "738291038271",
"username": "TradeKing",
"avatarUrl": "https://..."
},
"items": [
{
"id": "839201847501",
"swapId": "839201847500",
"inventoryItemId": "839201847382",
"isCreatorItem": true,
"itemValueCents": "75000",
"inventoryItem": {
"id": "839201847382",
"userId": "738291038270",
"itemId": "100001",
"status": "IN_SWAP",
"item": {
"id": "100001",
"name": "AK-47 | Redline",
"marketHashName": "AK-47 | Redline (Field-Tested)",
"imageUrl": "https://...",
"rarity": "CLASSIFIED",
"valueCentsBrl": "75000"
}
}
},
{
"id": "839201847502",
"swapId": "839201847500",
"inventoryItemId": "839201847383",
"isCreatorItem": true,
"itemValueCents": "50000",
"inventoryItem": {
"id": "839201847383",
"userId": "738291038270",
"itemId": "100002",
"status": "IN_SWAP",
"item": {
"id": "100002",
"name": "USP-S | Kill Confirmed",
"marketHashName": "USP-S | Kill Confirmed (Minimal Wear)",
"imageUrl": "https://...",
"rarity": "COVERT",
"valueCentsBrl": "50000"
}
}
}
]
}
}Erros Possiveis
| Status | Mensagem | Causa |
|---|---|---|
| 400 | Must select between 1 and 10 items | Nenhum item selecionado ou mais de 10 |
| 400 | Private swaps must specify an acceptor | Swap privado sem acceptorId |
| 400 | Cannot create swap with yourself | acceptorId === userId |
| 400 | You do not own all selected items | Item pertence a outro usuario |
| 400 | One or more items are not available | Item com status diferente de AVAILABLE |
| 404 | One or more items not found | ID de item inexistente |
| 404 | Item details not found | Inventory item sem relacao com Item |
2. PUT /swap/:id/accept - Aceitar Swap
Aceita uma proposta de swap existente, realizando a transferencia atomica de itens entre os dois usuarios.
Autenticacao: Obrigatoria (AuthGuard)
Use Case: src/application/use-cases/swap/accept-swap.use-case.ts
Request
PUT /swap/839201847500/accept
Content-Type: application/json
Cookie: sessionId=def456
{
"inventoryItemIds": ["839201847400", "839201847401"]
}| Campo | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
inventoryItemIds | string[] | Sim | IDs dos itens do acceptor para trocar |
Validacoes (Pre-Lock)
- Existencia do swap: O swap deve existir no banco.
- Status PENDING: O swap deve estar com status
PENDING. - Nao expirado:
expiresAtdeve ser maior que a data atual. - Autorizacao: Se o swap for privado (
acceptorId != null), apenas o acceptor designado pode aceitar. - Auto-aceite: O creator nao pode aceitar o proprio swap.
Validacoes (Dentro do Lock)
- Re-fetch: O swap e buscado novamente dentro do lock para garantir que ainda esta
PENDING. - Itens do acceptor: Todos devem existir, pertencer ao userId e estar
AVAILABLE.
Logica de Execucao
// accept-swap.use-case.ts (simplificado)
async execute(userId, swapId, inventoryItemIds) {
// 1. Busca o swap
const swap = await this.swapRepository.findById(swapId);
if (!swap) throw new NotFoundException('Swap not found');
// 2. Validacoes pre-lock
if (swap.status !== SwapStatus.PENDING) {
throw new BadRequestException('Swap is not pending');
}
if (swap.expiresAt < new Date()) {
throw new BadRequestException('Swap has expired');
}
if (swap.acceptorId && swap.acceptorId !== userId) {
throw new ForbiddenException('This swap is for another user');
}
if (swap.creatorId === userId) {
throw new BadRequestException('Cannot accept your own swap');
}
// 3. Ordenacao de locks para prevenir deadlocks
const [firstUserId, secondUserId] = [userId, swap.creatorId].sort((a, b) =>
a < b ? -1 : a > b ? 1 : 0,
);
// 4. Adquire locks na ordem: swap -> primeiro user -> segundo user
return this.lockService.withLock(`swap:${swapId}`, async () => {
return this.lockService.withUserBalanceLock(firstUserId, async () => {
return this.lockService.withUserBalanceLock(secondUserId, async () => {
// 5. Re-fetch dentro do lock (double-check)
const freshSwap = await this.swapRepository.findById(swapId);
if (!freshSwap || freshSwap.status !== SwapStatus.PENDING) {
throw new BadRequestException('Swap is no longer available');
}
// 6. Valida itens do acceptor
const items = await Promise.all(
inventoryItemIds.map((id) => this.inventoryRepository.findById(id)),
);
for (const item of items) {
if (!item) throw new NotFoundException('One or more items not found');
if (item.userId !== userId) throw new BadRequestException('...');
if (item.status !== InventoryItemStatus.AVAILABLE) throw new BadRequestException('...');
}
// 7. Calcula valor total do acceptor
const totalValueCents = items.reduce(
(sum, item) => sum + item.item.valueCentsBrl,
BigInt(0),
);
// 8. Transaction atomica
return this.prisma.$transaction(async (tx) => {
// 8a. Define acceptor e valor no swap
await this.swapRepository.setAcceptor(swapId, userId, totalValueCents);
// 8b. Marca itens do acceptor como IN_SWAP e cria SwapItems
for (const item of items) {
await this.inventoryRepository.updateStatus(item.id, InventoryItemStatus.IN_SWAP);
await this.swapRepository.addItem({
swapId,
inventoryItemId: item.id,
isCreatorItem: false,
itemValueCents: item.item.valueCentsBrl,
});
}
// 8c. Transfere itens do creator para o acceptor
const creatorItems = freshSwap.items.filter((i) => i.isCreatorItem);
for (const swapItem of creatorItems) {
await this.inventoryRepository.updateOwner(
swapItem.inventoryItemId,
userId, // novo dono = acceptor
InventoryItemStatus.AVAILABLE,
'Swap',
);
}
// 8d. Transfere itens do acceptor para o creator
for (const item of items) {
await this.inventoryRepository.updateOwner(
item.id,
freshSwap.creatorId, // novo dono = creator
InventoryItemStatus.AVAILABLE,
'Swap',
);
}
// 8e. Marca swap como ACCEPTED
await this.swapRepository.updateStatus(swapId, SwapStatus.ACCEPTED, new Date());
return this.swapRepository.findById(swapId);
});
});
});
});
}Response (200 OK)
{
"success": true,
"swap": {
"id": "839201847500",
"creatorId": "738291038270",
"acceptorId": "738291038271",
"status": "ACCEPTED",
"creatorValueCents": "125000",
"acceptorValueCents": "118000",
"isPublic": false,
"message": "Troca justa, aceita ai",
"expiresAt": "2026-02-12T14:30:00.000Z",
"createdAt": "2026-02-11T14:30:00.000Z",
"completedAt": "2026-02-11T15:45:00.000Z",
"creator": {
"id": "738291038270",
"username": "SkinMaster",
"avatarUrl": "https://..."
},
"acceptor": {
"id": "738291038271",
"username": "TradeKing",
"avatarUrl": "https://..."
},
"items": [
{
"id": "839201847501",
"swapId": "839201847500",
"inventoryItemId": "839201847382",
"isCreatorItem": true,
"itemValueCents": "75000",
"inventoryItem": {
"id": "839201847382",
"userId": "738291038271",
"status": "AVAILABLE",
"item": { "name": "AK-47 | Redline", "rarity": "CLASSIFIED" }
}
},
{
"id": "839201847502",
"swapId": "839201847500",
"inventoryItemId": "839201847383",
"isCreatorItem": true,
"itemValueCents": "50000",
"inventoryItem": {
"id": "839201847383",
"userId": "738291038271",
"status": "AVAILABLE",
"item": { "name": "USP-S | Kill Confirmed", "rarity": "COVERT" }
}
},
{
"id": "839201847503",
"swapId": "839201847500",
"inventoryItemId": "839201847400",
"isCreatorItem": false,
"itemValueCents": "68000",
"inventoryItem": {
"id": "839201847400",
"userId": "738291038270",
"status": "AVAILABLE",
"item": { "name": "M4A4 | Howl", "rarity": "CONTRABAND" }
}
},
{
"id": "839201847504",
"swapId": "839201847500",
"inventoryItemId": "839201847401",
"isCreatorItem": false,
"itemValueCents": "50000",
"inventoryItem": {
"id": "839201847401",
"userId": "738291038270",
"status": "AVAILABLE",
"item": { "name": "Glock-18 | Fade", "rarity": "COVERT" }
}
}
]
}
}Apos o aceite, os itens do creator (isCreatorItem: true) agora pertencem ao acceptor (userId mudou), e vice-versa. Ambos retornam ao status AVAILABLE.
Erros Possiveis
| Status | Mensagem | Causa |
|---|---|---|
| 400 | Swap is not pending | Swap ja foi aceito/cancelado/recusado |
| 400 | Swap has expired | expiresAt < now |
| 400 | Cannot accept your own swap | Creator tentando aceitar |
| 400 | Swap is no longer available | Outro usuario aceitou primeiro (re-fetch) |
| 400 | You do not own all selected items | Item nao pertence ao acceptor |
| 400 | One or more items are not available | Item com status diferente de AVAILABLE |
| 403 | This swap is for another user | Swap privado para outro acceptor |
| 404 | Swap not found | ID inexistente |
| 404 | One or more items not found | ID de item inexistente |
3. PUT /swap/:id/cancel - Cancelar Swap
Cancela um swap criado pelo usuario. Apenas o creator pode cancelar. Devolve todos os itens (creator e acceptor) para o status AVAILABLE.
Autenticacao: Obrigatoria (AuthGuard)
Use Case: src/application/use-cases/swap/cancel-swap.use-case.ts
Request
PUT /swap/839201847500/cancel
Cookie: sessionId=abc123Nenhum body necessario.
Validacoes
- Existencia do swap: O swap deve existir.
- Autorizacao: Apenas o creator pode cancelar (
swap.creatorId === userId). - Status PENDING: Apenas swaps com status
PENDINGpodem ser cancelados.
Logica de Execucao
// cancel-swap.use-case.ts (simplificado)
async execute(userId, swapId) {
const swap = await this.swapRepository.findById(swapId);
if (!swap) throw new NotFoundException('Swap not found');
if (swap.creatorId !== userId) {
throw new ForbiddenException('Only the creator can cancel this swap');
}
if (swap.status !== SwapStatus.PENDING) {
throw new BadRequestException('Can only cancel pending swaps');
}
return this.prisma.$transaction(async (tx) => {
// Devolve itens do creator para AVAILABLE
const creatorItems = swap.items?.filter((item) => item.isCreatorItem) || [];
for (const swapItem of creatorItems) {
await this.inventoryRepository.updateStatus(
swapItem.inventoryItemId,
InventoryItemStatus.AVAILABLE,
);
}
// Devolve itens do acceptor para AVAILABLE (se existirem)
const acceptorItems = swap.items?.filter((item) => !item.isCreatorItem) || [];
if (acceptorItems.length > 0) {
for (const swapItem of acceptorItems) {
await this.inventoryRepository.updateStatus(
swapItem.inventoryItemId,
InventoryItemStatus.AVAILABLE,
);
}
}
await this.swapRepository.updateStatus(swapId, SwapStatus.CANCELLED);
return { success: true };
});
}Response (200 OK)
{
"success": true,
"message": "Swap cancelled successfully"
}Erros Possiveis
| Status | Mensagem | Causa |
|---|---|---|
| 400 | Can only cancel pending swaps | Swap nao esta PENDING |
| 403 | Only the creator can cancel this swap | Usuario nao e o creator |
| 404 | Swap not found | ID inexistente |
4. PUT /swap/:id/decline - Recusar Swap
Recusa uma proposta de swap. Em swaps privados, apenas o acceptor designado pode recusar. Em swaps publicos, qualquer usuario pode recusar.
Autenticacao: Obrigatoria (AuthGuard)
Use Case: src/application/use-cases/swap/decline-swap.use-case.ts
Request
PUT /swap/839201847500/decline
Cookie: sessionId=def456Nenhum body necessario.
Validacoes
- Existencia do swap: O swap deve existir.
- Autorizacao:
- Swap privado: apenas o
acceptorIddesignado pode recusar. - Swap publico (
isPublic = true): qualquer usuario pode recusar.
- Swap privado: apenas o
- Status PENDING: Apenas swaps com status
PENDINGpodem ser recusados.
Logica de Execucao
// decline-swap.use-case.ts (simplificado)
async execute(userId, swapId) {
const swap = await this.swapRepository.findById(swapId);
if (!swap) throw new NotFoundException('Swap not found');
// Swap privado: apenas o acceptor designado
if (!swap.isPublic && swap.acceptorId !== userId) {
throw new ForbiddenException('You cannot decline this swap');
}
if (swap.status !== SwapStatus.PENDING) {
throw new BadRequestException('Can only decline pending swaps');
}
return this.prisma.$transaction(async (tx) => {
// Devolve APENAS os itens do creator para AVAILABLE
const creatorItems = swap.items?.filter((item) => item.isCreatorItem) || [];
for (const swapItem of creatorItems) {
await this.inventoryRepository.updateStatus(
swapItem.inventoryItemId,
InventoryItemStatus.AVAILABLE,
);
}
await this.swapRepository.updateStatus(swapId, SwapStatus.DECLINED);
return { success: true };
});
}Diferenca fundamental entre Decline e Cancel:
- Decline: Devolve APENAS itens do creator (
isCreatorItem = true). O acceptor nunca commitou itens no decline. - Cancel: Devolve itens de AMBOS os lados.
Response (200 OK)
{
"success": true,
"message": "Swap declined successfully"
}Erros Possiveis
| Status | Mensagem | Causa |
|---|---|---|
| 400 | Can only decline pending swaps | Swap nao esta PENDING |
| 403 | You cannot decline this swap | Swap privado e usuario nao e o acceptor |
| 404 | Swap not found | ID inexistente |
5. GET /swap/my-swaps - Meus Swaps
Retorna os swaps pendentes do usuario, divididos em dois grupos: swaps criados pelo usuario e swaps recebidos (onde o usuario e o acceptor).
Autenticacao: Obrigatoria (AuthGuard)
Use Case: src/application/use-cases/swap/get-my-swaps.use-case.ts
Request
GET /swap/my-swaps
Cookie: sessionId=abc123Nenhum parametro necessario.
Logica de Execucao
// get-my-swaps.use-case.ts
async execute(userId) {
const [created, received] = await Promise.all([
this.swapRepository.findPendingByCreator(userId),
this.swapRepository.findPendingByAcceptor(userId),
]);
return { created, received };
}Filtros do Repository
Ambas as queries filtram por:
status = PENDINGexpiresAt > now()(nao expirado)- Ordenado por
createdAt DESC(mais recentes primeiro)
// swap.repository.ts - findPendingByCreator
async findPendingByCreator(creatorId: bigint) {
return this.prisma.swap.findMany({
where: {
creatorId,
status: SwapStatus.PENDING,
expiresAt: { gt: new Date() },
},
include: {
acceptor: { select: { id: true, username: true, avatarUrl: true } },
items: { include: { inventoryItem: { include: { item: true } } } },
},
orderBy: { createdAt: 'desc' },
});
}
// swap.repository.ts - findPendingByAcceptor
async findPendingByAcceptor(acceptorId: bigint) {
return this.prisma.swap.findMany({
where: {
acceptorId,
status: SwapStatus.PENDING,
expiresAt: { gt: new Date() },
},
include: {
creator: { select: { id: true, username: true, avatarUrl: true } },
items: { include: { inventoryItem: { include: { item: true } } } },
},
orderBy: { createdAt: 'desc' },
});
}Response (200 OK)
{
"created": [
{
"id": "839201847500",
"creatorId": "738291038270",
"acceptorId": "738291038271",
"status": "PENDING",
"creatorValueCents": "125000",
"acceptorValueCents": null,
"isPublic": false,
"message": "Troca justa",
"expiresAt": "2026-02-12T14:30:00.000Z",
"createdAt": "2026-02-11T14:30:00.000Z",
"acceptor": {
"id": "738291038271",
"username": "TradeKing",
"avatarUrl": "https://..."
},
"items": [
{
"id": "839201847501",
"isCreatorItem": true,
"itemValueCents": "75000",
"inventoryItem": {
"item": { "name": "AK-47 | Redline", "imageUrl": "..." }
}
}
]
}
],
"received": [
{
"id": "839201847600",
"creatorId": "738291038272",
"acceptorId": "738291038270",
"status": "PENDING",
"creatorValueCents": "90000",
"creator": {
"id": "738291038272",
"username": "SkinCollector",
"avatarUrl": "https://..."
},
"items": [...]
}
]
}6. GET /swap/public - Swaps Publicos
Retorna os swaps publicos pendentes disponiveis para qualquer usuario aceitar. Limitado a 50 resultados.
Autenticacao: Obrigatoria (AuthGuard)
Use Case: src/application/use-cases/swap/get-public-swaps.use-case.ts
Request
GET /swap/public
Cookie: sessionId=abc123Nenhum parametro necessario.
Logica de Execucao
// get-public-swaps.use-case.ts
async execute() {
return this.swapRepository.findPublicSwaps();
}Filtros do Repository
// swap.repository.ts - findPublicSwaps
async findPublicSwaps() {
return this.prisma.swap.findMany({
where: {
isPublic: true,
status: SwapStatus.PENDING,
expiresAt: { gt: new Date() },
},
include: {
creator: { select: { id: true, username: true, avatarUrl: true } },
items: { include: { inventoryItem: { include: { item: true } } } },
},
orderBy: { createdAt: 'desc' },
take: 50,
});
}Filtros aplicados:
isPublic = truestatus = PENDINGexpiresAt > now()(nao expirado)- Ordenado por
createdAt DESC - Limite de 50 resultados
Response (200 OK)
[
{
"id": "839201847700",
"creatorId": "738291038273",
"acceptorId": null,
"status": "PENDING",
"creatorValueCents": "200000",
"acceptorValueCents": null,
"isPublic": true,
"message": "Aceito qualquer oferta justa",
"expiresAt": "2026-02-12T14:30:00.000Z",
"createdAt": "2026-02-11T14:30:00.000Z",
"creator": {
"id": "738291038273",
"username": "BigTrader",
"avatarUrl": "https://..."
},
"items": [
{
"id": "839201847701",
"isCreatorItem": true,
"itemValueCents": "200000",
"inventoryItem": {
"item": {
"name": "AWP | Dragon Lore",
"imageUrl": "...",
"rarity": "COVERT",
"valueCentsBrl": "200000"
}
}
}
]
}
]7. POST /swap/site - Site Swap (Waxpeer)
Troca itens do inventario do usuario por um item do marketplace Waxpeer. O usuario oferece de 1 a 6 itens e recebe um item especifico com preco em tempo real. Inclui margem dinamica (via PriceMarginService.getCurrent(), default 1.5 = 50%) sobre o preco Waxpeer e tolerancia de 10%; a diferenca entre o valor ofertado e o alvo (0-10%) e creditada de volta no saldo do usuario.
Autenticacao: Obrigatoria (AuthGuard)
Use Case: src/application/use-cases/swap/perform-site-swap.use-case.ts
Constantes
const MAX_ITEMS = 6; // Maximo de itens oferecidos
const TOLERANCE_PERCENT = 10; // Tolerancia de 10% para baixo
// Margem obtida em runtime via PriceMarginService.getCurrent() (default 1.5 = 50%)Request
POST /swap/site
Content-Type: application/json
Cookie: sessionId=abc123
{
"inventoryItemIds": ["839201847382", "839201847383", "839201847384"],
"targetMarketHashName": "AK-47 | Fire Serpent (Field-Tested)"
}| Campo | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
inventoryItemIds | string[] | Sim | IDs dos itens do inventario (1 a 6 itens) |
targetMarketHashName | string | Sim | Nome exato do item no marketplace Waxpeer |
Validacoes
- Quantidade de itens: Minimo 1, maximo 6.
- Target name: Nao pode ser vazio.
- Disponibilidade Waxpeer: O item deve existir no marketplace com match exato (
isExactMatch). - Estoque Waxpeer: O item deve ter pelo menos 1 unidade disponivel (
count >= 1). - Propriedade dos itens: Todos devem pertencer ao userId.
- Status dos itens: Todos devem estar
AVAILABLE. - Valor do target dentro do range permitido (detalhado abaixo).
Calculo de Preco
O calculo de preco do item alvo segue estas etapas:
1. Preco base Waxpeer (USD centavos)
Ex: 5000 (= $50.00)
2. Aplica margem (PriceMarginService.getCurrent(), default 1.5 = 50%)
priceCentsUsdWithMargin = 5000 * 1.5 = 7500 (= $75.00)
3. Converte USD -> BRL
exchangeRate = 5.25 (exemplo)
priceCentsBrl = (7500 / 100) * 5.25 * 100 = 39375 (= R$393.75)Formula completa:
priceCentsBrl = round((priceCentsUsd * priceMargin / 100) * exchangeRate * 100)
priceMargin = PriceMarginService.getCurrent() (default 1.5 = 50%)Validacao de Valor
O valor do item alvo deve estar dentro de um range especifico:
const maxAllowedTargetValue = totalUserValueCents;
const minAllowedTargetValue = (totalUserValueCents * BigInt(100 - TOLERANCE_PERCENT)) / BigInt(100);
// minAllowed = totalUser * 90 / 100
// Regra: minAllowed <= targetValueBrl <= maxAllowed- Limite superior: O item alvo NAO pode valer mais do que a soma dos itens oferecidos.
- Limite inferior: O item alvo NAO pode valer menos que 90% da soma dos itens oferecidos.
Isso significa que o usuario pode receber um item de valor igual ou ate 10% menor que o que ofereceu.
Logica de Execucao
// perform-site-swap.use-case.ts (simplificado)
async execute(userId, inventoryItemIds, targetMarketHashName) {
// 1. Validacoes de input
if (inventoryItemIds.length === 0) throw new BadRequestException('...');
if (inventoryItemIds.length > MAX_ITEMS) throw new BadRequestException('...');
if (!targetMarketHashName?.trim()) throw new BadRequestException('...');
// 2. Busca preco do item no Waxpeer
const waxpeerPrice = await this.waxpeerService.getItemPrice(targetMarketHashName);
if (!waxpeerPrice || !waxpeerPrice.isExactMatch) throw new NotFoundException('...');
if (waxpeerPrice.count < 1) throw new BadRequestException('Item temporarily out of stock');
// 3. Calcula preco em BRL com margem (dinamica via PriceMarginService)
const priceMargin = this.priceMarginService.getCurrent();
const priceCentsUsdWithMargin = Math.round(waxpeerPrice.priceCentsUsd * priceMargin);
const exchangeRate = await this.exchangeRateService.getUsdToBrlRate();
const priceCentsBrl = Math.round((priceCentsUsdWithMargin / 100) * exchangeRate * 100);
// 4. Adquire lock do usuario
return this.lockService.withUserBalanceLock(userId, async () => {
return this.prisma.$transaction(async (tx) => {
// 5. Busca e valida itens dentro da transaction
const inventoryItems = [];
for (const invItemId of inventoryItemIds) {
const invItem = await tx.userInventory.findUnique({
where: { id: invItemId },
include: { item: true },
});
if (!invItem) throw new NotFoundException('...');
if (invItem.userId !== userId) throw new BadRequestException('...');
if (invItem.status !== 'AVAILABLE') throw new BadRequestException('...');
inventoryItems.push(invItem);
}
// 6. Calcula valor total dos itens do usuario
const totalUserValueCents = inventoryItems.reduce(
(sum, inv) => sum + inv.item.valueCentsBrl,
BigInt(0),
);
// 7. Valida range de valor
const targetValueBrl = BigInt(priceCentsBrl);
const maxAllowedTargetValue = totalUserValueCents;
const minAllowedTargetValue =
(totalUserValueCents * BigInt(100 - TOLERANCE_PERCENT)) / BigInt(100);
if (targetValueBrl > maxAllowedTargetValue) {
throw new BadRequestException('Target item is more expensive than your items...');
}
if (targetValueBrl < minAllowedTargetValue) {
throw new BadRequestException('Target item is too cheap...');
}
// 8. Find or create item no banco de dados
let targetItem = await tx.item.findUnique({
where: { marketHashName: targetMarketHashName },
});
if (targetItem) {
targetItem = await tx.item.update({
where: { id: targetItem.id },
data: {
valueCents: BigInt(priceCentsUsdWithMargin),
valueCentsBrl: BigInt(priceCentsBrl),
imageUrl: waxpeerPrice.imageUrl,
},
});
} else {
targetItem = await tx.item.create({
data: {
id: this.snowflakeService.generate(),
name: parsed.skinName || parsed.weaponName,
marketHashName: targetMarketHashName,
imageUrl: waxpeerPrice.imageUrl,
rarity: this.inferRarity(targetMarketHashName),
valueCents: BigInt(priceCentsUsdWithMargin),
valueCentsBrl: BigInt(priceCentsBrl),
weaponCategory: this.inferWeaponCategory(targetMarketHashName),
exterior: parsed.exterior,
isStatTrak: parsed.isStatTrak,
},
});
}
// 9. Marca itens do usuario como IN_SWAP
for (const invItem of inventoryItems) {
await tx.userInventory.update({
where: { id: invItem.id },
data: { status: 'IN_SWAP' },
});
}
// 10. Cria novo item no inventario do usuario
const newInventoryItem = await tx.userInventory.create({
data: {
id: this.snowflakeService.generate(),
userId,
itemId: targetItem.id,
status: 'AVAILABLE',
acquiredFrom: 'Swap',
},
});
// 11. Registra SiteSwap e SiteSwapItems para auditoria
const siteSwap = await tx.siteSwap.create({
data: {
id: this.snowflakeService.generate(),
userId,
targetItemId: targetItem.id,
targetItemValueCents: BigInt(priceCentsBrl),
totalOfferedValueCents: totalUserValueCents,
},
});
for (const invItem of inventoryItems) {
await tx.siteSwapItem.create({
data: {
id: this.snowflakeService.generate(),
siteSwapId: siteSwap.id,
userInventoryId: invItem.id,
itemId: invItem.itemId,
itemValueCents: invItem.item.valueCentsBrl,
},
});
}
// 11b. Credita o refund (diferenca ofertado - alvo) no saldo do usuario
const refundCents = totalUserValueCents - BigInt(priceCentsBrl);
if (refundCents > BigInt(0)) {
await this.transactionService.credit(userId, refundCents, 'SWAP_REFUND', undefined, tx);
const newBalanceCents = await this.transactionService.getUserBalance(userId);
this.webSocketGateway.emitBalanceUpdate(userId, newBalanceCents);
}
// 12. Emite live drop apos 3 segundos
setTimeout(() => {
this.webSocketGateway.emitLiveDrop({
userName: user.username,
userAvatar: user.avatarUrl,
itemName: targetItem.name,
itemImage: targetItem.imageUrl,
itemValueCents: BigInt(priceCentsBrl),
rarity: targetItem.rarity,
source: 'swap',
timestamp: new Date(),
dropChance: 100,
});
}, 3000);
return { success: true, itemsSwapped: [...], receivedItem: {...} };
});
});
}Response (200 OK)
{
"success": true,
"itemsSwapped": [
{
"id": "839201847382",
"name": "P250 | Sand Dune",
"imageUrl": "https://...",
"rarity": "CONSUMER",
"valueCentsBrl": "150",
"valueFormatted": "R$ 1,50"
},
{
"id": "839201847383",
"name": "MAG-7 | Wildwood",
"imageUrl": "https://...",
"rarity": "CONSUMER",
"valueCentsBrl": "120",
"valueFormatted": "R$ 1,20"
}
],
"totalSwappedValueCents": "270",
"totalSwappedValueFormatted": "R$ 2,70",
"receivedItem": {
"inventoryId": "839201847500",
"id": "100050",
"marketHashName": "SG 553 | Pulse (Factory New)",
"name": "SG 553 | Pulse",
"imageUrl": "https://...",
"rarity": "MIL_SPEC",
"valueCentsBrl": "250",
"valueFormatted": "R$ 2,50"
}
}O controller adiciona os campos valueFormatted e totalSwappedValueFormatted via MoneyService.format().
A diferenca entre o total ofertado (R$ 2,70) e o item recebido (R$ 2,50) — R$ 0,20 — e creditada de volta no saldo do usuario via transactionService.credit, dentro da mesma transaction.
Erros Possiveis
| Status | Mensagem | Causa |
|---|---|---|
| 400 | You must select at least 1 item | Nenhum item selecionado |
| 400 | You can select at most 6 items | Mais de 6 itens |
| 400 | Target item market hash name is required | Nome vazio |
| 400 | Item temporarily out of stock on marketplace | Waxpeer sem estoque |
| 400 | Target item is more expensive than your items | Target > total oferecido |
| 400 | Target item is too cheap | Target < 90% do total |
| 400 | Item does not belong to user | Item de outro usuario |
| 400 | Item {name} is not available | Item com status diferente de AVAILABLE |
| 404 | Item not available on marketplace | Item nao encontrado no Waxpeer |
| 404 | Inventory item {id} not found | ID de item inexistente |
| 503 | Unable to fetch item price from marketplace | Waxpeer indisponivel |
8. GET /swap/targets - Itens Disponiveis Waxpeer
Retorna a lista de itens disponiveis no Waxpeer para serem usados como alvo em Site Swaps. Suporta filtragem, busca e paginacao. Este e um endpoint publico (nao requer autenticacao).
Autenticacao: Nao requerida (decorador @Public())
Use Case: src/application/use-cases/swap/get-swap-targets.use-case.ts
Request
GET /swap/targets?minValueCents=1000&maxValueCents=50000&search=AK-47&sortOrder=desc&limit=20&offset=0| Parametro | Tipo | Obrigatorio | Default | Descricao |
|---|---|---|---|---|
minValueCents | number | Nao | - | Valor minimo em centavos BRL |
maxValueCents | number | Nao | - | Valor maximo em centavos BRL |
search | string | Nao | - | Busca por nome do item (case insensitive) |
sortOrder | 'asc' | 'desc' | Nao | 'desc' | Ordenacao por preco |
limit | number | Nao | 20 | Quantidade de itens por pagina |
offset | number | Nao | 0 | Offset para paginacao |
Logica de Execucao
// get-swap-targets.use-case.ts (simplificado)
async execute(filters) {
const { minValueCents, maxValueCents, search, sortOrder = 'desc', limit = 20, offset = 0 } = filters;
// 1. Busca snapshot de itens do Waxpeer (cache)
const waxpeerItems = await this.waxpeerSnapshotService.getFilteredItems();
// 2. Busca taxa de cambio USD -> BRL e margem dinamica
const exchangeRate = await this.exchangeRateService.getUsdToBrlRate();
const priceMargin = this.priceMarginService.getCurrent();
const searchLower = search?.trim()?.toLowerCase();
const mappedItems = [];
for (const item of waxpeerItems) {
const parsed = this.itemNameParser.parse(item.name);
// 3. Calcula preco com margem e converte para BRL
const priceCentsWithMargin = Math.round(item.priceCents * priceMargin);
const priceCentsBrl = Math.round((priceCentsWithMargin / 100) * exchangeRate * 100);
if (priceCentsBrl <= 0) continue;
// 4. Aplica filtros
if (minValueCents !== undefined && priceCentsBrl < minValueCents) continue;
if (maxValueCents !== undefined && priceCentsBrl > maxValueCents) continue;
if (searchLower && !item.name.toLowerCase().includes(searchLower)) continue;
mappedItems.push({
marketHashName: item.name,
name: item.name,
imageUrl: item.imageUrl,
rarity: item.rarity,
valueCentsBrl: priceCentsBrl.toString(),
valueCents: priceCentsWithMargin.toString(),
weaponCategory: item.weaponCategory,
exterior: parsed.exterior,
isStatTrak: parsed.isStatTrak,
});
}
// 5. Ordena por preco
mappedItems.sort((a, b) => {
const diff = Number(a.valueCentsBrl) - Number(b.valueCentsBrl);
return sortOrder === 'asc' ? diff : -diff;
});
// 6. Pagina
const total = mappedItems.length;
const paginatedItems = mappedItems.slice(offset, offset + limit);
const hasMore = offset + limit < total;
return { items: paginatedItems, total, hasMore };
}Response (200 OK)
{
"items": [
{
"marketHashName": "AK-47 | Redline (Field-Tested)",
"name": "AK-47 | Redline (Field-Tested)",
"imageUrl": "https://...",
"rarity": "CLASSIFIED",
"valueCentsBrl": "4200",
"valueCents": "800",
"weaponCategory": "RIFLE",
"exterior": "Field-Tested",
"isStatTrak": false
},
{
"marketHashName": "AK-47 | Asiimov (Battle-Scarred)",
"name": "AK-47 | Asiimov (Battle-Scarred)",
"imageUrl": "https://...",
"rarity": "COVERT",
"valueCentsBrl": "9800",
"valueCents": "1560",
"weaponCategory": "RIFLE",
"exterior": "Battle-Scarred",
"isStatTrak": false
}
],
"total": 1523,
"hasMore": true
}Campos da Response
| Campo | Tipo | Descricao |
|---|---|---|
items[].marketHashName | string | Nome completo do item (usado como parametro em POST /swap/site) |
items[].name | string | Nome de exibicao |
items[].imageUrl | string | URL da imagem do item |
items[].rarity | string | Raridade do item |
items[].valueCentsBrl | string | Preco em centavos BRL (com margem dinamica via PriceMarginService) |
items[].valueCents | string | Preco em centavos USD (com margem dinamica via PriceMarginService) |
items[].weaponCategory | string | Categoria da arma |
items[].exterior | string? | Condicao (Factory New, Minimal Wear, etc) |
items[].isStatTrak | boolean | Se e StatTrak |
total | number | Total de itens que passaram pelos filtros |
hasMore | boolean | Se existem mais paginas |
Regras de Negocio
Swap User-to-User
- Limite de itens: Minimo 1, maximo 10 itens por lado.
- Expiracao: Swaps expiram em 24 horas apos a criacao.
- Swap privado: Deve especificar
acceptorId. Apenas o acceptor designado pode aceitar ou recusar. - Swap publico: Qualquer usuario pode aceitar. Qualquer usuario pode recusar (decline).
- Auto-swap proibido: O creator nao pode criar swap com si mesmo e nao pode aceitar o proprio swap.
- Itens bloqueados: Ao criar um swap, os itens do creator sao marcados como
IN_SWAPe ficam indisponiveis para outras operacoes. - Valores em BRL: Todos os calculos utilizam
valueCentsBrldos itens, nuncavalueCents(USD). - Sem restricao de valor: Nao ha restricao de diferenca de valor entre creator e acceptor no swap user-to-user. O acceptor decide livremente quais itens oferecer.
- Atomicidade: A transferencia de itens e atomica via Prisma transaction.
- acquiredFrom: Apos transferencia, o campo
acquiredFromdo item e atualizado para'Swap'.
Site Swap (Waxpeer)
- Limite de itens: Minimo 1, maximo 6 itens oferecidos.
- Margem de preco: O site aplica margem dinamica via
PriceMarginService.getCurrent()(default 1.5 = 50%) sobre o preco Waxpeer. - Tolerancia: O item alvo pode valer ate 10% menos que os itens oferecidos (
TOLERANCE_PERCENT = 10). - Limite superior: O item alvo nao pode valer mais que a soma dos itens oferecidos.
- Conversao de moeda: USD -> BRL usando taxa de cambio em tempo real do
ExchangeRateService. - Match exato: O item deve existir no Waxpeer com
isExactMatch = true. - Estoque: O item deve ter pelo menos 1 unidade disponivel no Waxpeer.
- Find or create: Se o item ja existe no banco (
marketHashName), atualiza preco e imagem. Caso contrario, cria novo. - Live drop: Um evento de live drop e emitido via WebSocket apos 3 segundos de delay.
- Rarity/Category inferidos: Se o item e criado, rarity e weaponCategory sao inferidos pelo nome.
- Refund da diferenca: A diferenca entre o valor ofertado e o valor do alvo (0-10%) e creditada de volta no saldo do usuario via
transactionService.credit, dentro da mesma transaction. O usuario sempre recebe 100% do valor ofertado (item alvo + refund).
GGR (Gross Gaming Revenue) do Site Swap
O lucro do site no Site Swap vem da margem de preco aplicada ao item alvo:
- Margem dinamica: O preco exibido do item alvo ja inclui a margem retornada por
PriceMarginService.getCurrent()(default 1.5 = 50%). O alvo custa ao siteprecoExibido / marginpara ser sourced no Waxpeer. - Tolerancia de 10% NAO e lucro: A diferenca entre o valor ofertado e o valor do alvo (0-10%) e creditada de volta no saldo do usuario via
transactionService.credit. Ou seja, o usuario sempre recebe 100% do valor que ofertou (item alvo + refund), e a tolerancia nao fica retida pelo site.
Com a margem default de 50%, o valor exibido do alvo e 1.5x o custo real de sourcing no Waxpeer — essa diferenca e a margem bruta do site sobre a operacao.
Calculo de Precos (Site Swap)
Exemplo Numerico Completo
Cenario: Usuario quer trocar 3 skins por uma AK-47 Fire Serpent.
Itens do usuario (BRL):
| Item | valueCentsBrl |
|---|---|
| Glock-18 Fade | R$ 350,00 (35000) |
| USP-S Kill Confirmed | R$ 180,00 (18000) |
| P250 Asiimov | R$ 50,00 (5000) |
| Total | R$ 580,00 (58000) |
Preco Waxpeer da AK-47 Fire Serpent:
- Preco base: $64.00 USD (6400 centavos)
- Com margem 50%: $96.00 USD (9600 centavos)
- Taxa de cambio: R$ 5,25
- Em BRL: (9600 / 100) * 5.25 * 100 = R$ 504,00 (50400 centavos)
Validacao de range:
- maxAllowedTargetValue = 58000 (R$ 580,00)
- minAllowedTargetValue = 58000 * 90 / 100 = 52200 (R$ 522,00)
- targetValueBrl = 50400 (R$ 504,00)
Resultado: REJEITADO. R$ 504,00 < R$ 522,00 (minimo 90% de R$ 580,00).
O usuario precisaria oferecer menos itens ou o preco do Waxpeer precisaria ser maior.
Se o usuario remover o P250 Asiimov:
- Novo total: R$ 530,00 (53000)
- minAllowed = 53000 * 90 / 100 = 47700 (R$ 477,00)
- maxAllowed = 53000 (R$ 530,00)
- targetValue = 50400 (R$ 504,00)
- 47700 <= 50400 <= 53000: APROVADO.
Resultado nesse caso:
- Usuario entregou itens: R$ 530,00
- Recebeu: item alvo (valor exibido R$ 504,00) + refund de R$ 26,00 no saldo = R$ 530,00 (100% do ofertado)
- Custo real de sourcing do alvo no Waxpeer: $64.00 * 5.25 = R$ 336,00
- Margem do site embutida no alvo: R$ 504,00 exibido vs R$ 336,00 de custo (50% via PriceMarginService)
Formula Resumida
Preco exibido (BRL) = round((precoWaxpeerUsdCents * margin / 100) * taxaCambio * 100)
margin = PriceMarginService.getCurrent() (default 1.5 = 50%)
Range valido:
min = totalUserBrl * 90%
max = totalUserBrl * 100%
min <= precoExibidoBrl <= max
Refund creditado no saldo = totalUserBrl - precoExibidoBrl (0 a 10% do ofertado)Prevencao de Deadlocks
O Problema
Quando dois usuarios tentam aceitar swaps um do outro ao mesmo tempo, pode ocorrer deadlock se os locks nao forem adquiridos em ordem consistente.
A Solucao
O AcceptSwapUseCase ordena os userIds em ordem crescente antes de adquirir os locks:
// accept-swap.use-case.ts
const [firstUserId, secondUserId] = [userId, swap.creatorId].sort((a, b) =>
a < b ? -1 : a > b ? 1 : 0,
);Hierarquia de Locks
A aquisicao segue uma hierarquia estrita de 3 niveis:
Nivel 1: lock:swap:{swapId} (TTL 5s) - Garante exclusividade no swap
Nivel 2: lock:user:balance:{minId} (TTL 10s) - Lock do usuario com menor ID
Nivel 3: lock:user:balance:{maxId} (TTL 10s) - Lock do usuario com maior IDEssa hierarquia garante que:
- Apenas um processo pode operar em um swap especifico.
- Locks de usuario sao sempre adquiridos na mesma ordem, independente de quem e o caller.
- Se o lock nao puder ser adquirido apos 10 tentativas (retryCount), a operacao falha.
Cenario Detalhado
Usuarios: Alice (id=100), Bob (id=200)
Swap #1: Alice criou, Bob aceita
Swap #2: Bob criou, Alice aceita
Thread 1 (Bob aceita Swap #1):
1. Lock swap:1
2. Lock user:balance:100 (menor ID) <-- Alice
3. Lock user:balance:200 (maior ID) <-- Bob
Thread 2 (Alice aceita Swap #2):
1. Lock swap:2
2. Lock user:balance:100 (menor ID) <-- Alice [ESPERA Thread 1]
3. Lock user:balance:200 (maior ID) <-- Bob [ESPERA Thread 1]
Resultado: Thread 2 espera Thread 1 finalizar. SEM DEADLOCK.Edge Cases e Cenarios Criticos
1. Multiplos usuarios tentam aceitar swap publico
Cenario: Um swap publico tem 3 usuarios tentando aceitar ao mesmo tempo.
Protecao: O lock swap:{swapId} garante exclusividade. O primeiro a adquirir o lock processa normalmente. Os demais, ao conseguirem o lock, encontram o swap com status ACCEPTED no re-fetch e recebem o erro Swap is no longer available.
// Dentro do lock, re-fetch garante consistencia
const freshSwap = await this.swapRepository.findById(swapId);
if (!freshSwap || freshSwap.status !== SwapStatus.PENDING) {
throw new BadRequestException('Swap is no longer available');
}2. Swap expira durante processamento
Cenario: Um swap com expiresAt = 14:30:00 e aceito as 14:29:59. O processamento leva 2 segundos.
Protecao: A validacao de expiracao e feita ANTES de adquirir os locks. Se o swap expirou durante a espera pelo lock, o re-fetch dentro da transaction detecta. O deleteExpiredSwaps() do repository atualiza o status para EXPIRED via cron job.
3. Item fica indisponivel entre a validacao e a transaction
Cenario: O usuario seleciona itens, mas outro processo (venda, upgrade, batalha) marca o item como indisponivel entre a validacao pre-lock e a validacao intra-transaction.
Protecao no Create: O withUserBalanceLock impede operacoes concorrentes no inventario do mesmo usuario.
Protecao no Accept: O lock triplo (swap + dois users) garante exclusividade total. A validacao dentro do lock verifica novamente o status dos itens.
Protecao no Site Swap: O withUserBalanceLock + busca dentro da transaction (tx.userInventory.findUnique) garante consistencia.
4. Preco do Waxpeer muda entre consulta e execucao
Cenario: O usuario consulta o preco via GET /swap/targets, mas quando faz POST /swap/site, o preco mudou.
Protecao: O POST /swap/site busca o preco FRESH do Waxpeer (waxpeerService.getItemPrice) a cada chamada, nao usa cache. O calculo e a validacao sao feitos com o preco atualizado.
5. Item do Waxpeer nao existe no banco de dados
Cenario: O usuario tenta fazer Site Swap com um item que nunca foi visto pelo sistema.
Protecao: O use case faz find-or-create. Se o item nao existe (marketHashName), cria um novo registro na tabela Item com os dados do Waxpeer, inferindo rarity e weaponCategory pelo nome.
6. Itens orfaos com status IN_SWAP
Cenario: Um swap expira ou e cancelado, mas o cron job de limpeza falha.
Deteccao: Itens com status IN_SWAP que nao pertencem a nenhum swap PENDING sao orfaos.
Resolucao: O deleteExpiredSwaps() do repository atualiza swaps expirados para EXPIRED, mas NAO atualiza o status dos itens automaticamente. Os itens precisam ser devolvidos manualmente ou por um job de reconciliacao.
7. Race condition entre Create e Accept no mesmo item
Cenario: Usuario A cria swap com item X. Antes de A completar, outro processo tenta usar o item X.
Protecao: O withUserBalanceLock no Create garante que o item e marcado como IN_SWAP atomicamente. Qualquer outro processo que tente usar o item encontra status IN_SWAP e recebe erro.
Exemplos Numericos
Exemplo 1: Swap User-to-User Completo
CRIACAO:
Creator: Usuario A (id=100)
Itens do Creator:
- AK-47 Redline FT: R$ 42,00 (4200 centavos BRL)
- M4A1-S Hyper Beast FT: R$ 38,00 (3800 centavos BRL)
Total creator: R$ 80,00 (8000 centavos)
Acceptor: Usuario B (id=200) [swap privado]
Expiracao: +24 horas
Resultado:
- Swap PENDING criado
- 2 itens marcados IN_SWAP
- 2 SwapItems criados (isCreatorItem=true)
- creatorValueCents = 8000
ACEITE:
Acceptor: Usuario B (id=200)
Itens do Acceptor:
- AWP Asiimov FT: R$ 75,00 (7500 centavos BRL)
Total acceptor: R$ 75,00 (7500 centavos)
Lock ordering:
firstUserId = 100 (menor)
secondUserId = 200 (maior)
Locks: swap:X -> user:balance:100 -> user:balance:200
Resultado:
- acceptorValueCents = 7500
- 1 item do acceptor marcado IN_SWAP, SwapItem criado (isCreatorItem=false)
- AK-47 Redline -> userId = 200 (aceitor), status = AVAILABLE
- M4A1-S Hyper Beast -> userId = 200 (acceptor), status = AVAILABLE
- AWP Asiimov -> userId = 100 (creator), status = AVAILABLE
- Swap status = ACCEPTED, completedAt = agoraExemplo 2: Site Swap com Calculo Detalhado
DADOS:
Usuario tem 4 itens:
- Glock Water Elemental FT: R$ 12,00 (1200 centavos)
- P250 Muertos FT: R$ 8,50 (850 centavos)
- Five-SeveN Monkey Business MW: R$ 5,00 (500 centavos)
- Nova Hyper Beast FT: R$ 4,50 (450 centavos)
Total oferecido: R$ 30,00 (3000 centavos)
Item alvo: Deagle Kumicho Dragon FT
Preco Waxpeer: $3.36 USD (336 centavos USD)
Margem 50%: 336 * 1.5 = 504 centavos USD
Taxa cambio: 5.25
Preco BRL: (504 / 100) * 5.25 * 100 = 2646 centavos = R$ 26,46
VALIDACAO DE RANGE:
maxAllowed = 3000 (R$ 30,00)
minAllowed = 3000 * 90 / 100 = 2700 (R$ 27,00)
targetValue = 2646 (R$ 26,46)
2646 < 2700 --> REJEITADO (abaixo do minimo de 90%)
AJUSTE: Se o usuario remover a Nova Hyper Beast:
Total oferecido: R$ 25,50 (2550 centavos)
minAllowed = 2550 * 90 / 100 = 2295 (R$ 22,95)
maxAllowed = 2550 (R$ 25,50)
targetValue = 2646 (R$ 26,46)
2646 > 2550 --> REJEITADO (acima do maximo)
AJUSTE 2: Se mantiver todos os 4 itens e o preco Waxpeer for $3.84:
Margem: 384 * 1.5 = 576 centavos USD
BRL: (576 / 100) * 5.25 * 100 = 3024 centavos = R$ 30,24
minAllowed = 3000 * 90 / 100 = 2700
maxAllowed = 3000
targetValue = 3024
3024 > 3000 --> REJEITADO (acima do maximo)
CENARIO QUE FUNCIONA:
Preco Waxpeer: $3.60 USD (360 centavos USD)
Margem: 360 * 1.5 = 540 centavos USD
BRL: (540 / 100) * 5.25 * 100 = 2835 centavos = R$ 28,35
minAllowed = 2700 (R$ 27,00)
maxAllowed = 3000 (R$ 30,00)
targetValue = 2835 (R$ 28,35)
2700 <= 2835 <= 3000 --> APROVADO
Resultado:
User entregou itens: R$ 30,00
Recebeu: alvo (valor exibido R$ 28,35) + refund R$ 1,65 no saldo = R$ 30,00 (100%)
Custo real de sourcing (sem margem): (360/100) * 5.25 = R$ 18,90
Margem do site embutida no alvo: R$ 28,35 exibido vs R$ 18,90 custo (50% via PriceMarginService)Exemplo 3: Decline vs Cancel
CENARIO:
Creator: Usuario A
Acceptor (privado): Usuario B
Itens do creator: AK-47 Redline (IN_SWAP), M4A1-S (IN_SWAP)
Status: PENDING (nenhum item do acceptor commitado)
DECLINE (por Usuario B):
- AK-47 Redline -> AVAILABLE (devolvido ao Creator)
- M4A1-S -> AVAILABLE (devolvido ao Creator)
- Status -> DECLINED
- Itens do acceptor: nenhum afetado (nunca foram adicionados)
CANCEL (por Usuario A, em cenario hipotetico onde acceptor ja commitou):
- AK-47 Redline -> AVAILABLE (devolvido ao Creator)
- M4A1-S -> AVAILABLE (devolvido ao Creator)
- Itens do acceptor (se existirem) -> AVAILABLE (devolvidos ao Acceptor)
- Status -> CANCELLEDArquivos do Sistema
Camada de Dominio
| Arquivo | Descricao |
|---|---|
prisma/schema.prisma (linhas 697-778) | Modelos Swap, SwapItem, SiteSwap, SiteSwapItem, enum SwapStatus |
src/domain/repositories/swap.repository.interface.ts | Interface ISwapRepository com 8 metodos |
Camada de Aplicacao (Use Cases)
| Arquivo | Descricao |
|---|---|
src/application/use-cases/swap/create-swap.use-case.ts | Criacao de swap user-to-user |
src/application/use-cases/swap/accept-swap.use-case.ts | Aceite de swap com lock ordering |
src/application/use-cases/swap/cancel-swap.use-case.ts | Cancelamento pelo creator |
src/application/use-cases/swap/decline-swap.use-case.ts | Recusa pelo acceptor |
src/application/use-cases/swap/get-my-swaps.use-case.ts | Listagem de swaps do usuario |
src/application/use-cases/swap/get-public-swaps.use-case.ts | Listagem de swaps publicos |
src/application/use-cases/swap/perform-site-swap.use-case.ts | Site swap via Waxpeer |
src/application/use-cases/swap/get-swap-targets.use-case.ts | Listagem de itens Waxpeer |
Camada de Infraestrutura
| Arquivo | Descricao |
|---|---|
src/infrastructure/database/repositories/swap.repository.ts | Implementacao do ISwapRepository |
src/infrastructure/locks/lock.service.ts | Redlock distributed locks |
src/infrastructure/external-apis/waxpeer.service.ts | Integracao com API Waxpeer |
src/infrastructure/external-apis/waxpeer-snapshot.service.ts | Cache/snapshot de precos Waxpeer |
src/infrastructure/external-apis/exchange-rate.service.ts | Taxa de cambio USD/BRL |
src/infrastructure/external-apis/item-name-parser.service.ts | Parser de nomes de itens CS2 |
Camada de Apresentacao
| Arquivo | Descricao |
|---|---|
src/presentation/controllers/swap.controller.ts | Controller com 8 endpoints |
src/presentation/modules/swap.module.ts | Modulo NestJS do swap |
Interface do Repository
// src/domain/repositories/swap.repository.interface.ts
export interface ISwapRepository {
create(data: {
creatorId: bigint;
acceptorId?: bigint;
creatorValueCents: bigint;
isPublic: boolean;
message?: string;
expiresAt: Date;
}): Promise<any>;
findById(id: bigint): Promise<any>;
findPendingByCreator(creatorId: bigint): Promise<any[]>;
findPendingByAcceptor(acceptorId: bigint): Promise<any[]>;
findPublicSwaps(): Promise<any[]>;
updateStatus(id: bigint, status: SwapStatus, completedAt?: Date): Promise<any>;
setAcceptor(id: bigint, acceptorId: bigint, acceptorValueCents: bigint): Promise<any>;
addItem(data: {
swapId: bigint;
inventoryItemId: bigint;
isCreatorItem: boolean;
itemValueCents: bigint;
}): Promise<any>;
getSwapItems(swapId: bigint): Promise<any[]>;
deleteExpiredSwaps(): Promise<number>;
}Modulo NestJS
// src/presentation/modules/swap.module.ts
@Module({
imports: [DatabaseModule, RedisModule, LockModule, AuthModule, WebSocketModule, HttpModule],
controllers: [SwapController],
providers: [
PrismaClient,
MoneyService,
WaxpeerService,
WaxpeerSnapshotService,
ExchangeRateService,
ItemNameParserService,
PriceMarginService,
TransactionService,
CreateSwapUseCase,
AcceptSwapUseCase,
CancelSwapUseCase,
DeclineSwapUseCase,
GetMySwapsUseCase,
GetPublicSwapsUseCase,
PerformSiteSwapUseCase,
GetSwapTargetsUseCase,
{
provide: 'ISwapRepository',
useClass: SwapRepository,
},
{
provide: 'IUserInventoryRepository',
useClass: UserInventoryRepository,
},
{
provide: 'IUserRepository',
useClass: UserRepository,
},
],
})
export class SwapModule {}Debugging e Troubleshooting
Logs do Site Swap
O PerformSiteSwapUseCase emite logs estruturados:
[PerformSiteSwapUseCase] [Swap] Target: AK-47 | Fire Serpent (Field-Tested),
Waxpeer: 6400c USD, With margin: 9600c USD, BRL: 50400c (rate: 5.25)Verificar Item com Status IN_SWAP Orfao
Se um item esta preso com status IN_SWAP sem um swap PENDING associado:
-- Buscar itens orfaos
SELECT ui.id, ui.user_id, ui.item_id, ui.status, i.name
FROM user_inventory ui
JOIN items i ON ui.item_id = i.id
WHERE ui.status = 'IN_SWAP'
AND NOT EXISTS (
SELECT 1 FROM swap_items si
JOIN swaps s ON si.swap_id = s.id
WHERE si.inventory_item_id = ui.id
AND s.status = 'PENDING'
);
-- Corrigir (usar com cautela)
UPDATE user_inventory
SET status = 'AVAILABLE'
WHERE id IN (/* IDs dos itens orfaos */);Verificar Locks Ativos
// Verificar se ha locks travados no Redis
const client = redisService.getClient();
const keys = await client.keys('lock:*');
console.log('Active locks:', keys);Verificar Swap Expirado que Nao Foi Limpo
SELECT id, creator_id, status, expires_at
FROM swaps
WHERE status = 'PENDING'
AND expires_at < NOW();Validar Integridade de um Swap Aceito
-- Verificar se todos os itens foram transferidos corretamente
SELECT
s.id AS swap_id,
s.status,
si.inventory_item_id,
si.is_creator_item,
ui.user_id AS current_owner,
ui.status AS item_status,
CASE
WHEN si.is_creator_item AND ui.user_id = s.acceptor_id THEN 'OK'
WHEN NOT si.is_creator_item AND ui.user_id = s.creator_id THEN 'OK'
ELSE 'INCONSISTENTE'
END AS integrity_check
FROM swaps s
JOIN swap_items si ON si.swap_id = s.id
JOIN user_inventory ui ON ui.id = si.inventory_item_id
WHERE s.id = 839201847500
AND s.status = 'ACCEPTED';Se algum registro retornar INCONSISTENTE, a transferencia de itens falhou parcialmente.
Verificar Historico de Site Swaps
SELECT
ss.id,
ss.user_id,
i.name AS target_item,
ss.target_item_value_cents,
ss.total_offered_value_cents,
ss.created_at,
ROUND((ss.total_offered_value_cents - ss.target_item_value_cents)::numeric /
ss.total_offered_value_cents * 100, 2) AS spread_percent
FROM site_swaps ss
JOIN items i ON ss.target_item_id = i.id
ORDER BY ss.created_at DESC
LIMIT 20;Diagnostico de Erro "Swap is no longer available"
Esse erro ocorre quando o usuario tenta aceitar um swap que ja foi aceito por outro usuario (race condition resolvida pelo lock):
- Verificar o status atual do swap no banco.
- Verificar quem aceitou (campo
acceptor_id). - Verificar o timestamp de
completed_atvs o timestamp da tentativa do usuario. - O lock
swap:{swapId}garante que apenas um aceite e processado.
Diagnostico de Erro "Target item is more expensive"
Target item (R$ X) is more expensive than your items (R$ Y).
You cannot receive more than you offer.- Verificar os valores
valueCentsBrldos itens do usuario no inventario. - Verificar o preco atual do item no Waxpeer (
waxpeerService.getItemPrice). - Calcular:
precoWaxpeer * margin / 100 * taxaCambio * 100= targetValueBrl (margin =PriceMarginService.getCurrent(), default 1.5). - Se targetValueBrl > totalUserValueCents, o erro esta correto.
- Solucao: o usuario precisa oferecer mais itens ou escolher um item mais barato.
Diagnostico de Erro "Target item is too cheap"
Target item (R$ X) is too cheap.
Minimum allowed is R$ Y (90% of your R$ Z).- O item alvo vale menos que 90% do total oferecido.
- Solucao: o usuario deve remover itens da oferta ou escolher um item mais caro.
- Range valido: [totalUser * 90%, totalUser * 100%].
Indices de Performance do Banco
A tabela swaps possui 5 indices para otimizar as queries mais comuns:
@@index([creatorId, status]) // findPendingByCreator
@@index([acceptorId, status]) // findPendingByAcceptor
@@index([status, createdAt]) // queries de listagem com ordenacao
@@index([isPublic, status]) // findPublicSwaps
@@index([expiresAt]) // deleteExpiredSwaps / cron de limpezaA tabela swap_items possui 2 indices:
@@index([swapId]) // buscar itens de um swap
@@index([inventoryItemId]) // verificar se item esta em algum swapA tabela site_swaps possui 3 indices:
@@index([userId]) // buscar site swaps de um usuario
@@index([createdAt]) // listagem cronologica
@@index([targetItemValueCents]) // buscar por faixa de valorA tabela site_swap_items possui 2 indices:
@@index([siteSwapId]) // buscar itens de um site swap
@@index([userInventoryId]) // verificar se item foi usado em site swapResumo de Endpoints
| Metodo | Rota | Auth | Descricao |
|---|---|---|---|
| POST | /swap | Sim | Criar proposta de swap |
| PUT | /swap/:id/accept | Sim | Aceitar swap com itens |
| PUT | /swap/:id/cancel | Sim | Cancelar swap (creator) |
| PUT | /swap/:id/decline | Sim | Recusar swap (acceptor) |
| GET | /swap/my-swaps | Sim | Listar meus swaps pendentes |
| GET | /swap/public | Sim | Listar swaps publicos (max 50) |
| POST | /swap/site | Sim | Swap com Waxpeer |
| GET | /swap/targets | Nao | Listar itens Waxpeer com filtros |
