Skip to content

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):

prisma
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):

prisma
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:

CampoTipoDescricao
idBigIntSnowflake ID gerado via SnowflakeService.generate()
userIdBigIntFK para User.id
itemIdBigIntFK para Item.id
statusInventoryItemStatusStatus atual do item. Default: AVAILABLE
acquiredAtDateTimeTimestamp de quando o item entrou no inventario. Default: now()
acquiredFromString?Origem do item (texto livre). Nullable
lockedUntilDateTime?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:

prisma
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)

prisma
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")
}
prisma
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:

typescript
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):

typescript
await this.userInventoryRepository.updateStatus(inventoryItem.id, 'LOCKED');
inventoryItem.status = 'LOCKED';

Codigo real (upgrade):

typescript
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):

typescript
await this.userInventoryRepository.updateStatus(inventoryItem.id, 'AVAILABLE');
inventoryItem.status = 'AVAILABLE';

Codigo real (tracker - cancelado/falhado):

typescript
if (newStatus === 'CANCELLED' || newStatus === 'FAILED') {
  await this.userInventoryRepository.updateStatus(withdrawal.userInventoryId, 'AVAILABLE');
}

Codigo real (upgrade - validacao falhou):

typescript
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):

  1. Saldo insuficiente (Insufficient balance)
  2. Valor total zero (Total value must be greater than zero)
  3. Valor total >= item alvo (Total value must be less than target item value)
  4. Erro no calculo de chance (excecao do UpgradeProbabilityService)
  5. 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:

typescript
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:

typescript
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):

typescript
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):

typescript
for (const item of items) {
  await this.inventoryRepository.updateStatus(item.id, InventoryItemStatus.IN_SWAP);
}

Codigo real (site-swap via Prisma direto):

typescript
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.ts
  • src/application/use-cases/swap/decline-swap.use-case.ts

Codigo real (cancel):

typescript
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):

typescript
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:

typescript
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

DeParaGatilhoReversivel?
AVAILABLESOLDVenda (individual/multipla/total)Nao
AVAILABLELOCKEDInicio de withdrawal ou upgradeSim
AVAILABLEUSED_IN_UPGRADEUpgrade executado (apos LOCKED)Nao
AVAILABLEIN_SWAPCriacao ou aceitacao de swapSim
LOCKEDAVAILABLEFalha no withdrawal ou erro no upgrade-
LOCKEDWITHDRAWNWithdrawal completadoNao
IN_SWAPAVAILABLECancel 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:

ValorUse CaseArquivo
"Caixa - {nome_da_caixa}"Abertura de caixaopen-case.use-case.ts (linha 143)
"Upgrade"Upgrade bem-sucedidoperform-upgrade.use-case.ts (linha 197)
"Batalha"Vitoria em batalhaexecute-battle.use-case.ts (linha 616)
"Swap"Troca via swapaccept-swap.use-case.ts, perform-site-swap.use-case.ts (linha 178)
"Sorteio"Vitoria em sorteio/raffledraw-raffle.use-case.ts (linha 113)
"Evento"Recompensa de eventoclaim-event-reward.use-case.ts (linha 50)
"Evento - Atire na Caixa"Evento de tiro na caixaperform-event-shoot.use-case.ts (linha 111)

Exemplo de criacao com acquiredFrom:

typescript
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:

ParametroTipoObrigatorioDefaultDescricao
statusInventoryItemStatusNao-Filtrar por status
limitnumberNao15Itens por pagina
offsetnumberNao0Pular N itens
sortBy'value' | 'acquiredAt' | 'name'Nao'acquiredAt'Campo de ordenacao
sortOrder'asc' | 'desc'Nao'desc'Direcao da ordenacao
searchstringNao-Busca textual no nome do item (case insensitive)

Response 200 (InventoryListResponseDto):

