Sistema de Inventario
Documentacao completa do sistema de inventario do projeto. Cobre modelo de dados, transicoes de status, endpoints HTTP, eventos WebSocket, use cases, repositorio, regras de validacao e edge cases.
1. Modelo de Dados
1.1 Enum InventoryItemStatus
Definido em prisma/schema.prisma (linha 414):
enum InventoryItemStatus {
AVAILABLE
LOCKED
SOLD
WITHDRAWN
USED_IN_UPGRADE
USED_IN_BATTLE
IN_SWAP
}Cada valor representa um estado mutuamente exclusivo de um item no inventario do usuario.
1.2 Tabela UserInventory
Definida em prisma/schema.prisma (linha 424):
model UserInventory {
id BigInt @id
userId BigInt @map("user_id")
itemId BigInt @map("item_id")
status InventoryItemStatus @default(AVAILABLE)
acquiredAt DateTime @default(now()) @map("acquired_at")
acquiredFrom String? @map("acquired_from")
lockedUntil DateTime? @map("locked_until")
user User @relation(fields: [userId], references: [id])
item Item @relation(fields: [itemId], references: [id])
swapItems SwapItem[]
siteSwapItems SiteSwapItem[]
waxpeerWithdrawals WaxpeerWithdrawal[]
@@map("user_inventory")
@@index([userId, status])
@@index([itemId])
}Campos:
| Campo | Tipo | Descricao |
|---|---|---|
id | BigInt | Snowflake ID gerado via SnowflakeService.generate() |
userId | BigInt | FK para User.id |
itemId | BigInt | FK para Item.id |
status | InventoryItemStatus | Status atual do item. Default: AVAILABLE |
acquiredAt | DateTime | Timestamp de quando o item entrou no inventario. Default: now() |
acquiredFrom | String? | Origem do item (texto livre). Nullable |
lockedUntil | DateTime? | Data/hora ate quando o item esta travado. Nullable |
Indices de performance:
@@index([userId, status])- Busca por inventario do usuario filtrado por status@@index([itemId])- Busca por item especifico
1.3 Tabela Item (referencia)
Cada UserInventory referencia um Item:
model Item {
id BigInt @id
name String
marketHashName String @unique @map("market_hash_name")
imageUrl String @map("image_url")
rarity Rarity
valueCents BigInt @map("value_cents") // USD cents
valueCentsBrl BigInt @default(0) @map("value_cents_brl") // BRL cents
originalPriceCents BigInt @default(0) @map("original_price_cents")
marginPercent Float @default(20) @map("margin_percent")
weaponCategory WeaponCategory @map("weapon_category")
exterior ItemExterior?
isStatTrak Boolean @default(false) @map("is_stattrak")
}REGRA CRITICA: Todas as operacoes financeiras do inventario (venda, swap, upgrade) utilizam valueCentsBrl (centavos de real). O campo valueCents (USD) e apenas referencia.
1.4 Tabela WaxpeerWithdrawal (withdrawal tracking)
model WaxpeerWithdrawal {
id BigInt @id
userId BigInt @map("user_id")
userInventoryId BigInt @map("user_inventory_id")
itemId BigInt @map("item_id")
waxpeerId String? @unique @map("waxpeer_id")
projectId String @unique @map("project_id")
tradeId String? @map("trade_id")
pricePaidCents BigInt @map("price_paid_cents")
skinValueUsdCents BigInt @default(0) @map("skin_value_usd_cents")
status WaxpeerWithdrawalStatus @default(PENDING)
autoApproved Boolean @default(false) @map("auto_approved")
waxpeerStatus Int? @map("waxpeer_status")
failureReason String? @map("failure_reason")
metadata Json?
sendUntil DateTime? @map("send_until")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
completedAt DateTime? @map("completed_at")
}enum WaxpeerWithdrawalStatus {
PENDING_APPROVAL
PENDING
WAITING_SELLER
TRADE_SENT
COMPLETED
CANCELLED
FAILED
}2. Mapa Completo de Transicoes de Status
2.1 Diagrama de Estados
+-------------------+
| AVAILABLE |
+-------------------+
/ | | \ \
/ | | \ \
v v v v v
+------+ +----+ +----------+ +-------+ +-------+
|LOCKED| |SOLD| |USED_IN_ | |USED_IN| |IN_SWAP|
| | | | |UPGRADE | |BATTLE | | |
+------+ +----+ +----------+ +-------+ +-------+
/ \
v v
+---------+ +---------+
|WITHDRAWN| |AVAILABLE|
+---------+ +---------+
(terminal) (rollback)
IN_SWAP -> AVAILABLE (cancel/decline)2.2 Transicoes Detalhadas
AVAILABLE -> SOLD
Trigger: Venda de item (individual, multipla ou total)
Arquivos:
src/application/use-cases/inventory/sell-inventory-item.use-case.ts(linha 40)src/application/use-cases/inventory/sell-multiple-items.use-case.ts(linha 53)src/application/use-cases/inventory/sell-all-items.use-case.ts(linha 36)
Codigo real:
await this.userInventoryRepository.updateStatus(inventoryItemId, 'SOLD');Estado terminal: Sim. Item vendido nao volta ao inventario.
AVAILABLE -> LOCKED
Trigger: Inicio de withdrawal via Waxpeer ou inicio de upgrade (lock temporario)
Arquivos:
src/application/use-cases/inventory/request-withdraw-item.use-case.ts(linha 269)src/application/use-cases/upgrade/perform-upgrade.use-case.ts(linha 76)
Codigo real (withdrawal):
await this.userInventoryRepository.updateStatus(inventoryItem.id, 'LOCKED');
inventoryItem.status = 'LOCKED';Codigo real (upgrade):
for (const invItem of inventoryItems) {
await this.userInventoryRepository.updateStatus(invItem.id, 'LOCKED');
}LOCKED -> AVAILABLE (rollback)
Trigger: Falha na compra Waxpeer, withdrawal cancelado/falhado, ou erro durante upgrade
Arquivos:
src/application/use-cases/inventory/request-withdraw-item.use-case.ts(linha 335)src/application/services/waxpeer-withdrawal-tracker.service.ts(linha 116)src/infrastructure/external-apis/waxpeer-websocket.service.ts(linha 274)src/application/use-cases/upgrade/perform-upgrade.use-case.ts(linhas 93, 105, 114, 129, 138)
Codigo real (withdrawal falhou):
await this.userInventoryRepository.updateStatus(inventoryItem.id, 'AVAILABLE');
inventoryItem.status = 'AVAILABLE';Codigo real (tracker - cancelado/falhado):
if (newStatus === 'CANCELLED' || newStatus === 'FAILED') {
await this.userInventoryRepository.updateStatus(withdrawal.userInventoryId, 'AVAILABLE');
}Codigo real (upgrade - validacao falhou):
if (userBalance < balanceUsed) {
if (hasItems) {
for (const invItem of inventoryItems) {
await this.userInventoryRepository.updateStatus(invItem.id, 'AVAILABLE');
}
}
throw new BadRequestException('Insufficient balance');
}Cenarios de rollback no upgrade (5 cenarios distintos):
- Saldo insuficiente (
Insufficient balance) - Valor total zero (
Total value must be greater than zero) - Valor total >= item alvo (
Total value must be less than target item value) - Erro no calculo de chance (excecao do
UpgradeProbabilityService) - Chance <= 0 (
Upgrade chance is too low)
LOCKED -> WITHDRAWN
Trigger: Withdrawal completado com sucesso via Waxpeer
Arquivos:
src/application/services/waxpeer-withdrawal-tracker.service.ts(linha 114)src/infrastructure/external-apis/waxpeer-websocket.service.ts(linha 266)
Codigo real:
if (newStatus === 'COMPLETED') {
await this.userInventoryRepository.updateStatus(withdrawal.userInventoryId, 'WITHDRAWN');
}Estado terminal: Sim. Item sacado nao retorna ao inventario.
AVAILABLE -> USED_IN_UPGRADE
Trigger: Upgrade bem-sucedido ou malsucedido (itens de entrada sao consumidos)
Arquivo: src/application/use-cases/upgrade/perform-upgrade.use-case.ts (linha 188)
Codigo real:
for (const invItem of inventoryItems) {
await this.userInventoryRepository.updateStatus(invItem.id, 'USED_IN_UPGRADE');
}Observacao: Primeiro o item vai para LOCKED (validacao), depois para USED_IN_UPGRADE (confirmacao). A transicao efetiva e AVAILABLE -> LOCKED -> USED_IN_UPGRADE.
Estado terminal: Sim. Item consumido em upgrade nao retorna.
AVAILABLE -> USED_IN_BATTLE
Observacao: Este status existe no enum e no DTO mas NAO possui uma transicao explicita no codigo fonte atual. Itens vencidos em batalhas sao distribuidos via userInventoryRepository.create() com acquiredFrom: 'Batalha'. O status USED_IN_BATTLE e reservado para implementacao futura ou e utilizado em fluxo nao mapeado neste momento.
AVAILABLE -> IN_SWAP
Trigger: Criacao de swap (pelo criador) ou aceitacao de swap (pelo aceitante)
Arquivos:
src/application/use-cases/swap/create-swap.use-case.ts(linha 74)src/application/use-cases/swap/accept-swap.use-case.ts(linha 85)src/application/use-cases/swap/perform-site-swap.use-case.ts(linha 168)
Codigo real (create-swap):
for (const item of items) {
if (!item?.item) throw new NotFoundException('Item details not found');
await this.inventoryRepository.updateStatus(item.id, InventoryItemStatus.IN_SWAP);
}Codigo real (accept-swap):
for (const item of items) {
await this.inventoryRepository.updateStatus(item.id, InventoryItemStatus.IN_SWAP);
}Codigo real (site-swap via Prisma direto):
for (const invItem of inventoryItems) {
await tx.userInventory.update({
where: { id: invItem.id },
data: { status: 'IN_SWAP' },
});
}IN_SWAP -> AVAILABLE (cancel/decline)
Trigger: Swap cancelado pelo criador ou recusado pelo destinatario
Arquivos:
src/application/use-cases/swap/cancel-swap.use-case.tssrc/application/use-cases/swap/decline-swap.use-case.ts
Codigo real (cancel):
return this.prisma.$transaction(async (tx) => {
const creatorItems = swap.items?.filter((item: any) => item.isCreatorItem) || [];
for (const swapItem of creatorItems) {
await this.inventoryRepository.updateStatus(
swapItem.inventoryItemId,
InventoryItemStatus.AVAILABLE,
);
}
const acceptorItems = swap.items?.filter((item: any) => !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 };
});Codigo real (decline):
return this.prisma.$transaction(async (tx) => {
const creatorItems = swap.items?.filter((item: any) => 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: No cancel, TODOS os itens (criador + aceitante) voltam para AVAILABLE. No decline, apenas os itens do CRIADOR voltam (pois o aceitante nunca travou itens em decline).
IN_SWAP -> AVAILABLE (swap aceito - troca de dono)
Trigger: Swap aceito com sucesso
Arquivo: src/application/use-cases/swap/accept-swap.use-case.ts
Codigo real:
const creatorItems = freshSwap.items.filter((item: any) => item.isCreatorItem);
for (const swapItem of creatorItems) {
await this.inventoryRepository.updateOwner(
swapItem.inventoryItemId,
userId, // aceitante recebe itens do criador
InventoryItemStatus.AVAILABLE,
'Swap',
);
}
const acceptorItems = items;
for (const item of acceptorItems) {
await this.inventoryRepository.updateOwner(
item.id,
freshSwap.creatorId, // criador recebe itens do aceitante
InventoryItemStatus.AVAILABLE,
'Swap',
);
}Observacao: O metodo updateOwner muda o userId, reseta acquiredAt para new Date(), define acquiredFrom: 'Swap' e seta status para AVAILABLE.
2.3 Tabela Resumo de Transicoes
| De | Para | Gatilho | Reversivel? |
|---|---|---|---|
| AVAILABLE | SOLD | Venda (individual/multipla/total) | Nao |
| AVAILABLE | LOCKED | Inicio de withdrawal ou upgrade | Sim |
| AVAILABLE | USED_IN_UPGRADE | Upgrade executado (apos LOCKED) | Nao |
| AVAILABLE | IN_SWAP | Criacao ou aceitacao de swap | Sim |
| LOCKED | AVAILABLE | Falha no withdrawal ou erro no upgrade | - |
| LOCKED | WITHDRAWN | Withdrawal completado | Nao |
| IN_SWAP | AVAILABLE | Cancel ou decline do swap | - |
3. Tracking de Origem (acquiredFrom)
O campo acquiredFrom registra como o item chegou ao inventario do usuario. Valores utilizados no codigo:
| Valor | Use Case | Arquivo |
|---|---|---|
"Caixa - {nome_da_caixa}" | Abertura de caixa | open-case.use-case.ts (linha 143) |
"Upgrade" | Upgrade bem-sucedido | perform-upgrade.use-case.ts (linha 197) |
"Batalha" | Vitoria em batalha | execute-battle.use-case.ts (linha 616) |
"Swap" | Troca via swap | accept-swap.use-case.ts, perform-site-swap.use-case.ts (linha 178) |
"Sorteio" | Vitoria em sorteio/raffle | draw-raffle.use-case.ts (linha 113) |
"Evento" | Recompensa de evento | claim-event-reward.use-case.ts (linha 50) |
"Evento - Atire na Caixa" | Evento de tiro na caixa | perform-event-shoot.use-case.ts (linha 111) |
Exemplo de criacao com acquiredFrom:
const inventoryItem = await this.userInventoryRepository.create({
userId,
itemId: wonItem.item.id,
status: 'AVAILABLE',
acquiredFrom: `Caixa - ${caseData.name}`,
});4. Endpoints HTTP
Todos os endpoints estao em src/presentation/controllers/inventory.controller.ts. Base path: /inventory Guard: AuthGuard (todos os endpoints requerem autenticacao via Session ID) Tag OpenAPI: Inventory
4.1 GET /inventory
Descricao: Retorna o inventario do usuario com paginacao, filtros e ordenacao.
Query Parameters:
| Parametro | Tipo | Obrigatorio | Default | Descricao |
|---|---|---|---|---|
status | InventoryItemStatus | Nao | - | Filtrar por status |
limit | number | Nao | 15 | Itens por pagina |
offset | number | Nao | 0 | Pular N itens |
sortBy | 'value' | 'acquiredAt' | 'name' | Nao | 'acquiredAt' | Campo de ordenacao |
sortOrder | 'asc' | 'desc' | Nao | 'desc' | Direcao da ordenacao |
search | string | Nao | - | Busca textual no nome do item (case insensitive) |
Response 200 (InventoryListResponseDto):
{
items: [
{
id: string, // Snowflake ID do registro de inventario
itemId: string, // Snowflake ID do item
itemName: string, // Ex: "AK-47 | Redline"
itemImage: string, // URL da imagem
itemRarity: string, // Ex: "COVERT"
itemValueCents: string, // valueCentsBrl em string. Ex: "15900"
itemValueFormatted: string, // Ex: "R$ 159,00"
weaponCategory: string, // Ex: "RIFLE"
exterior?: string, // Ex: "FIELD_TESTED". Pode ser undefined
isStatTrak: boolean,
status: string, // Ex: "AVAILABLE"
acquiredAt: Date,
acquiredFrom?: string, // Ex: "Caixa - Snow". Pode ser undefined
lockedUntil?: Date, // Pode ser undefined
}
],
total: number, // Total de itens com filtro aplicado
limit: number,
offset: number,
hasMore: boolean, // offset + limit < total
totalAvailableCount: number, // Total de itens AVAILABLE (sem filtro de search)
totalAvailableValueCents: string, // Soma de valueCentsBrl de todos AVAILABLE
totalAvailableValueFormatted: string, // Ex: "R$ 1.234,56"
}Observacao sobre ordenacao por valor: Quando sortBy === 'value', a ordenacao usa item.valueCentsBrl (BRL, nao USD).
Use Case: GetUserInventoryUseCase (delegacao direta ao repositorio com filtros)
4.2 DELETE /inventory/:id/sell
Descricao: Vende um unico item do inventario. Credita valueCentsBrl integral (sem taxa).
Parametros de rota:
| Parametro | Tipo | Descricao |
|---|---|---|
id | string | Snowflake ID do item de inventario |
Response 200 (SellItemResponseDto):
{
success: true,
itemsSold: 1,
totalValueCents: string, // valueCentsBrl do item. Ex: "15900"
totalValueFormatted: string, // Ex: "R$ 159,00"
}Response 404: "Inventory item not found" ou "Item details not found"Response 400: "Item does not belong to user" ou "Item is not available (status: LOCKED)"
Use Case: SellInventoryItemUseCase
4.3 POST /inventory/sell-multiple
Descricao: Vende multiplos itens em uma unica transacao atomica.
Request Body (SellMultipleItemsDto):
{
inventoryItemIds: string[] // Array de Snowflake IDs. Min: 1, Max: 100
}Response 200 (SellItemResponseDto):
{
success: true,
itemsSold: number, // Quantidade vendida
totalValueCents: string, // Soma de valueCentsBrl de todos
totalValueFormatted: string, // Ex: "R$ 1.234,56"
}Response 400: "No items selected", "Maximum 100 items per batch", "Item does not belong to user", "Item {id} is not available (status: ...)", "Item details not found"Response 404: "Inventory item not found"
Use Case: SellMultipleItemsUseCase
4.4 POST /inventory/sell-all
Descricao: Vende TODOS os itens com status AVAILABLE do usuario.
Request Body: Nenhum.
Response 200 (SellItemResponseDto):
{
success: true,
itemsSold: number,
totalValueCents: string,
totalValueFormatted: string,
}Response 400: "No items available to sell"
Use Case: SellAllItemsUseCase
4.5 POST /inventory/:id/withdraw
Descricao: Inicia o processo de withdrawal (saque) de um item via Waxpeer. O endpoint verifica o preco e, se canWithdraw, trava o item e cria o registro de saque. A partir dai ha um branch de auto-aprovacao: se elegivel (autoApproved: true), a compra na Waxpeer roda em seguida; se nao elegivel (PENDING_APPROVAL), o saque aguarda revisao de um admin.
Parametros de rota:
| Parametro | Tipo | Descricao |
|---|---|---|
id | string | Snowflake ID do item de inventario |
Response 200:
{
success: boolean, // Se a compra Waxpeer (auto) foi bem-sucedida; false no fluxo admin
canWithdraw: boolean, // Se todas as condicoes de saque foram atendidas
inventoryItem: {
id: string,
userId: string,
itemId: string,
status: string, // "LOCKED" se withdrawal criado (auto ou pending_approval); "AVAILABLE" se nao pode sacar
acquiredAt: Date,
acquiredFrom: string | null,
},
item: {
id: string,
name: string,
marketHashName: string,
imageUrl: string,
rarity: string,
valueCentsBrl: string,
originalPriceCents: string, // Preco original em USD cents
marginPercent: number,
weaponCategory: string,
exterior: string | null,
isStatTrak: boolean,
},
waxpeerPrice: { // Pode ser null
priceCentsUsd: number,
priceWaxpeerCoins: number,
count: number,
isExactMatch: boolean,
imageUrl: string,
} | null,
priceComparison: { // Pode ser null
ourPriceCentsUsd: number, // valueCents do item (USD, com margem)
ourPriceUsd: string,
maxPayCents: number, // Teto = ceil(valueCents / priceMargin)
maxPayUsd: string,
waxpeerMinPriceCents: number,
waxpeerMinPriceUsd: string,
marginPercent?: number, // (1 - 1/priceMargin) * 100
waxpeerCount: number,
hasStock: boolean,
isPriceValid: boolean, // waxpeerMin <= maxPayCents
canWithdraw: boolean,
canSwap: boolean,
failedConditions: string[],
reason: string,
userMessage: string,
} | null,
purchase: { // Pode ser null (fluxo admin ou nao comprou)
success: boolean,
waxpeerId?: string,
projectId?: string,
pricePaid?: number,
pricePaidUsd?: string,
name?: string,
sendUntil?: number, // Unix timestamp
averageTime?: number,
error?: string,
} | null,
withdrawalId?: string, // ID do registro WaxpeerWithdrawal
autoApproved?: boolean, // true = auto-aprovado (PENDING); false = revisao admin (PENDING_APPROVAL)
}Response 404: "Item not found in inventory", "Item details not found", "Usuario nao encontrado"Response 400: "Item does not belong to this user", "Item is not available for withdrawal. Current status: ...", "This item already has a pending withdrawal. Status: ...", "Cadastre seu Trade URL do Steam no perfil antes de retirar itens.", "Trade URL invalido. Atualize no perfil."Response 409: "Este item ja esta sendo processado. Por favor, aguarde."
Use Case: RequestWithdrawItemUseCase
Condicoes para withdrawal ser executado:
- Item encontrado e pertence ao usuario
- Item nao e
restrictedToGames(itens da Loja nao podem ser sacados) - Status do item e AVAILABLE
- Nao existe withdrawal ativo para este item
- Usuario tem
tradeUrlcadastrado - Trade URL segue o regex:
^https://steamcommunity\.com/tradeoffer/new/\?partner=(\d+)&token=([a-zA-Z0-9_-]+)$ - Item encontrado no Waxpeer (
isExactMatch === true) - Waxpeer tem estoque (
count >= 1) - Preco Waxpeer <=
maxPayCents(isPriceValid === true), ondemaxPayCents = ceil(valueCents / priceMargin)
4.6 GET /inventory/withdrawals/active
Descricao: Retorna withdrawals ativos do usuario (status != COMPLETED/CANCELLED/FAILED).
Response 200:
{
withdrawals: [
{
id: string,
inventoryItemId: string,
itemId: string,
itemName: string,
itemImage: string,
itemRarity: string,
itemValueCents: string, // valueCentsBrl
projectId: string,
waxpeerId: string | null,
tradeId: string | null,
status: WaxpeerWithdrawalStatus,
waxpeerStatus: number | null,
pricePaidCents: string, // USD cents pago no Waxpeer
sendUntil: Date | null,
createdAt: Date,
}
]
}4.7 POST /inventory/withdrawals/:id/refresh
Descricao: Forca atualizacao do status de um withdrawal especifico consultando a Waxpeer.
Parametros de rota:
| Parametro | Tipo | Descricao |
|---|---|---|
id | string | ID do WaxpeerWithdrawal |
Response 200:
{
id: string,
status: WaxpeerWithdrawalStatus,
waxpeerStatus: number | null,
tradeId: string | null,
}Erro: "Withdrawal not found" se nao existe ou nao pertence ao usuario.
4.8 GET /inventory/withdrawals/history
Descricao: Retorna historico completo de withdrawals com paginacao.
Query Parameters:
| Parametro | Tipo | Obrigatorio | Default |
|---|---|---|---|
limit | number | Nao | 50 |
offset | number | Nao | 0 |
Response 200:
{
withdrawals: [
{
id: string,
inventoryItemId: string,
itemId: string,
itemName: string,
itemImage: string,
itemRarity: string,
itemValueCents: string,
projectId: string,
waxpeerId: string | null,
tradeId: string | null,
status: WaxpeerWithdrawalStatus,
waxpeerStatus: number | null,
pricePaidCents: string,
failureReason: string | null,
sendUntil: Date | null,
createdAt: Date,
completedAt: Date | null,
}
]
}5. Use Cases
5.1 SellInventoryItemUseCase
Arquivo: src/application/use-cases/inventory/sell-inventory-item.use-case.ts
Dependencias:
PrismaClient- Transacao atomicaLockService- Distributed lock via RedlockIUserInventoryRepository- Acesso ao inventarioTransactionService- Double-entry bookkeepingWebSocketGatewayService- Notificacao em tempo real
Fluxo completo:
- Busca item por ID (
findById) com include doItem - Valida existencia do item de inventario
- Valida existencia do item base (
itemrelation) - Valida ownership (
inventoryItem.userId !== userId) - Valida status (
!== 'AVAILABLE') - Adquire distributed lock:
user:balance:{userId}(TTL: 10s) - Dentro de
$transaction:- Atualiza status para
SOLD - Credita
item.valueCentsBrlintegral viaTransactionService.credit() - Obtem novo saldo via
TransactionService.getUserBalance() - Emite
balance:updatedvia WebSocket
- Atualiza status para
- Retorna
{ inventoryItem, valueCents: item.valueCentsBrl }
Codigo completo:
async execute(userId: bigint, inventoryItemId: bigint) {
const inventoryItem: any = await this.userInventoryRepository.findById(inventoryItemId);
if (!inventoryItem) {
throw new NotFoundException('Inventory item not found');
}
if (!inventoryItem?.item) {
throw new NotFoundException('Item details not found');
}
if (inventoryItem.userId !== userId) {
throw new BadRequestException('Item does not belong to user');
}
if (inventoryItem.status !== 'AVAILABLE') {
throw new BadRequestException(`Item is not available (status: ${inventoryItem.status})`);
}
return this.lockService.withUserBalanceLock(userId, async () => {
return this.prisma.$transaction(async (tx) => {
await this.userInventoryRepository.updateStatus(inventoryItemId, 'SOLD');
await this.transactionService.credit(
userId,
inventoryItem.item.valueCentsBrl,
`Sold item: ${inventoryItem.item.name}`,
);
const newBalanceCents = await this.transactionService.getUserBalance(userId);
this.webSocketGateway.emitBalanceUpdate(userId, newBalanceCents);
return {
inventoryItem,
valueCents: inventoryItem.item.valueCentsBrl,
};
});
});
}REGRA: O valor creditado e valueCentsBrl (BRL) INTEGRAL. Nao ha taxa de venda.
5.2 SellMultipleItemsUseCase
Arquivo: src/application/use-cases/inventory/sell-multiple-items.use-case.ts
Fluxo completo:
- Valida que array nao esta vazio
- Valida que array tem no maximo 100 itens
- Busca todos os itens em paralelo (
Promise.all) - Valida cada item: existencia, ownership, status AVAILABLE
- Adquire distributed lock:
user:balance:{userId}(TTL: 10s) - Dentro de
$transaction:- Calcula
totalValueCentssomandovalueCentsBrlde cada item - Atualiza cada item para
SOLD - Credita
totalValueCentscomo transacao unica
- Calcula
- Retorna
{ itemsSold, totalValueCents, items }
Codigo completo:
async execute(userId: bigint, inventoryItemIds: bigint[]) {
if (inventoryItemIds.length === 0) {
throw new BadRequestException('No items selected');
}
if (inventoryItemIds.length > 100) {
throw new BadRequestException('Maximum 100 items per batch');
}
const inventoryItems: any[] = await Promise.all(
inventoryItemIds.map((id) => this.userInventoryRepository.findById(id)),
);
for (const invItem of inventoryItems) {
if (!invItem) {
throw new NotFoundException('Inventory item not found');
}
if (invItem.userId !== userId) {
throw new BadRequestException('Item does not belong to user');
}
if (invItem.status !== 'AVAILABLE') {
throw new BadRequestException(
`Item ${invItem.id} is not available (status: ${invItem.status})`,
);
}
}
return this.lockService.withUserBalanceLock(userId, async () => {
return this.prisma.$transaction(async (tx) => {
const totalValueCents = inventoryItems.reduce((sum, item: any) => {
if (!item?.item) throw new NotFoundException('Item details not found');
return sum + item.item.valueCentsBrl;
}, BigInt(0));
for (const invItem of inventoryItems) {
await this.userInventoryRepository.updateStatus(invItem!.id, 'SOLD');
}
await this.transactionService.credit(
userId,
totalValueCents,
`Sold ${inventoryItems.length} items`,
);
return {
itemsSold: inventoryItems.length,
totalValueCents,
items: inventoryItems,
};
});
});
}Diferenca do SellAll: Este use case NAO emite balance:updated via WebSocket. O controller tambem nao o faz.
Limite de batch: Maximo 100 itens por requisicao.
5.3 SellAllItemsUseCase
Arquivo: src/application/use-cases/inventory/sell-all-items.use-case.ts
Fluxo completo:
- Busca todos os itens do usuario com status
AVAILABLE - Valida que existe pelo menos 1 item
- Adquire distributed lock:
user:balance:{userId}(TTL: 10s) - Dentro de
$transaction:- Calcula
totalValueCentssomandovalueCentsBrl - Atualiza CADA item para
SOLD - Credita
totalValueCentscomo transacao unica - Emite
balance:updatedvia WebSocket
- Calcula
- Retorna
{ itemsSold, totalValueCents }
Codigo completo:
async execute(userId: bigint) {
const inventoryItems = await this.userInventoryRepository.findByUserId(userId, {
status: 'AVAILABLE',
});
if (inventoryItems.length === 0) {
throw new BadRequestException('No items available to sell');
}
return this.lockService.withUserBalanceLock(userId, async () => {
return this.prisma.$transaction(async (tx) => {
const totalValueCents = inventoryItems.reduce((sum, item: any) => {
if (!item?.item) return sum;
return sum + item.item.valueCentsBrl;
}, BigInt(0));
for (const invItem of inventoryItems) {
await this.userInventoryRepository.updateStatus(invItem.id, 'SOLD');
}
await this.transactionService.credit(
userId,
totalValueCents,
`Sold all ${inventoryItems.length} items`,
);
const newBalanceCents = await this.transactionService.getUserBalance(userId);
this.webSocketGateway.emitBalanceUpdate(userId, newBalanceCents);
return {
itemsSold: inventoryItems.length,
totalValueCents,
};
});
});
}Observacao: Itens sem item relation (orfaos) sao ignorados no calculo (if (!item?.item) return sum;), mas ainda sao atualizados para SOLD.
5.4 GetUserInventoryUseCase
Arquivo: src/application/use-cases/inventory/get-user-inventory.use-case.ts
Delegacao pura ao repositorio:
async execute(
userId: bigint,
filters?: {
status?: InventoryItemStatus;
limit?: number;
offset?: number;
sortBy?: 'value' | 'acquiredAt' | 'name';
sortOrder?: 'asc' | 'desc';
search?: string;
},
) {
return this.userInventoryRepository.findByUserId(userId, filters);
}5.5 RequestWithdrawItemUseCase
Arquivo: src/application/use-cases/inventory/request-withdraw-item.use-case.ts
Fluxo completo:
- Adquire distributed lock:
withdrawal:item:{inventoryItemId}(TTL: 30s) - Busca item de inventario
- Valida ownership,
restrictedToGames(itens da Loja nao podem ser sacados) e status AVAILABLE - Verifica se ja existe withdrawal ativo para este item
- Busca dados do usuario (tradeUrl)
- Valida formato da Trade URL via regex
- Busca item base para obter
valueCents(USD) emarketHashName - Calcula
maxPayCents = ceil(valueCents / priceMargin)(PriceMarginService.getCurrent()) - Consulta preco no Waxpeer (
waxpeerService.getItemPrice) - Compara precos e monta
PriceComparison - Se
canWithdraw === true, dentro dewithAutoApprovalLock(userId):- Avalia elegibilidade (
autoApprovalEvaluator.evaluate) - Gera
projectId:wd_{inventoryItemId}_{timestamp}e atualiza item paraLOCKED - Elegivel → cria
WaxpeerWithdrawalstatus: PENDING,autoApproved: true; depoisrunAutoPurchase(viaWaxpeerPurchaseExecutorService, fora do lock): compra OK →WAITING_SELLER+withdrawal:waiting_seller; falha →FAILED, itemAVAILABLE,withdrawal:failed - Nao elegivel → cria
WaxpeerWithdrawalstatus: PENDING_APPROVAL,autoApproved: false; emitewithdrawal:pending_approvale auditaSKIN_WITHDRAWAL_REQUESTED(aguarda aprovacao do admin)
- Avalia elegibilidade (
Regex de Trade URL:
const TRADE_URL_REGEX =
/^https:\/\/steamcommunity\.com\/tradeoffer\/new\/\?partner=(\d+)&token=([a-zA-Z0-9_-]+)$/;Condicoes do PriceComparison:
const valueCentsUsd = Number(item.valueCents);
const priceMargin = await this.priceMarginService.getCurrent();
const maxPayCents = Math.ceil(valueCentsUsd / priceMargin);
const hasStock = waxpeerCount >= 1;
const isPriceValid = waxpeerMinPriceCents <= maxPayCents;
const canWithdraw = waxpeerPrice.isExactMatch && hasStock && isPriceValid;6. Interface do Repositorio
Arquivo: src/domain/repositories/user-inventory.repository.interface.ts
export interface IUserInventoryRepository {
create(data: {
userId: bigint;
itemId: bigint;
status?: InventoryItemStatus;
acquiredFrom?: string;
}): Promise<UserInventory>;
findById(id: bigint): Promise<UserInventory | null>;
findByUserId(
userId: bigint,
filters?: {
status?: InventoryItemStatus;
limit?: number;
offset?: number;
sortBy?: 'value' | 'acquiredAt' | 'name';
sortOrder?: 'asc' | 'desc';
search?: string;
},
): Promise<UserInventory[]>;
updateStatus(
id: bigint,
status: InventoryItemStatus,
lockedUntil?: Date,
): Promise<UserInventory>;
deleteById(id: bigint): Promise<void>;
countByUserId(userId: bigint, status?: InventoryItemStatus): Promise<number>;
countByUserIdWithFilters(
userId: bigint,
filters?: {
status?: InventoryItemStatus;
search?: string;
},
): Promise<number>;
updateOwner(
id: bigint,
newUserId: bigint,
status: InventoryItemStatus,
acquiredFrom?: string,
): Promise<UserInventory>;
getTotalValueByUserId(
userId: bigint,
status?: InventoryItemStatus,
): Promise<bigint>;
}7. Implementacao do Repositorio
Arquivo: src/infrastructure/database/repositories/user-inventory.repository.ts
7.1 create
async create(data: {
userId: bigint;
itemId: bigint;
status?: InventoryItemStatus;
acquiredFrom?: string;
}) {
return this.prisma.userInventory.create({
data: {
id: this.snowflake.generate(),
userId: data.userId,
itemId: data.itemId,
status: data.status || 'AVAILABLE',
acquiredFrom: data.acquiredFrom,
},
});
}7.2 findById
async findById(id: bigint) {
return this.prisma.userInventory.findUnique({
where: { id },
include: {
item: true,
},
});
}Sempre inclui a relation item para ter acesso a valueCentsBrl, name, etc.
7.3 findByUserId
async findByUserId(
userId: bigint,
filters?: {
status?: InventoryItemStatus;
limit?: number;
offset?: number;
sortBy?: 'value' | 'acquiredAt' | 'name';
sortOrder?: 'asc' | 'desc';
search?: string;
},
) {
const sortBy = filters?.sortBy || 'acquiredAt';
const sortOrder = filters?.sortOrder || 'desc';
let orderBy: any;
if (sortBy === 'value') {
orderBy = { item: { valueCentsBrl: sortOrder } };
} else if (sortBy === 'name') {
orderBy = { item: { name: sortOrder } };
} else {
orderBy = { acquiredAt: sortOrder };
}
const items = await this.prisma.userInventory.findMany({
where: {
userId,
...(filters?.status && { status: filters.status }),
...(filters?.search && {
item: {
name: {
contains: filters.search,
mode: 'insensitive',
},
},
}),
},
include: {
item: true,
},
orderBy,
take: filters?.limit,
skip: filters?.offset,
});
return items;
}Busca textual: Usa contains + mode: 'insensitive' no campo item.name.
7.4 updateStatus
async updateStatus(id: bigint, status: InventoryItemStatus, lockedUntil?: Date) {
return this.prisma.userInventory.update({
where: { id },
data: {
status,
...(lockedUntil && { lockedUntil }),
},
});
}O campo lockedUntil so e setado se fornecido. Nao e limpo automaticamente ao voltar para AVAILABLE.
7.5 updateOwner
async updateOwner(
id: bigint,
newUserId: bigint,
status: InventoryItemStatus,
acquiredFrom?: string,
) {
return this.prisma.userInventory.update({
where: { id },
data: {
userId: newUserId,
status,
acquiredAt: new Date(),
...(acquiredFrom && { acquiredFrom }),
},
});
}Muda userId, reseta acquiredAt e opcionalmente acquiredFrom. Usado no fluxo de swap.
7.6 getTotalValueByUserId
async getTotalValueByUserId(userId: bigint, status?: InventoryItemStatus): Promise<bigint> {
const result = await this.prisma.userInventory.findMany({
where: {
userId,
...(status && { status }),
},
include: {
item: {
select: {
valueCentsBrl: true,
},
},
},
});
return result.reduce((sum, inv) => sum + inv.item.valueCentsBrl, BigInt(0));
}Soma valueCentsBrl (BRL) de todos os itens filtrados.
7.7 deleteById
async deleteById(id: bigint): Promise<void> {
await this.prisma.userInventory.delete({
where: { id },
});
}Hard delete. Metodo existe na interface mas nao e utilizado nos fluxos normais de inventario.
7.8 countByUserId
async countByUserId(userId: bigint, status?: InventoryItemStatus) {
return this.prisma.userInventory.count({
where: {
userId,
...(status && { status }),
},
});
}7.9 countByUserIdWithFilters
async countByUserIdWithFilters(
userId: bigint,
filters?: {
status?: InventoryItemStatus;
search?: string;
},
) {
return this.prisma.userInventory.count({
where: {
userId,
...(filters?.status && { status: filters.status }),
...(filters?.search && {
item: {
name: {
contains: filters.search,
mode: 'insensitive',
},
},
}),
},
});
}8. DTOs
Arquivo: src/application/dto/inventory.dto.ts
8.1 GetInventoryQueryDto
export class GetInventoryQueryDto {
@IsOptional()
@IsEnum([
'AVAILABLE', 'LOCKED', 'SOLD', 'WITHDRAWN',
'USED_IN_UPGRADE', 'USED_IN_BATTLE', 'IN_SWAP',
])
status?: 'AVAILABLE' | 'LOCKED' | 'SOLD' | 'WITHDRAWN'
| 'USED_IN_UPGRADE' | 'USED_IN_BATTLE' | 'IN_SWAP';
@IsOptional()
@Type(() => Number)
limit?: number;
@IsOptional()
@Type(() => Number)
offset?: number;
@IsOptional()
@IsEnum(['value', 'acquiredAt', 'name'])
sortBy?: 'value' | 'acquiredAt' | 'name';
@IsOptional()
@IsEnum(['asc', 'desc'])
sortOrder?: 'asc' | 'desc';
@IsOptional()
search?: string;
}8.2 SellMultipleItemsDto
export class SellMultipleItemsDto {
@IsArray()
@ArrayMinSize(1)
@IsString({ each: true })
inventoryItemIds: string[];
}8.3 InventoryItemResponseDto
export class InventoryItemResponseDto {
id: string;
itemId: string;
itemName: string;
itemImage: string;
itemRarity: string;
itemValueCents: string;
itemValueFormatted: string;
weaponCategory: string;
exterior?: string;
isStatTrak: boolean;
status: string;
acquiredAt: Date;
acquiredFrom?: string;
lockedUntil?: Date;
}8.4 SellItemResponseDto
export class SellItemResponseDto {
success: boolean;
itemsSold: number;
totalValueCents: string;
totalValueFormatted: string;
}8.5 InventoryListResponseDto
export class InventoryListResponseDto {
items: InventoryItemResponseDto[];
total: number;
limit: number;
offset: number;
hasMore: boolean;
totalAvailableCount: number;
totalAvailableValueCents: string;
totalAvailableValueFormatted: string;
}9. Eventos WebSocket
Todos os eventos sao emitidos para a sala privada user:{userId} (apenas o usuario autenticado recebe).
9.1 balance:updated
Trigger: Venda individual (SellInventoryItemUseCase) e venda total (SellAllItemsUseCase).
ATENCAO: SellMultipleItemsUseCase NAO emite este evento.
Payload:
{
balanceCents: string // Novo saldo em centavos BRL. Ex: "186321"
}Codigo de emissao:
const newBalanceCents = await this.transactionService.getUserBalance(userId);
this.webSocketGateway.emitBalanceUpdate(userId, newBalanceCents);9.2 withdrawal:pending
Trigger: Inicio do processo de withdrawal, logo antes de executar a compra no Waxpeer.
Payload:
{
inventoryItemId: string, // ID do UserInventory
itemName: string, // Nome do item
projectId: string, // ID do projeto no formato "wd_{inventoryItemId}_{timestamp}"
}9.3 withdrawal:waiting_seller
Trigger: Compra bem-sucedida no Waxpeer, aguardando vendedor enviar trade.
Payload:
{
inventoryItemId: string,
sellerName?: string, // Nome do vendedor no Waxpeer (pode ser undefined)
sendUntil?: number, // Unix timestamp ate quando o vendedor deve enviar
}9.4 withdrawal:processing
Trigger: Status intermediario de processamento.
Payload:
{
inventoryItemId: string,
waxpeerId?: string,
sendUntil?: number,
}9.5 withdrawal:trade_sent
Trigger: Vendedor enviou a trade offer no Steam.
Payload:
{
inventoryItemId: string,
tradeId?: string, // ID do trade no Steam
}9.6 withdrawal:completed
Trigger: Withdrawal finalizado com sucesso. Item foi entregue ao usuario no Steam.
Payload:
{
inventoryItemId: string,
itemName: string,
}Efeito colateral: Status do UserInventory muda para WITHDRAWN.
9.7 withdrawal:cancelled
Trigger: Withdrawal cancelado (pelo vendedor, timeout, etc).
Payload:
{
inventoryItemId: string,
reason: string, // Ex: "Cancelado pelo vendedor ou timeout"
}Efeito colateral: Status do UserInventory volta para AVAILABLE.
9.8 withdrawal:failed
Trigger: Falha no processamento do withdrawal.
Payload:
{
inventoryItemId: string,
reason: string, // Ex: "Falha no processamento"
}Efeito colateral: Status do UserInventory volta para AVAILABLE.
10. Distributed Locks
O sistema utiliza Redlock para prevenir race conditions em operacoes concorrentes.
10.1 User Balance Lock
Chave: user:balance:{userId}TTL: 10.000ms (10 segundos) Usado em: SellInventoryItemUseCase, SellMultipleItemsUseCase, SellAllItemsUseCase
return this.lockService.withUserBalanceLock(userId, async () => {
return this.prisma.$transaction(async (tx) => {
// operacao atomica
});
});10.2 Withdrawal Lock
Chave: withdrawal:item:{inventoryItemId}TTL: 30.000ms (30 segundos) Usado em: RequestWithdrawItemUseCase
return await this.lockService.withWithdrawalLock(inventoryItemId, async () => {
return await this.executeWithdrawal(userId, inventoryItemId);
});Se o lock nao pode ser adquirido (ExecutionError), retorna HTTP 409: "Este item ja esta sendo processado. Por favor, aguarde."
11. Transacao Financeira (Double-Entry)
Quando um item e vendido, o TransactionService.credit() cria um lancamento double-entry:
await this.transactionService.credit(
userId,
inventoryItem.item.valueCentsBrl,
`Sold item: ${inventoryItem.item.name}`,
);Isso gera:
- DEBITO na conta do sistema (
SYSTEM_USER_ID) - CREDITO na conta do usuario (
userId) - Valor:
valueCentsBrlintegral (sem taxa) - Reason:
"Sold item: {itemName}"ou"Sold all {N} items"ou"Sold {N} items"
12. Edge Cases e Regras de Validacao
12.1 Venda
- Item sem relacao
item: Na venda individual, lancaNotFoundException('Item details not found'). Na venda total (sell-all), itens sem relacao sao IGNORADOS no calculo de valor mas ainda marcados como SOLD. Na venda multipla, lancaNotFoundExceptiondentro da transacao. - Item com status diferente de AVAILABLE: Lanca
BadRequestExceptioncom mensagem incluindo o status atual. - Item de outro usuario: Lanca
BadRequestException('Item does not belong to user'). - Inventario vazio (sell-all): Lanca
BadRequestException('No items available to sell'). - Batch vazio (sell-multiple): Lanca
BadRequestException('No items selected'). - Batch > 100 itens: Lanca
BadRequestException('Maximum 100 items per batch'). - Concorrencia: Distributed lock impede duas operacoes de saldo simultaneas para o mesmo usuario.
12.2 Withdrawal
- Trade URL ausente: Lanca
BadRequestException('Cadastre seu Trade URL do Steam no perfil antes de retirar itens.'). - Trade URL formato invalido: Lanca
BadRequestException('Trade URL invalido. Atualize no perfil.'). - Withdrawal duplicado: Verifica
findActiveByUserInventoryId. Se existe, lancaBadRequestException('This item already has a pending withdrawal. Status: ...'). - Item da Loja (
restrictedToGames): LancaBadRequestException— itens restritos a jogos nao podem ser sacados para a Steam. - Item sem estoque no Waxpeer:
canWithdrawserafalse. Nao tenta comprar. - Preco Waxpeer acima do teto (
maxPayCents):isPriceValidserafalse. Nao tenta comprar. - Item nao encontrado no Waxpeer (match inexato):
isExactMatchserafalse. Nao tenta comprar. - Erro na API Waxpeer:
priceComparisone preenchido com valores zerados ecanWithdraw: false. - Compra falhou: Item volta para AVAILABLE, withdrawal marcado como FAILED, WebSocket emite
withdrawal:failed. - Lock nao adquirido (item sendo processado): HTTP 409.
- Compra OK mas vendedor cancela depois: O
WaxpeerWithdrawalTrackerServiceouWaxpeerWebSocketServicedetecta, atualiza status e devolve item para AVAILABLE.
12.3 Swap
- Cancel so pelo criador:
ForbiddenException('Only the creator can cancel this swap'). - Decline: Apenas se
!isPublic && swap.acceptorId !== userId. - Apenas swaps PENDING podem ser cancelados/recusados:
BadRequestException('Can only cancel/decline pending swaps'). - No cancel: Itens do criador E aceitante voltam para AVAILABLE.
- No decline: Apenas itens do criador voltam para AVAILABLE.
13. Fluxo de Withdrawal (Waxpeer) - Ciclo de Vida Completo
13.1 Mapeamento de Status Waxpeer -> Status Interno
Definido em waxpeer-withdrawal-tracker.service.ts e waxpeer-websocket.service.ts:
| Waxpeer Status (numerico) | Condicao | Status Interno |
|---|---|---|
| 0, 1 | - | WAITING_SELLER |
| 2, 3, 4 | - | TRADE_SENT |
| 5 | - | COMPLETED |
| 6 | reason contem "cancel"/"failed"/"declined" | CANCELLED |
| 6 | outros reasons | FAILED |
| qualquer | is_released === true | COMPLETED |
| qualquer | stage === 9 | COMPLETED |
| qualquer | done === true && status !== 6 | COMPLETED |
13.2 Efeito no UserInventory por Status do Withdrawal
| Status Withdrawal | Efeito no UserInventory |
|---|---|
PENDING_APPROVAL | LOCKED (aguardando aprovacao do admin) |
PENDING | LOCKED (ja feito no request) |
WAITING_SELLER | Permanece LOCKED |
TRADE_SENT | Permanece LOCKED |
COMPLETED | -> WITHDRAWN |
CANCELLED | -> AVAILABLE (rollback) |
FAILED | -> AVAILABLE (rollback) |
13.3 Dois Canais de Atualizacao
O withdrawal pode ser atualizado por dois mecanismos:
Polling (
WaxpeerWithdrawalTrackerService): Consulta periodica via API REST do Waxpeer. UsaforceCheckWithdrawal()para refresh manual.WebSocket (
WaxpeerWebSocketService): Recebe eventos em tempo real do Waxpeer. Mais rapido, mas depende da conexao WebSocket estar ativa.
Ambos os canais executam a mesma logica: mapear status Waxpeer para status interno, atualizar WaxpeerWithdrawal, atualizar UserInventory e emitir evento WebSocket.
14. Formatacao de Valores
O MoneyService e utilizado no controller para formatar valores para exibicao:
format(cents: bigint, locale = 'pt-BR', currency = 'BRL'): string {
const reais = this.toReais(cents);
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
}).format(reais);
}Exemplo: MoneyService.format(15900n) -> "R$ 159,00"
Campos formatados nos responses:
itemValueFormatted(lista de inventario)totalValueFormatted(venda)totalAvailableValueFormatted(lista de inventario)
15. Arquivos Relacionados
| Arquivo | Responsabilidade |
|---|---|
prisma/schema.prisma | Definicao do modelo UserInventory, enum InventoryItemStatus, WaxpeerWithdrawal |
src/domain/repositories/user-inventory.repository.interface.ts | Interface do repositorio |
src/infrastructure/database/repositories/user-inventory.repository.ts | Implementacao Prisma do repositorio |
src/application/dto/inventory.dto.ts | DTOs de request e response |
src/application/use-cases/inventory/get-user-inventory.use-case.ts | Use case de listagem |
src/application/use-cases/inventory/sell-inventory-item.use-case.ts | Use case de venda individual |
src/application/use-cases/inventory/sell-multiple-items.use-case.ts | Use case de venda multipla |
src/application/use-cases/inventory/sell-all-items.use-case.ts | Use case de venda total |
src/application/use-cases/inventory/request-withdraw-item.use-case.ts | Use case de withdrawal |
src/presentation/controllers/inventory.controller.ts | Controller HTTP |
src/infrastructure/websocket/websocket.gateway.ts | Emissao de eventos WebSocket |
src/application/services/waxpeer-withdrawal-tracker.service.ts | Tracking de withdrawal via polling |
src/infrastructure/external-apis/waxpeer-websocket.service.ts | Tracking de withdrawal via WebSocket |
src/infrastructure/locks/lock.service.ts | Distributed locks (Redlock) |
src/application/services/transaction.service.ts | Double-entry bookkeeping |
src/application/services/money.service.ts | Formatacao de valores monetarios |
src/application/use-cases/swap/create-swap.use-case.ts | Swap: marca itens como IN_SWAP |
src/application/use-cases/swap/accept-swap.use-case.ts | Swap: troca ownership, marca IN_SWAP e AVAILABLE |
src/application/use-cases/swap/cancel-swap.use-case.ts | Swap: reverte itens para AVAILABLE |
src/application/use-cases/swap/decline-swap.use-case.ts | Swap: reverte itens do criador para AVAILABLE |
src/application/use-cases/swap/perform-site-swap.use-case.ts | Site swap: marca IN_SWAP, cria novo item AVAILABLE |
src/application/use-cases/upgrade/perform-upgrade.use-case.ts | Upgrade: LOCKED -> USED_IN_UPGRADE ou AVAILABLE |
src/application/use-cases/case-opening/open-case.use-case.ts | Cria item com acquiredFrom "Caixa - {nome}" |
src/application/use-cases/battle/execute-battle.use-case.ts | Cria item com acquiredFrom "Batalha" |
src/application/use-cases/raffle/draw-raffle.use-case.ts | Cria item com acquiredFrom "Sorteio" |
src/application/use-cases/event/claim-event-reward.use-case.ts | Cria item com acquiredFrom "Evento" |
src/application/use-cases/event-shoot/perform-event-shoot.use-case.ts | Cria item com acquiredFrom "Evento - Atire na Caixa" |