typescript
{
  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:

ParametroTipoDescricao
idstringSnowflake ID do item de inventario

Response 200 (SellItemResponseDto):

typescript
{
  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):

typescript
{
  inventoryItemIds: string[]   // Array de Snowflake IDs. Min: 1, Max: 100
}

Response 200 (SellItemResponseDto):

typescript
{
  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):

typescript
{
  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:

ParametroTipoDescricao
idstringSnowflake ID do item de inventario

Response 200:

typescript
{
  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:

  1. Item encontrado e pertence ao usuario
  2. Item nao e restrictedToGames (itens da Loja nao podem ser sacados)
  3. Status do item e AVAILABLE
  4. Nao existe withdrawal ativo para este item
  5. Usuario tem tradeUrl cadastrado
  6. Trade URL segue o regex: ^https://steamcommunity\.com/tradeoffer/new/\?partner=(\d+)&token=([a-zA-Z0-9_-]+)$
  7. Item encontrado no Waxpeer (isExactMatch === true)
  8. Waxpeer tem estoque (count >= 1)
  9. Preco Waxpeer <= maxPayCents (isPriceValid === true), onde maxPayCents = ceil(valueCents / priceMargin)

4.6 GET /inventory/withdrawals/active

Descricao: Retorna withdrawals ativos do usuario (status != COMPLETED/CANCELLED/FAILED).

Response 200:

typescript
{
  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:

ParametroTipoDescricao
idstringID do WaxpeerWithdrawal

Response 200:

typescript
{
  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:

ParametroTipoObrigatorioDefault
limitnumberNao50
offsetnumberNao0

Response 200:

typescript
{
  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 atomica
  • LockService - Distributed lock via Redlock
  • IUserInventoryRepository - Acesso ao inventario
  • TransactionService - Double-entry bookkeeping
  • WebSocketGatewayService - Notificacao em tempo real

Fluxo completo:

  1. Busca item por ID (findById) com include do Item
  2. Valida existencia do item de inventario
  3. Valida existencia do item base (item relation)
  4. Valida ownership (inventoryItem.userId !== userId)
  5. Valida status (!== 'AVAILABLE')
  6. Adquire distributed lock: user:balance:{userId} (TTL: 10s)
  7. Dentro de $transaction:
    • Atualiza status para SOLD
    • Credita item.valueCentsBrl integral via TransactionService.credit()
    • Obtem novo saldo via TransactionService.getUserBalance()
    • Emite balance:updated via WebSocket
  8. Retorna { inventoryItem, valueCents: item.valueCentsBrl }

Codigo completo:

typescript
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:

  1. Valida que array nao esta vazio
  2. Valida que array tem no maximo 100 itens
  3. Busca todos os itens em paralelo (Promise.all)
  4. Valida cada item: existencia, ownership, status AVAILABLE
  5. Adquire distributed lock: user:balance:{userId} (TTL: 10s)
  6. Dentro de $transaction:
    • Calcula totalValueCents somando valueCentsBrl de cada item
    • Atualiza cada item para SOLD
    • Credita totalValueCents como transacao unica
  7. Retorna { itemsSold, totalValueCents, items }

Codigo completo:

typescript
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:

  1. Busca todos os itens do usuario com status AVAILABLE
  2. Valida que existe pelo menos 1 item
  3. Adquire distributed lock: user:balance:{userId} (TTL: 10s)
  4. Dentro de $transaction:
    • Calcula totalValueCents somando valueCentsBrl
    • Atualiza CADA item para SOLD
    • Credita totalValueCents como transacao unica
    • Emite balance:updated via WebSocket
  5. Retorna { itemsSold, totalValueCents }

Codigo completo:

typescript
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:

typescript
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:

  1. Adquire distributed lock: withdrawal:item:{inventoryItemId} (TTL: 30s)
  2. Busca item de inventario
  3. Valida ownership, restrictedToGames (itens da Loja nao podem ser sacados) e status AVAILABLE
  4. Verifica se ja existe withdrawal ativo para este item
  5. Busca dados do usuario (tradeUrl)
  6. Valida formato da Trade URL via regex
  7. Busca item base para obter valueCents (USD) e marketHashName
  8. Calcula maxPayCents = ceil(valueCents / priceMargin) (PriceMarginService.getCurrent())
  9. Consulta preco no Waxpeer (waxpeerService.getItemPrice)
  10. Compara precos e monta PriceComparison
  11. Se canWithdraw === true, dentro de withAutoApprovalLock(userId):
    • Avalia elegibilidade (autoApprovalEvaluator.evaluate)
    • Gera projectId: wd_{inventoryItemId}_{timestamp} e atualiza item para LOCKED
    • Elegivel → cria WaxpeerWithdrawal status: PENDING, autoApproved: true; depois runAutoPurchase (via WaxpeerPurchaseExecutorService, fora do lock): compra OK → WAITING_SELLER + withdrawal:waiting_seller; falha → FAILED, item AVAILABLE, withdrawal:failed
    • Nao elegivel → cria WaxpeerWithdrawal status: PENDING_APPROVAL, autoApproved: false; emite withdrawal:pending_approval e audita SKIN_WITHDRAWAL_REQUESTED (aguarda aprovacao do admin)

Regex de Trade URL:

typescript
const TRADE_URL_REGEX =
  /^https:\/\/steamcommunity\.com\/tradeoffer\/new\/\?partner=(\d+)&token=([a-zA-Z0-9_-]+)$/;

Condicoes do PriceComparison:

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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

typescript
async countByUserId(userId: bigint, status?: InventoryItemStatus) {
  return this.prisma.userInventory.count({
    where: {
      userId,
      ...(status && { status }),
    },
  });
}

7.9 countByUserIdWithFilters

typescript
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

typescript
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

typescript
export class SellMultipleItemsDto {
  @IsArray()
  @ArrayMinSize(1)
  @IsString({ each: true })
  inventoryItemIds: string[];
}

8.3 InventoryItemResponseDto

typescript
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

typescript
export class SellItemResponseDto {
  success: boolean;
  itemsSold: number;
  totalValueCents: string;
  totalValueFormatted: string;
}

8.5 InventoryListResponseDto

typescript
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:

typescript
{
  balanceCents: string  // Novo saldo em centavos BRL. Ex: "186321"
}

Codigo de emissao:

typescript
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:

typescript
{
  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:

typescript
{
  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:

typescript
{
  inventoryItemId: string,
  waxpeerId?: string,
  sendUntil?: number,
}

9.5 withdrawal:trade_sent

Trigger: Vendedor enviou a trade offer no Steam.

Payload:

typescript
{
  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:

typescript
{
  inventoryItemId: string,
  itemName: string,
}

Efeito colateral: Status do UserInventory muda para WITHDRAWN.

9.7 withdrawal:cancelled

Trigger: Withdrawal cancelado (pelo vendedor, timeout, etc).

Payload:

typescript
{
  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:

typescript
{
  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

typescript
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

typescript
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:

typescript
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: valueCentsBrl integral (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, lanca NotFoundException('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, lanca NotFoundException dentro da transacao.
  • Item com status diferente de AVAILABLE: Lanca BadRequestException com 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, lanca BadRequestException('This item already has a pending withdrawal. Status: ...').
  • Item da Loja (restrictedToGames): Lanca BadRequestException — itens restritos a jogos nao podem ser sacados para a Steam.
  • Item sem estoque no Waxpeer: canWithdraw sera false. Nao tenta comprar.
  • Preco Waxpeer acima do teto (maxPayCents): isPriceValid sera false. Nao tenta comprar.
  • Item nao encontrado no Waxpeer (match inexato): isExactMatch sera false. Nao tenta comprar.
  • Erro na API Waxpeer: priceComparison e preenchido com valores zerados e canWithdraw: 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 WaxpeerWithdrawalTrackerService ou WaxpeerWebSocketService detecta, 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)CondicaoStatus Interno
0, 1-WAITING_SELLER
2, 3, 4-TRADE_SENT
5-COMPLETED
6reason contem "cancel"/"failed"/"declined"CANCELLED
6outros reasonsFAILED
qualqueris_released === trueCOMPLETED
qualquerstage === 9COMPLETED
qualquerdone === true && status !== 6COMPLETED

13.2 Efeito no UserInventory por Status do Withdrawal

Status WithdrawalEfeito no UserInventory
PENDING_APPROVALLOCKED (aguardando aprovacao do admin)
PENDINGLOCKED (ja feito no request)
WAITING_SELLERPermanece LOCKED
TRADE_SENTPermanece LOCKED
COMPLETED-> WITHDRAWN
CANCELLED-> AVAILABLE (rollback)
FAILED-> AVAILABLE (rollback)

13.3 Dois Canais de Atualizacao

O withdrawal pode ser atualizado por dois mecanismos:

  1. Polling (WaxpeerWithdrawalTrackerService): Consulta periodica via API REST do Waxpeer. Usa forceCheckWithdrawal() para refresh manual.

  2. 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:

typescript
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

ArquivoResponsabilidade
prisma/schema.prismaDefinicao do modelo UserInventory, enum InventoryItemStatus, WaxpeerWithdrawal
src/domain/repositories/user-inventory.repository.interface.tsInterface do repositorio
src/infrastructure/database/repositories/user-inventory.repository.tsImplementacao Prisma do repositorio
src/application/dto/inventory.dto.tsDTOs de request e response
src/application/use-cases/inventory/get-user-inventory.use-case.tsUse case de listagem
src/application/use-cases/inventory/sell-inventory-item.use-case.tsUse case de venda individual
src/application/use-cases/inventory/sell-multiple-items.use-case.tsUse case de venda multipla
src/application/use-cases/inventory/sell-all-items.use-case.tsUse case de venda total
src/application/use-cases/inventory/request-withdraw-item.use-case.tsUse case de withdrawal
src/presentation/controllers/inventory.controller.tsController HTTP
src/infrastructure/websocket/websocket.gateway.tsEmissao de eventos WebSocket
src/application/services/waxpeer-withdrawal-tracker.service.tsTracking de withdrawal via polling
src/infrastructure/external-apis/waxpeer-websocket.service.tsTracking de withdrawal via WebSocket
src/infrastructure/locks/lock.service.tsDistributed locks (Redlock)
src/application/services/transaction.service.tsDouble-entry bookkeeping
src/application/services/money.service.tsFormatacao de valores monetarios
src/application/use-cases/swap/create-swap.use-case.tsSwap: marca itens como IN_SWAP
src/application/use-cases/swap/accept-swap.use-case.tsSwap: troca ownership, marca IN_SWAP e AVAILABLE
src/application/use-cases/swap/cancel-swap.use-case.tsSwap: reverte itens para AVAILABLE
src/application/use-cases/swap/decline-swap.use-case.tsSwap: reverte itens do criador para AVAILABLE
src/application/use-cases/swap/perform-site-swap.use-case.tsSite swap: marca IN_SWAP, cria novo item AVAILABLE
src/application/use-cases/upgrade/perform-upgrade.use-case.tsUpgrade: LOCKED -> USED_IN_UPGRADE ou AVAILABLE
src/application/use-cases/case-opening/open-case.use-case.tsCria item com acquiredFrom "Caixa - {nome}"
src/application/use-cases/battle/execute-battle.use-case.tsCria item com acquiredFrom "Batalha"
src/application/use-cases/raffle/draw-raffle.use-case.tsCria item com acquiredFrom "Sorteio"
src/application/use-cases/event/claim-event-reward.use-case.tsCria item com acquiredFrom "Evento"
src/application/use-cases/event-shoot/perform-event-shoot.use-case.tsCria item com acquiredFrom "Evento - Atire na Caixa"

Documentação Técnica - ProCases