Skip to content

Sistema Admin / Backoffice - Documentacao Completa

Indice

  1. Visao Geral
  2. Autenticacao e Autorizacao
  3. Dashboard
  4. Gerenciamento de Usuarios
  5. Gerenciamento de Caixas
  6. Gerenciamento de Itens
  7. Gerenciamento de Batalhas
  8. Gerenciamento Financeiro
  9. Analytics
  10. DTOs e Interfaces de Resposta

Visao Geral

O sistema Admin/Backoffice expoe 44 endpoints sob o prefixo /admin, organizados em 8 grupos funcionais. Todos os endpoints passam por dois guards obrigatorios a nivel de classe: AuthGuard (verifica sessao ativa) e AdminGuard (verifica role do usuario).

Arquivo do Controller: src/presentation/controllers/admin.controller.tsArquivo de DTOs: src/application/dto/admin.dto.tsUse Cases: src/application/use-cases/admin/

Arquitetura

AdminController (Presentation)
  -> Use Case (Application)
    -> PrismaClient / Repository (Infrastructure)
    -> TransactionService, MoneyService, LockService (Application Services)

Toda formatacao monetaria e feita pelo MoneyService.format(), que converte bigint em centavos para formato brasileiro R$ X.XXX,XX. O controller NUNCA contem logica de negocio; apenas delega para use cases e formata a resposta HTTP.


Autenticacao e Autorizacao

Guards Aplicados

Os dois guards sao aplicados a nivel de classe, portanto todos os 44 endpoints os herdam automaticamente.

typescript
@Controller('admin')
@UseGuards(AuthGuard, AdminGuard)
export class AdminController { ... }

AuthGuard

Verifica se a requisicao possui um sessionId valido. A sessao e armazenada em Redis e deve estar ativa. Se a sessao nao existir ou estiver expirada, retorna 401 Unauthorized.

AdminGuard

Verifica se o usuario autenticado possui role ADMIN ou SUPER_ADMIN. Caso contrario, retorna 403 Forbidden.

typescript
@Injectable()
export class AdminGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const user = request.user;

    if (!user) {
      throw new ForbiddenException('User not authenticated');
    }

    if (user.role !== 'ADMIN' && user.role !== 'SUPER_ADMIN') {
      throw new ForbiddenException('Admin access required');
    }

    return true;
  }
}

Roles Aceitas

RoleAcesso
USERNenhum endpoint admin
ADMINTodos os endpoints admin
SUPER_ADMINTodos os endpoints admin + protecao contra ban

Respostas de Erro Comuns

json
// 401 - Sessao invalida ou expirada
{
  "statusCode": 401,
  "message": "Unauthorized"
}

// 403 - Usuario sem permissao admin
{
  "statusCode": 403,
  "message": "Admin access required"
}

Dashboard

O grupo de Dashboard fornece 4 endpoints para visualizacao geral da plataforma em tempo real.

GET /admin/dashboard/stats

Retorna estatisticas gerais do dashboard incluindo totais de usuarios, receita, depositos, saques e lucro.

Query Parameters: Nenhum

Response (200):

typescript
interface DashboardStatsResponseDto {
  totalUsers: number;
  activeUsersToday: number;
  totalRevenueCents: string;
  revenueTodayCents: string;
  totalCaseOpenings: number;
  caseOpeningsToday: number;
  totalBattles: number;
  battlesToday: number;
  pendingDeposits: number;
  pendingWithdrawals: number;
  houseEdgeCents: string;
  totalDepositedCents: string;
  totalDepositedFormatted: string;
  totalWithdrawnCents: string;
  totalWithdrawnFormatted: string;
  totalPlatformBalanceCents: string;
  totalPlatformBalanceFormatted: string;
  profitCents: string;
  profitFormatted: string;
}

Exemplo de Response:

json
{
  "totalUsers": 1523,
  "activeUsersToday": 87,
  "totalRevenueCents": "4523100",
  "revenueTodayCents": "156000",
  "totalCaseOpenings": 9812,
  "caseOpeningsToday": 342,
  "totalBattles": 1203,
  "battlesToday": 45,
  "pendingDeposits": 3,
  "pendingWithdrawals": 7,
  "houseEdgeCents": "4523100",
  "totalDepositedCents": "12500000",
  "totalDepositedFormatted": "R$ 125.000,00",
  "totalWithdrawnCents": "7976900",
  "totalWithdrawnFormatted": "R$ 79.769,00",
  "totalPlatformBalanceCents": "3200000",
  "totalPlatformBalanceFormatted": "R$ 32.000,00",
  "profitCents": "4523100",
  "profitFormatted": "R$ 45.231,00"
}

Calculo de Metricas:

  • totalRevenueCents = totalDepositedCents - totalWithdrawnCents
  • revenueTodayCents = depositosHoje - saquesHoje
  • houseEdgeCents = mesmo que profitCents (depositos confirmados - saques completados)
  • totalPlatformBalanceCents = soma de balanceCents de todos os usuarios

GET /admin/dashboard/revenue-chart

Retorna dados para o grafico de receita segmentados por periodo.

Query Parameters:

ParametroTipoObrigatorioDefaultValores
periodstringNaoweekday, week, month, year

Intervalos por Periodo:

  • day: 24 intervalos de 1 hora (ultimas 24h)
  • week: 7 intervalos de 1 dia (ultimos 7 dias)
  • month: 30 intervalos de 1 dia (ultimos 30 dias)
  • year: 12 intervalos de 1 mes (ultimos 12 meses)

Response (200):

typescript
interface RevenueChartDataPoint {
  date: string;       // ISO 8601
  revenue: number;    // centavos (depositos - saques)
  deposits: number;   // centavos total depositado
  withdrawals: number; // centavos total sacado
  caseOpenings: number; // quantidade de aberturas
  battles: number;     // quantidade de batalhas
}

Exemplo de Response:

json
[
  {
    "date": "2026-02-04T00:00:00.000Z",
    "revenue": 230000,
    "deposits": 450000,
    "withdrawals": 220000,
    "caseOpenings": 156,
    "battles": 12
  },
  {
    "date": "2026-02-05T00:00:00.000Z",
    "revenue": 180000,
    "deposits": 380000,
    "withdrawals": 200000,
    "caseOpenings": 134,
    "battles": 9
  }
]

GET /admin/dashboard/overview

Retorna estatisticas completas de overview com GGR (Gross Gaming Revenue), ativos dos jogadores e breakdown por tipo de jogo (caixas, upgrades, batalhas, swaps).

Query Parameters:

ParametroTipoObrigatorioDefaultValores
periodstringNaomonthday, week, month, year

Response (200):

typescript
interface OverviewStatsResponse {
  totalGgrCents: string;
  totalGgrFormatted: string;
  totalWageredCents: string;
  totalWageredFormatted: string;
  totalPaidCents: string;
  totalPaidFormatted: string;
  totalPlayerBalanceCents: string;
  totalPlayerBalanceFormatted: string;
  totalPlayerInventoryCents: string;
  totalPlayerInventoryFormatted: string;
  totalPlayerAssetsCents: string;
  totalPlayerAssetsFormatted: string;
  uniqueActivePlayers: number;
  totalUsers: number;
  totalDepositedCents: string;
  totalDepositedFormatted: string;

  caseOpenings: GameTypeBreakdown;
  upgrades: GameTypeBreakdown;
  battles: GameTypeBreakdown;
  swaps: GameTypeBreakdown;

  period: string;
}

interface GameTypeBreakdown {
  count: number;
  wageredCents: string;
  wageredFormatted: string;
  paidCents: string;
  paidFormatted: string;
  ggrCents: string;
  ggrFormatted: string;
  rtpPercent: number;
}

Exemplo de Response:

json
{
  "totalGgrCents": "1250000",
  "totalGgrFormatted": "R$ 12.500,00",
  "totalWageredCents": "5000000",
  "totalWageredFormatted": "R$ 50.000,00",
  "totalPaidCents": "3750000",
  "totalPaidFormatted": "R$ 37.500,00",
  "totalPlayerBalanceCents": "890000",
  "totalPlayerBalanceFormatted": "R$ 8.900,00",
  "totalPlayerInventoryCents": "2300000",
  "totalPlayerInventoryFormatted": "R$ 23.000,00",
  "totalPlayerAssetsCents": "3190000",
  "totalPlayerAssetsFormatted": "R$ 31.900,00",
  "uniqueActivePlayers": 245,
  "totalUsers": 1523,
  "totalDepositedCents": "15000000",
  "totalDepositedFormatted": "R$ 150.000,00",
  "caseOpenings": {
    "count": 3421,
    "wageredCents": "3000000",
    "wageredFormatted": "R$ 30.000,00",
    "paidCents": "2100000",
    "paidFormatted": "R$ 21.000,00",
    "ggrCents": "900000",
    "ggrFormatted": "R$ 9.000,00",
    "rtpPercent": 70.0
  },
  "upgrades": {
    "count": 890,
    "wageredCents": "1200000",
    "wageredFormatted": "R$ 12.000,00",
    "paidCents": "960000",
    "paidFormatted": "R$ 9.600,00",
    "ggrCents": "240000",
    "ggrFormatted": "R$ 2.400,00",
    "rtpPercent": 80.0
  },
  "battles": {
    "count": 156,
    "wageredCents": "500000",
    "wageredFormatted": "R$ 5.000,00",
    "paidCents": "420000",
    "paidFormatted": "R$ 4.200,00",
    "ggrCents": "80000",
    "ggrFormatted": "R$ 800,00",
    "rtpPercent": 84.0
  },
  "swaps": {
    "count": 312,
    "wageredCents": "300000",
    "wageredFormatted": "R$ 3.000,00",
    "paidCents": "270000",
    "paidFormatted": "R$ 2.700,00",
    "ggrCents": "30000",
    "ggrFormatted": "R$ 300,00",
    "rtpPercent": 90.0
  },
  "period": "month"
}

Detalhes de Calculo:

  • totalPlayerAssets = totalPlayerBalance + totalPlayerInventory
  • totalDeposited = depositos confirmados + creditos administrativos
  • GGR de batalhas usa BattleSettlement (REMOVED_TO_POOL - ADDED_FROM_POOL)
  • Jogadores unicos sao contados via UNION de case_openings, upgrades, battles, battle_players e site_swaps
  • Bots (steamId com prefixo BOT_) sao excluidos da contagem de jogadores unicos

GET /admin/dashboard/recent-activity

Retorna as atividades mais recentes da plataforma, mesclando aberturas de caixas, batalhas, depositos, saques e upgrades em uma timeline unificada.

Query Parameters:

ParametroTipoObrigatorioDefaultMinMax
limitnumberNao10150

Response (200):

typescript
interface RecentActivityItem {
  id: string;
  type: 'CASE_OPENING' | 'BATTLE' | 'DEPOSIT' | 'WITHDRAWAL' | 'UPGRADE';
  userId: string;
  username: string;
  avatarUrl?: string;
  description: string;
  valueCents: string;
  valueFormatted: string;
  createdAt: string;
  metadata?: Record<string, unknown>;
}

Exemplo de Response:

json
[
  {
    "id": "98712345678",
    "type": "CASE_OPENING",
    "userId": "1",
    "username": "PlayerOne",
    "avatarUrl": "https://avatars.steamstatic.com/abc123.jpg",
    "description": "Abriu Star Case e ganhou AK-47 | Redline",
    "valueCents": "15900",
    "valueFormatted": "R$ 159,00",
    "createdAt": "2026-02-11T14:30:00.000Z",
    "metadata": {
      "caseId": "123",
      "itemId": "456"
    }
  },
  {
    "id": "98712345679",
    "type": "DEPOSIT",
    "userId": "2",
    "username": "PlayerTwo",
    "description": "Depositou via PIX",
    "valueCents": "50000",
    "valueFormatted": "R$ 500,00",
    "createdAt": "2026-02-11T14:28:00.000Z",
    "metadata": {
      "method": "PIX"
    }
  },
  {
    "id": "98712345680",
    "type": "UPGRADE",
    "userId": "3",
    "username": "PlayerThree",
    "description": "Ganhou upgrade para AWP | Dragon Lore",
    "valueCents": "125000",
    "valueFormatted": "R$ 1.250,00",
    "createdAt": "2026-02-11T14:25:00.000Z",
    "metadata": {
      "success": true,
      "chancePercent": 15.5
    }
  }
]

Descricoes por Tipo:

  • CASE_OPENING: "Abriu {caseName} e ganhou {itemName}"
  • BATTLE: "Criou uma batalha {type}" (ex: "ONE VS ONE")
  • DEPOSIT: "Depositou via {method}"
  • WITHDRAWAL: "Sacou via {method}"
  • UPGRADE: "Ganhou upgrade para {itemName}" ou "Perdeu upgrade para {itemName}"

Gerenciamento de Usuarios

O grupo de gerenciamento de usuarios expoe 9 endpoints para listagem, detalhes, moderacao e historico de atividades dos usuarios.

GET /admin/users

Lista todos os usuarios com filtros, paginacao e ordenacao.

Query Parameters:

ParametroTipoObrigatorioDefaultValores
pagenumberNao1Min: 1
limitnumberNao20Min: 1, Max: 100
statusstringNao-ACTIVE, SUSPENDED, BANNED
rolestringNao-USER, ADMIN, SUPER_ADMIN
searchstringNao-Busca por username/steamId
sortBystringNao-Campo de ordenacao
sortOrderstringNao-asc, desc

Response (200):

typescript
interface PaginatedResponse<UserAdminResponseDto> {
  data: UserAdminResponseDto[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}

interface UserAdminResponseDto {
  id: string;
  steamId: string;
  username: string;
  avatarUrl?: string;
  balanceCents: string;
  balanceFormatted: string;
  tickets: number;
  role: string;
  status: string;
  totalDepositedCents: string;
  totalDepositedFormatted: string;
  totalWithdrawnCents: string;
  totalWithdrawnFormatted: string;
  totalWageredCents: string;
  totalWageredFormatted: string;
  lastLoginAt?: string;
  lastLoginIp?: string;
  createdAt: string;
}

Exemplo de Response:

json
{
  "data": [
    {
      "id": "1",
      "steamId": "76561198012345678",
      "username": "PlayerOne",
      "avatarUrl": "https://avatars.steamstatic.com/abc.jpg",
      "balanceCents": "186321",
      "balanceFormatted": "R$ 1.863,21",
      "tickets": 5,
      "role": "USER",
      "status": "ACTIVE",
      "totalDepositedCents": "500000",
      "totalDepositedFormatted": "R$ 5.000,00",
      "totalWithdrawnCents": "250000",
      "totalWithdrawnFormatted": "R$ 2.500,00",
      "totalWageredCents": "800000",
      "totalWageredFormatted": "R$ 8.000,00",
      "lastLoginAt": "2026-02-11T10:00:00.000Z",
      "lastLoginIp": "203.0.113.10",
      "createdAt": "2025-11-01T08:00:00.000Z"
    }
  ],
  "total": 1523,
  "page": 1,
  "limit": 20,
  "totalPages": 77
}

GET /admin/users/:id

Retorna detalhes completos de um usuario especifico, incluindo informacoes de inventario.

Path Parameters:

ParametroTipoDescricao
idstringID do usuario (bigint)

Response (200):

typescript
interface UserDetailResponseDto {
  id: string;
  steamId: string;
  username: string;
  avatarUrl?: string;
  profileUrl?: string;
  balanceCents: string;
  balanceFormatted: string;
  tickets: number;
  role: string;
  status: string;
  twoFactorEnabled: boolean;
  referralCode?: string;
  referredById?: string;
  totalDepositedCents: string;
  totalDepositedFormatted: string;
  totalWithdrawnCents: string;
  totalWithdrawnFormatted: string;
  totalWageredCents: string;
  totalWageredFormatted: string;
  inventoryItemCount: number;
  inventoryValueCents: string;
  inventoryValueFormatted: string;
  lastLoginAt?: string;
  lastLoginIp?: string;
  createdAt: string;
  updatedAt: string;
}

Exemplo de Response:

json
{
  "id": "1",
  "steamId": "76561198012345678",
  "username": "PlayerOne",
  "avatarUrl": "https://avatars.steamstatic.com/abc.jpg",
  "profileUrl": "https://steamcommunity.com/profiles/76561198012345678",
  "balanceCents": "186321",
  "balanceFormatted": "R$ 1.863,21",
  "tickets": 5,
  "role": "USER",
  "status": "ACTIVE",
  "twoFactorEnabled": false,
  "referralCode": "PLAYER1ABC",
  "totalDepositedCents": "500000",
  "totalDepositedFormatted": "R$ 5.000,00",
  "totalWithdrawnCents": "250000",
  "totalWithdrawnFormatted": "R$ 2.500,00",
  "totalWageredCents": "800000",
  "totalWageredFormatted": "R$ 8.000,00",
  "inventoryItemCount": 12,
  "inventoryValueCents": "345000",
  "inventoryValueFormatted": "R$ 3.450,00",
  "lastLoginAt": "2026-02-11T10:00:00.000Z",
  "lastLoginIp": "203.0.113.10",
  "createdAt": "2025-11-01T08:00:00.000Z",
  "updatedAt": "2026-02-11T10:00:00.000Z"
}

PUT /admin/users/:id/status

Atualiza o status de um usuario (ativar, suspender ou banir).

Path Parameters:

ParametroTipoDescricao
idstringID do usuario (bigint)

Request Body:

typescript
class UpdateUserStatusDto {
  status: 'ACTIVE' | 'SUSPENDED' | 'BANNED';
}

Exemplo de Request:

json
{
  "status": "SUSPENDED"
}

Response (200):

json
{
  "success": true
}

POST /admin/users/:id/balance

Ajusta o saldo de um usuario (credito ou debito administrativo). Utiliza LockService.withUserBalanceLock() para prevenir race conditions. Cria transacao auditavel via TransactionService.

Path Parameters:

ParametroTipoDescricao
idstringID do usuario (bigint)

Request Body:

typescript
class UpdateUserBalanceDto {
  amountCents: number;  // positivo = credito, negativo = debito
  reason: string;
}

Exemplo de Request (Credito):

json
{
  "amountCents": 50000,
  "reason": "Compensacao por erro no sistema"
}

Exemplo de Request (Debito):

json
{
  "amountCents": -20000,
  "reason": "Correcao de saldo indevido"
}

Response (200):

json
{
  "success": true,
  "newBalanceCents": "236321",
  "newBalanceFormatted": "R$ 2.363,21"
}

Erros Possiveis:

json
// 404 - Usuario nao encontrado
{ "statusCode": 404, "message": "User not found" }

// 400 - Saldo insuficiente para debito
{ "statusCode": 400, "message": "Insufficient balance" }

Comportamento Interno:

  • Valores positivos: TransactionService.credit() com reason "Admin adjustment: {reason} (by admin {adminId})"
  • Valores negativos: TransactionService.debit() com mesma formatacao de reason
  • A operacao e executada dentro de LockService.withUserBalanceLock() (distributed lock via Redlock)
  • Toda a operacao roda em prisma.$transaction()

POST /admin/users/:id/ban

Bane um usuario, invalidando todas as suas sessoes ativas.

Path Parameters:

ParametroTipoDescricao
idstringID do usuario (bigint)

Request Body:

typescript
class BanUserDto {
  reason: string;
}

Exemplo de Request:

json
{
  "reason": "Uso de software nao autorizado"
}

Response (200):

json
{
  "success": true,
  "reason": "Uso de software nao autorizado"
}

Erros Possiveis:

json
// 404 - Usuario nao encontrado
{ "statusCode": 404, "message": "Usuario nao encontrado" }

// 400 - Ja banido
{ "statusCode": 400, "message": "Usuario ja esta banido" }

// 400 - Tentativa de banir SUPER_ADMIN
{ "statusCode": 400, "message": "Nao e possivel banir um SUPER_ADMIN" }

Comportamento Interno:

  • Atualiza user.status para BANNED
  • Deleta todas as sessoes do usuario (session.deleteMany)
  • Executa em prisma.$transaction() para atomicidade
  • SUPER_ADMIN nao pode ser banido (protecao)

POST /admin/users/:id/unban

Remove o ban de um usuario, reativando sua conta.

Path Parameters:

ParametroTipoDescricao
idstringID do usuario (bigint)

Request Body: Nenhum

Response (200):

json
{
  "success": true
}

GET /admin/users/:userId/case-openings

Retorna o historico de aberturas de caixas de um usuario especifico.

Path Parameters:

ParametroTipoDescricao
userIdstringID do usuario (bigint)

Query Parameters:

ParametroTipoObrigatorioDefaultValores
pagenumberNao1Min: 1
limitnumberNao20Min: 1, Max: 100
caseIdstringNao-Filtrar por caixa especifica

Response (200):

typescript
interface PaginatedResponse<CaseOpeningAdminResponseDto> {
  data: CaseOpeningAdminResponseDto[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}

Exemplo de Response:

json
{
  "data": [
    {
      "id": "98712345678",
      "caseId": "100",
      "caseName": "Star Case",
      "caseImageUrl": "https://cdn.example.com/cases/star.png",
      "itemId": "500",
      "itemName": "AK-47 | Redline",
      "itemImageUrl": "https://cdn.example.com/items/ak47-redline.png",
      "itemRarity": "CLASSIFIED",
      "pricePaidCents": "1990",
      "pricePaidFormatted": "R$ 19,90",
      "itemValueCents": "15900",
      "itemValueFormatted": "R$ 159,00",
      "profitCents": "13910",
      "profitFormatted": "R$ 139,10",
      "isFlip": false,
      "roll": 8734521,
      "createdAt": "2026-02-10T15:30:00.000Z"
    }
  ],
  "total": 45,
  "page": 1,
  "limit": 20,
  "totalPages": 3
}

GET /admin/users/:userId/battles

Retorna o historico de batalhas de um usuario especifico.

Path Parameters:

ParametroTipoDescricao
userIdstringID do usuario (bigint)

Query Parameters:

ParametroTipoObrigatorioDefaultValores
pagenumberNao1Min: 1
limitnumberNao20Min: 1, Max: 100
statusstringNao-WAITING, IN_PROGRESS, FINISHED, CANCELLED

Response (200):

json
{
  "data": [
    {
      "id": "200",
      "mode": "NORMAL",
      "type": "ONE_VS_ONE",
      "status": "FINISHED",
      "creatorId": "1",
      "creatorUsername": "PlayerOne",
      "creatorAvatarUrl": "https://avatars.steamstatic.com/abc.jpg",
      "winnerId": "1",
      "winnerUsername": "PlayerOne",
      "winnerTeam": 1,
      "totalValueCents": "5970",
      "totalValueFormatted": "R$ 59,70",
      "isPrivate": false,
      "playerCount": 2,
      "caseCount": 3,
      "createdAt": "2026-02-09T12:00:00.000Z",
      "startedAt": "2026-02-09T12:01:00.000Z",
      "finishedAt": "2026-02-09T12:05:00.000Z"
    }
  ],
  "total": 12,
  "page": 1,
  "limit": 20,
  "totalPages": 1
}

GET /admin/users/:userId/transactions

Retorna o historico de transacoes financeiras de um usuario.

Path Parameters:

ParametroTipoDescricao
userIdstringID do usuario (bigint)

Query Parameters:

ParametroTipoObrigatorioDefaultValores
pagenumberNao1Min: 1
limitnumberNao20Min: 1, Max: 100
typestringNao-CREDIT, DEBIT

Response (200):

json
{
  "data": [
    {
      "id": "300",
      "type": "CREDIT",
      "amountCents": "50000",
      "amountFormatted": "R$ 500,00",
      "reason": "DEPOSIT",
      "status": "COMPLETED",
      "metadata": { "depositId": "123", "method": "PIX" },
      "createdAt": "2026-02-10T14:00:00.000Z"
    },
    {
      "id": "301",
      "type": "DEBIT",
      "amountCents": "1990",
      "amountFormatted": "R$ 19,90",
      "reason": "CASE_OPENING",
      "status": "COMPLETED",
      "metadata": { "caseId": "100", "caseName": "Star Case" },
      "createdAt": "2026-02-10T14:05:00.000Z"
    }
  ],
  "total": 89,
  "page": 1,
  "limit": 20,
  "totalPages": 5
}

Gerenciamento de Caixas

O grupo de caixas expoe 7 endpoints para CRUD completo de caixas e gerenciamento de itens dentro delas, incluindo recalculo automatico de hash ranges.

GET /admin/cases

Lista todas as caixas com filtros, busca e ordenacao.

Query Parameters:

ParametroTipoObrigatorioDefaultValores
pagenumberNao1Min: 1
limitnumberNao20Min: 1, Max: 100
searchstringNao-Busca por nome
categorystringNao-Filtrar por categoria
isActivebooleanNao-Filtrar por status ativo
sortBystringNaocreatedAtCampo de ordenacao
sortOrderstringNaodescasc, desc

Response (200):

json
{
  "data": [
    {
      "id": "100",
      "name": "Star Case",
      "slug": "star-case",
      "description": "Uma caixa estelar com itens incriveis",
      "imageUrl": "https://cdn.example.com/cases/star.png",
      "bgColor": "#1a1a2e",
      "priceCents": "1990",
      "priceFormatted": "R$ 19,90",
      "riskLevel": "MEDIUM",
      "categoryId": "cat_1",
      "categoryName": "Populares",
      "isHot": true,
      "isNew": false,
      "order": 1,
      "status": "ACTIVE",
      "totalOpenings": 3421,
      "itemCount": 20,
      "createdAt": "2025-10-01T00:00:00.000Z",
      "updatedAt": "2026-02-01T00:00:00.000Z"
    }
  ],
  "total": 15,
  "page": 1,
  "limit": 20,
  "totalPages": 1
}

GET /admin/cases/:id

Retorna detalhes completos de uma caixa incluindo todos os seus itens com probabilidades e hash ranges.

Path Parameters:

ParametroTipoDescricao
idstringID da caixa (bigint)

Response (200):

json
{
  "id": "100",
  "name": "Star Case",
  "slug": "star-case",
  "description": "Uma caixa estelar com itens incriveis",
  "imageUrl": "https://cdn.example.com/cases/star.png",
  "bgColor": "#1a1a2e",
  "priceCents": "1990",
  "priceFormatted": "R$ 19,90",
  "riskLevel": "MEDIUM",
  "categoryId": "cat_1",
  "categoryName": "Populares",
  "isHot": true,
  "isNew": false,
  "order": 1,
  "status": "ACTIVE",
  "totalOpenings": 3421,
  "itemCount": 20,
  "createdAt": "2025-10-01T00:00:00.000Z",
  "updatedAt": "2026-02-01T00:00:00.000Z",
  "items": [
    {
      "id": "ci_1",
      "itemId": "500",
      "name": "AK-47 | Redline",
      "imageUrl": "https://cdn.example.com/items/ak47-redline.png",
      "rarity": "CLASSIFIED",
      "valueCents": "15900",
      "valueFormatted": "R$ 159,00",
      "weight": 100,
      "dropChance": 1.0,
      "isRare": false
    },
    {
      "id": "ci_2",
      "itemId": "501",
      "name": "AWP | Dragon Lore",
      "imageUrl": "https://cdn.example.com/items/awp-dlore.png",
      "rarity": "COVERT",
      "valueCents": "70765",
      "valueFormatted": "R$ 707,65",
      "weight": 5,
      "dropChance": 0.05,
      "isRare": true
    }
  ]
}

POST /admin/cases

Cria uma nova caixa.

Request Body:

typescript
class CreateCaseDto {
  name: string;                                              // obrigatorio
  description?: string;                                       // opcional
  imageUrl: string;                                           // obrigatorio
  bgColor?: string;                                           // opcional
  priceCents: number;                                         // obrigatorio, min: 1
  riskLevel: 'LOW' | 'MEDIUM' | 'HIGH' | 'MAXIMUM';         // obrigatorio
  categoryId: string;                                         // obrigatorio
  isHot?: boolean;                                            // opcional
  isNew?: boolean;                                            // opcional
  order?: number;                                             // opcional
}

Exemplo de Request:

json
{
  "name": "Diamond Case",
  "description": "A caixa mais rara do site",
  "imageUrl": "https://cdn.example.com/cases/diamond.png",
  "bgColor": "#2d1b69",
  "priceCents": 4990,
  "riskLevel": "HIGH",
  "categoryId": "cat_premium",
  "isHot": true,
  "isNew": true,
  "order": 0
}

Response (201):

json
{
  "id": "150",
  "name": "Diamond Case",
  "slug": "diamond-case",
  "priceCents": "4990",
  "priceFormatted": "R$ 49,90",
  "riskLevel": "HIGH",
  "status": "ACTIVE",
  "createdAt": "2026-02-11T15:00:00.000Z"
}

PATCH /admin/cases/:id

Atualiza campos de uma caixa existente. Todos os campos sao opcionais (partial update).

Path Parameters:

ParametroTipoDescricao
idstringID da caixa (bigint)

Request Body:

typescript
class UpdateCaseDto {
  name?: string;
  description?: string;
  imageUrl?: string;
  bgColor?: string;
  priceCents?: number;                                         // min: 1
  riskLevel?: 'LOW' | 'MEDIUM' | 'HIGH' | 'MAXIMUM';
  categoryId?: string;
  isHot?: boolean;
  isNew?: boolean;
  order?: number;
  status?: 'ACTIVE' | 'INACTIVE' | 'COMING_SOON';
}

Exemplo de Request:

json
{
  "priceCents": 3990,
  "isHot": false,
  "status": "INACTIVE"
}

Response (200):

json
{
  "id": "100",
  "name": "Star Case",
  "priceCents": "3990",
  "priceFormatted": "R$ 39,90",
  "status": "INACTIVE",
  "updatedAt": "2026-02-11T15:30:00.000Z"
}

DELETE /admin/cases/:id

Remove uma caixa do sistema. A caixa so pode ser deletada se nao houver aberturas vinculadas (integridade referencial).

Path Parameters:

ParametroTipoDescricao
idstringID da caixa (bigint)

Response (200):

json
{
  "success": true
}

POST /admin/cases/:caseId/items

Adiciona um item a uma caixa. Apos adicionar, recalcula automaticamente os hash ranges e drop chances de todos os itens da caixa com base nos pesos (weights).

Path Parameters:

ParametroTipoDescricao
caseIdstringID da caixa (bigint)

Request Body:

typescript
class AddCaseItemDto {
  itemId: string;   // ID do item (bigint como string)
  weight: number;   // peso do item (min: 1), determina a probabilidade
}

Exemplo de Request:

json
{
  "itemId": "600",
  "weight": 500
}

Response (201):

json
{
  "id": "ci_50",
  "itemId": "600",
  "name": "M4A4 | Howl",
  "imageUrl": "https://cdn.example.com/items/m4a4-howl.png",
  "rarity": "COVERT",
  "valueCents": "35000",
  "valueFormatted": "R$ 350,00",
  "weight": 500,
  "dropChance": 5.0,
  "isRare": true
}

Erros Possiveis:

json
// 404 - Caixa nao encontrada
{ "statusCode": 404, "message": "Caixa nao encontrada" }

// 404 - Item nao encontrado
{ "statusCode": 404, "message": "Item nao encontrado" }

// 400 - Item ja esta na caixa
{ "statusCode": 400, "message": "Este item ja esta na caixa" }

Recalculo de Hash Ranges: Apos adicionar o item, o use case chama recalculateDropChances() que:

  1. Busca todos os itens da caixa ordenados por ID
  2. Calcula o peso total (totalWeight)
  3. Para cada item, calcula dropChance = (weight / totalWeight) * 100
  4. Distribui os hash ranges de 0 ate HASH_MAX (10.000.000) proporcionalmente
  5. O ultimo item sempre termina em 10.000.000 (garante cobertura total)
typescript
private async recalculateDropChances(caseId: bigint) {
  const HASH_MAX = 10000000;
  const caseItems = await this.prisma.caseItem.findMany({
    where: { caseId },
    orderBy: { id: 'asc' },
  });

  const totalWeight = caseItems.reduce((sum, ci) => sum + ci.weight, 0);

  let currentRangeStart = 0;
  for (let i = 0; i < caseItems.length; i++) {
    const ci = caseItems[i];
    const dropChance = (ci.weight / totalWeight) * 100;
    const rangeSize = Math.floor((ci.weight / totalWeight) * HASH_MAX);
    const hashRangeEnd = i === caseItems.length - 1
      ? HASH_MAX
      : currentRangeStart + rangeSize;

    await this.prisma.caseItem.update({
      where: { id: ci.id },
      data: { dropChance, hashRangeStart: currentRangeStart, hashRangeEnd },
    });

    currentRangeStart = hashRangeEnd + 1;
  }
}

DELETE /admin/cases/:caseId/items/:itemId

Remove um item de uma caixa. Apos remover, recalcula os hash ranges e drop chances dos itens restantes.

Path Parameters:

ParametroTipoDescricao
caseIdstringID da caixa (bigint)
itemIdstringID do item (bigint)

Response (200):

json
{
  "success": true
}

Gerenciamento de Itens

O grupo de itens expoe 5 endpoints para CRUD completo de itens (skins/armas).

GET /admin/items

Lista todos os itens com filtros por raridade, categoria e busca textual.

Query Parameters:

ParametroTipoObrigatorioDefaultValores
pagenumberNao1Min: 1
limitnumberNao20Min: 1, Max: 100
searchstringNao-Busca por nome/marketHashName
raritystringNao-CONSUMER, INDUSTRIAL, MIL_SPEC, RESTRICTED, CLASSIFIED, COVERT, EXTRAORDINARY
categorystringNao-PISTOL, RIFLE, SMG, SNIPER, SHOTGUN, HEAVY, KNIFE, GLOVE, OTHER
sortBystringNaocreatedAtCampo de ordenacao
sortOrderstringNaodescasc, desc

Response (200):

json
{
  "data": [
    {
      "id": "500",
      "name": "AK-47 | Redline",
      "marketHashName": "AK-47 | Redline (Field-Tested)",
      "description": "Uma skin classica do CS",
      "imageUrl": "https://cdn.example.com/items/ak47-redline.png",
      "rarity": "CLASSIFIED",
      "valueCents": "15900",
      "valueFormatted": "R$ 159,00",
      "weaponCategory": "RIFLE",
      "collection": "Phoenix Collection",
      "exterior": "FIELD_TESTED",
      "isStatTrak": false,
      "createdAt": "2025-09-15T00:00:00.000Z",
      "updatedAt": "2026-01-20T00:00:00.000Z"
    }
  ],
  "total": 350,
  "page": 1,
  "limit": 20,
  "totalPages": 18
}

GET /admin/items/:id

Retorna detalhes completos de um item.

Path Parameters:

ParametroTipoDescricao
idstringID do item (bigint)

Response (200):

json
{
  "id": "500",
  "name": "AK-47 | Redline",
  "marketHashName": "AK-47 | Redline (Field-Tested)",
  "description": "Uma skin classica do CS",
  "imageUrl": "https://cdn.example.com/items/ak47-redline.png",
  "rarity": "CLASSIFIED",
  "valueCents": "15900",
  "valueFormatted": "R$ 159,00",
  "weaponCategory": "RIFLE",
  "collection": "Phoenix Collection",
  "exterior": "FIELD_TESTED",
  "isStatTrak": false,
  "createdAt": "2025-09-15T00:00:00.000Z",
  "updatedAt": "2026-01-20T00:00:00.000Z"
}

POST /admin/items

Cria um novo item no catalogo.

Request Body:

typescript
class CreateItemDto {
  name: string;                    // obrigatorio
  marketHashName: string;          // obrigatorio (nome unico do mercado Steam)
  description?: string;            // opcional
  imageUrl: string;                // obrigatorio
  rarity: 'CONSUMER' | 'INDUSTRIAL' | 'MIL_SPEC' | 'RESTRICTED'
        | 'CLASSIFIED' | 'COVERT' | 'EXTRAORDINARY';  // obrigatorio
  valueCents: number;              // obrigatorio, min: 1
  weaponCategory: 'PISTOL' | 'RIFLE' | 'SMG' | 'SNIPER'
                | 'SHOTGUN' | 'HEAVY' | 'KNIFE' | 'GLOVE' | 'OTHER'; // obrigatorio
  collection?: string;             // opcional
  exterior?: 'FACTORY_NEW' | 'MINIMAL_WEAR' | 'FIELD_TESTED'
           | 'WELL_WORN' | 'BATTLE_SCARRED' | 'NOT_PAINTED'; // opcional
  isStatTrak?: boolean;            // opcional, default: false
}

Exemplo de Request:

json
{
  "name": "AWP | Asiimov",
  "marketHashName": "AWP | Asiimov (Field-Tested)",
  "imageUrl": "https://cdn.example.com/items/awp-asiimov.png",
  "rarity": "COVERT",
  "valueCents": 25000,
  "weaponCategory": "SNIPER",
  "collection": "Operation Phoenix Weapon Case",
  "exterior": "FIELD_TESTED",
  "isStatTrak": false
}

Response (201):

json
{
  "id": "700",
  "name": "AWP | Asiimov",
  "marketHashName": "AWP | Asiimov (Field-Tested)",
  "imageUrl": "https://cdn.example.com/items/awp-asiimov.png",
  "rarity": "COVERT",
  "valueCents": "25000",
  "valueFormatted": "R$ 250,00",
  "weaponCategory": "SNIPER",
  "collection": "Operation Phoenix Weapon Case",
  "exterior": "FIELD_TESTED",
  "isStatTrak": false,
  "createdAt": "2026-02-11T16:00:00.000Z",
  "updatedAt": "2026-02-11T16:00:00.000Z"
}

PATCH /admin/items/:id

Atualiza campos de um item existente. Todos os campos sao opcionais (partial update).

Path Parameters:

ParametroTipoDescricao
idstringID do item (bigint)

Request Body:

typescript
class UpdateItemDto {
  name?: string;
  description?: string;
  imageUrl?: string;
  rarity?: 'CONSUMER' | 'INDUSTRIAL' | 'MIL_SPEC' | 'RESTRICTED'
         | 'CLASSIFIED' | 'COVERT' | 'EXTRAORDINARY';
  valueCents?: number;              // min: 1
  weaponCategory?: 'PISTOL' | 'RIFLE' | 'SMG' | 'SNIPER'
                 | 'SHOTGUN' | 'HEAVY' | 'KNIFE' | 'GLOVE' | 'OTHER';
  collection?: string;
  exterior?: 'FACTORY_NEW' | 'MINIMAL_WEAR' | 'FIELD_TESTED'
           | 'WELL_WORN' | 'BATTLE_SCARRED' | 'NOT_PAINTED';
  isStatTrak?: boolean;
}

Exemplo de Request:

json
{
  "valueCents": 28000,
  "rarity": "COVERT"
}

Response (200):

json
{
  "id": "700",
  "name": "AWP | Asiimov",
  "valueCents": "28000",
  "valueFormatted": "R$ 280,00",
  "rarity": "COVERT",
  "updatedAt": "2026-02-11T16:30:00.000Z"
}

DELETE /admin/items/:id

Remove um item do catalogo. O item so pode ser deletado se nao estiver vinculado a nenhuma caixa ou inventario de usuario (integridade referencial).

Path Parameters:

ParametroTipoDescricao
idstringID do item (bigint)

Response (200):

json
{
  "success": true
}

Gerenciamento de Batalhas

O grupo de batalhas expoe 3 endpoints para visualizacao e cancelamento de batalhas.

GET /admin/battles

Lista todas as batalhas com filtros por status, tipo e modo.

Query Parameters:

ParametroTipoObrigatorioDefaultValores
pagenumberNao1Min: 1
limitnumberNao20Min: 1, Max: 100
statusstringNao-WAITING, IN_PROGRESS, FINISHED, CANCELLED
typestringNao-ONE_VS_ONE, TWO_VS_TWO, THREE_VS_THREE, FOUR_VS_FOUR, ONE_VS_ONE_VS_ONE, ONE_VS_ONE_VS_ONE_VS_ONE
modestringNao-NORMAL, FLIP
sortBystringNaocreatedAtCampo de ordenacao
sortOrderstringNaodescasc, desc

Response (200):

json
{
  "data": [
    {
      "id": "200",
      "mode": "NORMAL",
      "type": "ONE_VS_ONE",
      "status": "FINISHED",
      "creatorId": "1",
      "creatorUsername": "PlayerOne",
      "creatorAvatarUrl": "https://avatars.steamstatic.com/abc.jpg",
      "winnerId": "2",
      "winnerUsername": "PlayerTwo",
      "winnerTeam": 2,
      "totalValueCents": "5970",
      "totalValueFormatted": "R$ 59,70",
      "isPrivate": false,
      "playerCount": 2,
      "caseCount": 3,
      "createdAt": "2026-02-09T12:00:00.000Z",
      "startedAt": "2026-02-09T12:01:00.000Z",
      "finishedAt": "2026-02-09T12:05:00.000Z"
    }
  ],
  "total": 203,
  "page": 1,
  "limit": 20,
  "totalPages": 11
}

GET /admin/battles/:id

Retorna detalhes completos de uma batalha incluindo jogadores, caixas e rodadas.

Path Parameters:

ParametroTipoDescricao
idstringID da batalha (bigint)

Response (200):

typescript
interface BattleDetailResponseDto {
  id: string;
  mode: string;
  type: string;
  status: string;
  creatorId: string;
  creatorUsername: string;
  creatorAvatarUrl?: string;
  winnerId?: string;
  winnerUsername?: string;
  winnerTeam?: number;
  totalValueCents: string;
  totalValueFormatted: string;
  isPrivate: boolean;
  playerCount: number;
  caseCount: number;
  createdAt: string;
  startedAt?: string;
  finishedAt?: string;
  players: BattlePlayerResponseDto[];
  cases: BattleCaseResponseDto[];
  rounds: BattleRoundResponseDto[];
}

Exemplo de Response:

json
{
  "id": "200",
  "mode": "NORMAL",
  "type": "ONE_VS_ONE",
  "status": "FINISHED",
  "creatorId": "1",
  "creatorUsername": "PlayerOne",
  "winnerId": "2",
  "winnerUsername": "PlayerTwo",
  "winnerTeam": 2,
  "totalValueCents": "5970",
  "totalValueFormatted": "R$ 59,70",
  "isPrivate": false,
  "playerCount": 2,
  "caseCount": 3,
  "createdAt": "2026-02-09T12:00:00.000Z",
  "startedAt": "2026-02-09T12:01:00.000Z",
  "finishedAt": "2026-02-09T12:05:00.000Z",
  "players": [
    {
      "id": "bp_1",
      "odUserId": "1",
      "odUsername": "PlayerOne",
      "odAvatarUrl": "https://avatars.steamstatic.com/abc.jpg",
      "team": 1,
      "totalProfitCents": "-5970",
      "totalProfitFormatted": "-R$ 59,70",
      "position": 2,
      "joinedAt": "2026-02-09T12:00:00.000Z"
    },
    {
      "id": "bp_2",
      "odUserId": "2",
      "odUsername": "PlayerTwo",
      "team": 2,
      "totalProfitCents": "5970",
      "totalProfitFormatted": "R$ 59,70",
      "position": 1,
      "joinedAt": "2026-02-09T12:00:30.000Z"
    }
  ],
  "cases": [
    {
      "id": "bc_1",
      "caseId": "100",
      "caseName": "Star Case",
      "caseImageUrl": "https://cdn.example.com/cases/star.png",
      "order": 1
    }
  ],
  "rounds": [
    {
      "id": "br_1",
      "roundNumber": 1,
      "playerId": "bp_1",
      "playerUsername": "PlayerOne",
      "itemId": "500",
      "itemName": "AK-47 | Redline",
      "itemImage": "https://cdn.example.com/items/ak47-redline.png",
      "itemRarity": "CLASSIFIED",
      "itemValueCents": "15900",
      "itemValueFormatted": "R$ 159,00",
      "isFlip": false,
      "createdAt": "2026-02-09T12:02:00.000Z"
    }
  ]
}

POST /admin/battles/:id/cancel

Cancela uma batalha e reembolsa todos os jogadores participantes. O valor reembolsado e o custo total das caixas selecionadas.

Path Parameters:

ParametroTipoDescricao
idstringID da batalha (bigint)

Request Body:

typescript
class CancelBattleDto {
  reason: string;
}

Exemplo de Request:

json
{
  "reason": "Batalha com problema tecnico"
}

Response (200):

json
{
  "success": true,
  "reason": "Batalha com problema tecnico",
  "refundedPlayers": 2
}

Erros Possiveis:

json
// 404 - Batalha nao encontrada
{ "statusCode": 404, "message": "Batalha nao encontrada" }

// 400 - Batalha ja finalizada
{ "statusCode": 400, "message": "Nao e possivel cancelar uma batalha ja finalizada" }

// 400 - Batalha ja cancelada
{ "statusCode": 400, "message": "Batalha ja esta cancelada" }

Comportamento Interno:

  1. Verifica se a batalha existe e nao esta FINISHED ou CANCELLED
  2. Atualiza status para CANCELLED e define finishedAt
  3. Calcula o custo total das caixas (totalCaseCost)
  4. Para cada jogador, cria uma transacao de credito via TransactionService.credit() com reason BATTLE_CANCELLED_REFUND
  5. Metadata da transacao inclui battleId, reason e adminUserId para auditoria

Gerenciamento Financeiro

O grupo financeiro expoe 8 endpoints para gerenciamento de depositos, saques e transacoes.

GET /admin/deposits

Lista todos os depositos com filtros.

Query Parameters:

ParametroTipoObrigatorioDefaultValores
pagenumberNao1Min: 1
limitnumberNao20Min: 1, Max: 100
statusstringNao-PENDING, CONFIRMED, FAILED, EXPIRED
methodstringNao-PIX, CREDIT_CARD, CRYPTO
userIdstringNao-Filtrar por usuario
sortBystringNaocreatedAtCampo de ordenacao
sortOrderstringNaodescasc, desc

Response (200):

json
{
  "data": [
    {
      "id": "dep_1",
      "userId": "1",
      "username": "PlayerOne",
      "avatarUrl": "https://avatars.steamstatic.com/abc.jpg",
      "amountCents": "50000",
      "amountFormatted": "R$ 500,00",
      "method": "PIX",
      "status": "PENDING",
      "externalId": "pix_abc123",
      "createdAt": "2026-02-11T14:00:00.000Z",
      "confirmedAt": null
    }
  ],
  "total": 156,
  "page": 1,
  "limit": 20,
  "totalPages": 8
}

POST /admin/deposits/:id/confirm

Confirma manualmente um deposito pendente, creditando o saldo no usuario.

Path Parameters:

ParametroTipoDescricao
idstringID do deposito (bigint)

Response (200):

json
{
  "success": true
}

POST /admin/deposits/:id/reject

Rejeita um deposito pendente.

Path Parameters:

ParametroTipoDescricao
idstringID do deposito (bigint)

Request Body:

typescript
class RejectDepositDto {
  reason: string;
}

Exemplo de Request:

json
{
  "reason": "Pagamento nao identificado"
}

Response (200):

json
{
  "success": true
}

GET /admin/withdrawals

Lista todos os saques com filtros.

Query Parameters:

ParametroTipoObrigatorioDefaultValores
pagenumberNao1Min: 1
limitnumberNao20Min: 1, Max: 100
statusstringNao-PENDING_2FA, PROCESSING, COMPLETED, FAILED, CANCELLED
methodstringNao-STEAM_TRADE, PIX
userIdstringNao-Filtrar por usuario
sortBystringNaocreatedAtCampo de ordenacao
sortOrderstringNaodescasc, desc

Response (200):

json
{
  "data": [
    {
      "id": "wd_1",
      "userId": "1",
      "username": "PlayerOne",
      "avatarUrl": "https://avatars.steamstatic.com/abc.jpg",
      "amountCents": "30000",
      "amountFormatted": "R$ 300,00",
      "method": "PIX",
      "status": "PROCESSING",
      "destination": "email@example.com",
      "failureReason": null,
      "createdAt": "2026-02-11T10:00:00.000Z",
      "processedAt": null
    }
  ],
  "total": 45,
  "page": 1,
  "limit": 20,
  "totalPages": 3
}

POST /admin/withdrawals/:id/approve

Aprova um saque pendente, movendo-o para o status de processamento.

Path Parameters:

ParametroTipoDescricao
idstringID do saque (bigint)

Response (200):

json
{
  "success": true
}

POST /admin/withdrawals/:id/reject

Rejeita um saque e reembolsa o usuario (credita o valor de volta no saldo).

Path Parameters:

ParametroTipoDescricao
idstringID do saque (bigint)

Request Body:

typescript
class RejectWithdrawalDto {
  reason: string;
}

Exemplo de Request:

json
{
  "reason": "Dados de conta invalidos"
}

Response (200):

json
{
  "success": true
}

POST /admin/withdrawals/:id/complete

Marca um saque como concluido apos o pagamento ter sido efetivamente realizado.

Path Parameters:

ParametroTipoDescricao
idstringID do saque (bigint)

Response (200):

json
{
  "success": true
}

GET /admin/transactions

Lista todas as transacoes financeiras da plataforma com filtros.

Query Parameters:

ParametroTipoObrigatorioDefaultValores
pagenumberNao1Min: 1
limitnumberNao20Min: 1, Max: 100
typestringNao-CREDIT, DEBIT
userIdstringNao-Filtrar por usuario
reasonstringNao-Filtrar por motivo (ex: DEPOSIT, CASE_OPENING)
sortBystringNaocreatedAtCampo de ordenacao
sortOrderstringNaodescasc, desc

Response (200):

json
{
  "data": [
    {
      "id": "tx_1",
      "userId": "1",
      "username": "PlayerOne",
      "avatarUrl": "https://avatars.steamstatic.com/abc.jpg",
      "type": "CREDIT",
      "amountCents": "50000",
      "amountFormatted": "R$ 500,00",
      "reason": "DEPOSIT",
      "status": "COMPLETED",
      "metadata": {
        "depositId": "dep_1",
        "method": "PIX"
      },
      "createdAt": "2026-02-10T14:00:00.000Z"
    },
    {
      "id": "tx_2",
      "userId": "1",
      "username": "PlayerOne",
      "type": "DEBIT",
      "amountCents": "1990",
      "amountFormatted": "R$ 19,90",
      "reason": "CASE_OPENING",
      "status": "COMPLETED",
      "metadata": {
        "caseId": "100",
        "caseOpeningId": "98712345678"
      },
      "createdAt": "2026-02-10T14:05:00.000Z"
    }
  ],
  "total": 5234,
  "page": 1,
  "limit": 20,
  "totalPages": 262
}

Analytics

O grupo de analytics expoe 8 endpoints para analise detalhada de performance da plataforma, metricas de jogos e lucratividade.

GET /admin/analytics/rtp-by-case

Retorna o RTP (Return To Player) calculado para cada caixa com base nas aberturas reais, comparando com o RTP esperado (teorico).

Query Parameters:

ParametroTipoObrigatorioDefaultValores
periodstringNaomonthday, week, month, year

Response (200):

typescript
interface RtpByCaseItem {
  caseId: string;
  caseName: string;
  caseImageUrl: string;
  totalOpenings: number;
  totalWageredCents: string;
  totalWageredFormatted: string;
  totalPaidCents: string;
  totalPaidFormatted: string;
  profitCents: string;
  profitFormatted: string;
  rtpPercent: number;           // RTP real (calculado das aberturas)
  expectedRtpPercent: number | null; // RTP teorico (calculado dos weights)
}

Exemplo de Response:

json
[
  {
    "caseId": "100",
    "caseName": "Star Case",
    "caseImageUrl": "https://cdn.example.com/cases/star.png",
    "totalOpenings": 3421,
    "totalWageredCents": "6807990",
    "totalWageredFormatted": "R$ 68.079,90",
    "totalPaidCents": "4776328",
    "totalPaidFormatted": "R$ 47.763,28",
    "profitCents": "2031662",
    "profitFormatted": "R$ 20.316,62",
    "rtpPercent": 70.16,
    "expectedRtpPercent": 70.16
  },
  {
    "caseId": "101",
    "caseName": "Snow Case",
    "caseImageUrl": "https://cdn.example.com/cases/snow.png",
    "totalOpenings": 1523,
    "totalWageredCents": "2421570",
    "totalWageredFormatted": "R$ 24.215,70",
    "totalPaidCents": "1533178",
    "totalPaidFormatted": "R$ 15.331,78",
    "profitCents": "888392",
    "profitFormatted": "R$ 8.883,92",
    "rtpPercent": 63.32,
    "expectedRtpPercent": 63.32
  }
]

Calculo do RTP Real: rtpPercent = (totalPaidCents / totalWageredCents) * 100Calculo do RTP Esperado: soma de (dropChance / 100) * valueCents de cada item, dividido pelo preco da caixa

A lista e ordenada por profitCents decrescente (caixas mais lucrativas primeiro).


GET /admin/analytics/battle-rtp

Retorna estatisticas de RTP de batalhas com breakdown por tipo (1v1, 2v2, etc.), modo (NORMAL, FLIP) e combinacao (tipo+modo).

Query Parameters:

ParametroTipoObrigatorioDefaultValores
periodstringNaomonthday, week, month, year

Response (200):

typescript
interface BattleRtpResponse {
  totalBattles: number;
  totalWageredCents: string;
  totalWageredFormatted: string;
  totalPaidCents: string;
  totalPaidFormatted: string;
  profitCents: string;
  profitFormatted: string;
  rtpPercent: number;
  byType: Record<string, BreakdownEntry>;
  byMode: Record<string, BreakdownEntry>;
  byCombination: Record<string, BreakdownEntry>;
}

interface BreakdownEntry {
  count: number;
  wageredCents: string;
  wageredFormatted: string;
  profitCents: string;
  profitFormatted: string;
  rtpPercent: number;
}

Exemplo de Response:

json
{
  "totalBattles": 203,
  "totalWageredCents": "2500000",
  "totalWageredFormatted": "R$ 25.000,00",
  "totalPaidCents": "2100000",
  "totalPaidFormatted": "R$ 21.000,00",
  "profitCents": "400000",
  "profitFormatted": "R$ 4.000,00",
  "rtpPercent": 84.0,
  "byType": {
    "ONE_VS_ONE": {
      "count": 150,
      "wageredCents": "1800000",
      "wageredFormatted": "R$ 18.000,00",
      "profitCents": "280000",
      "profitFormatted": "R$ 2.800,00",
      "rtpPercent": 84.44
    },
    "TWO_VS_TWO": {
      "count": 53,
      "wageredCents": "700000",
      "wageredFormatted": "R$ 7.000,00",
      "profitCents": "120000",
      "profitFormatted": "R$ 1.200,00",
      "rtpPercent": 82.86
    }
  },
  "byMode": {
    "NORMAL": {
      "count": 180,
      "wageredCents": "2200000",
      "wageredFormatted": "R$ 22.000,00",
      "profitCents": "350000",
      "profitFormatted": "R$ 3.500,00",
      "rtpPercent": 84.09
    },
    "FLIP": {
      "count": 23,
      "wageredCents": "300000",
      "wageredFormatted": "R$ 3.000,00",
      "profitCents": "50000",
      "profitFormatted": "R$ 500,00",
      "rtpPercent": 83.33
    }
  },
  "byCombination": {
    "ONE_VS_ONE . NORMAL": {
      "count": 130,
      "wageredCents": "1500000",
      "wageredFormatted": "R$ 15.000,00",
      "profitCents": "230000",
      "profitFormatted": "R$ 2.300,00",
      "rtpPercent": 84.67
    }
  }
}

Calculo do Lucro de Batalhas: O lucro da plataforma em batalhas vem da diferenca entre itens removidos para o pool (REMOVED_TO_POOL) e itens adicionados do pool para compensacao (ADDED_FROM_POOL) via BattleSettlement.


GET /admin/analytics/upgrade-stats

Retorna estatisticas do sistema de upgrade incluindo taxa de sucesso, valores investidos e lucro.

Query Parameters:

ParametroTipoObrigatorioDefaultValores
periodstringNaomonthday, week, month, year

Response (200):

typescript
interface UpgradeStatsResponse {
  totalUpgrades: number;
  successRate: number;
  totalInvestedCents: string;
  totalInvestedFormatted: string;
  totalWonCents: string;
  totalWonFormatted: string;
  profitCents: string;
  profitFormatted: string;
  averageChance: number;
}

Exemplo de Response:

json
{
  "totalUpgrades": 890,
  "successRate": 32.47,
  "totalInvestedCents": "1200000",
  "totalInvestedFormatted": "R$ 12.000,00",
  "totalWonCents": "960000",
  "totalWonFormatted": "R$ 9.600,00",
  "profitCents": "240000",
  "profitFormatted": "R$ 2.400,00",
  "averageChance": 35.20
}

Calculo:

  • totalInvested = soma de valueInvestedCents + balanceUsedCents de todos os upgrades
  • totalWon = soma de targetValueCents dos upgrades com success = true
  • successRate = (upgrades com sucesso / total de upgrades) * 100
  • profitCents = totalInvested - totalWon

GET /admin/analytics/bot-profitability

Retorna a lucratividade dos bots em batalhas. Bots sao identificados pelo prefixo BOT_ no campo steamId.

Query Parameters:

ParametroTipoObrigatorioDefaultValores
periodstringNaomonthday, week, month, year

Response (200):

typescript
interface BotProfitabilityItem {
  userId: string;
  username: string;
  totalBattles: number;
  totalProfitCents: string;
  totalProfitFormatted: string;
  winRate: number;
}

Exemplo de Response:

json
[
  {
    "userId": "999",
    "username": "BOT_Alpha",
    "totalBattles": 45,
    "totalProfitCents": "125000",
    "totalProfitFormatted": "R$ 1.250,00",
    "winRate": 55.56
  },
  {
    "userId": "998",
    "username": "BOT_Beta",
    "totalBattles": 32,
    "totalProfitCents": "-35000",
    "totalProfitFormatted": "-R$ 350,00",
    "winRate": 43.75
  }
]

A lista e ordenada por totalProfitCents decrescente (bots mais lucrativos primeiro). Retorna array vazio se nao houver bots cadastrados.


GET /admin/analytics/user-stats

Retorna estatisticas globais de usuarios: novos cadastros, usuarios ativos e metricas de receita por usuario.

Query Parameters: Nenhum

Response (200):

typescript
interface UserStatsResponse {
  newUsersToday: number;
  newUsersThisWeek: number;
  newUsersThisMonth: number;
  activeUsersToday: number;
  depositingUsers: number;
  arpuCents: string;
  arpuFormatted: string;
}

Exemplo de Response:

json
{
  "newUsersToday": 12,
  "newUsersThisWeek": 78,
  "newUsersThisMonth": 245,
  "activeUsersToday": 87,
  "depositingUsers": 412,
  "arpuCents": "8200",
  "arpuFormatted": "R$ 82,00"
}

Calculo:

  • activeUsersToday: usuarios com lastLoginAt >= inicio do dia
  • depositingUsers: quantidade de usuarios distintos com pelo menos 1 deposito confirmado
  • arpu (Average Revenue Per User): total de depositos confirmados / total de usuarios

GET /admin/analytics/financial-overview

Retorna visao financeira detalhada com breakdown por metodo de pagamento, fluxo liquido e medias.

Query Parameters:

ParametroTipoObrigatorioDefaultValores
periodstringNaomonthday, week, month, year

Response (200):

typescript
interface FinancialOverviewResponse {
  depositsByMethod: Record<string, MethodBreakdown>;
  withdrawalsByMethod: Record<string, MethodBreakdown>;
  netFlowCents: string;
  netFlowFormatted: string;
  avgDepositCents: string;
  avgDepositFormatted: string;
  avgWithdrawalCents: string;
  avgWithdrawalFormatted: string;
}

interface MethodBreakdown {
  count: number;
  totalCents: string;
  totalFormatted: string;
}

Exemplo de Response:

json
{
  "depositsByMethod": {
    "PIX": {
      "count": 320,
      "totalCents": "8500000",
      "totalFormatted": "R$ 85.000,00"
    },
    "CREDIT_CARD": {
      "count": 45,
      "totalCents": "2300000",
      "totalFormatted": "R$ 23.000,00"
    },
    "CRYPTO": {
      "count": 12,
      "totalCents": "1700000",
      "totalFormatted": "R$ 17.000,00"
    }
  },
  "withdrawalsByMethod": {
    "PIX": {
      "count": 89,
      "totalCents": "4200000",
      "totalFormatted": "R$ 42.000,00"
    },
    "STEAM_TRADE": {
      "count": 34,
      "totalCents": "1800000",
      "totalFormatted": "R$ 18.000,00"
    }
  },
  "netFlowCents": "6500000",
  "netFlowFormatted": "R$ 65.000,00",
  "avgDepositCents": "33156",
  "avgDepositFormatted": "R$ 331,56",
  "avgWithdrawalCents": "48780",
  "avgWithdrawalFormatted": "R$ 487,80"
}

Calculo:

  • netFlow = totalDepositos - totalSaques
  • avgDeposit = totalDepositos / quantidadeDepositos
  • avgWithdrawal = totalSaques / quantidadeSaques
  • Apenas depositos CONFIRMED e saques COMPLETED sao considerados

GET /admin/analytics/ggr

Retorna o GGR (Gross Gaming Revenue) com breakdown detalhado por tipo de jogo (caixas, upgrades, batalhas). Suporta filtro por usuario especifico.

Query Parameters:

ParametroTipoObrigatorioDefaultValores
periodstringNaomonthday, week, month, year
userIdstringNao-Filtrar GGR de usuario especifico

Response (200):

typescript
interface GgrResponse {
  totalGgrCents: string;
  totalGgrFormatted: string;

  caseOpeningGgr: {
    wageredCents: string;
    wageredFormatted: string;
    paidCents: string;
    paidFormatted: string;
    ggrCents: string;
    ggrFormatted: string;
    count: number;
    rtpPercent: number;
  };

  upgradeGgr: {
    wageredCents: string;
    wageredFormatted: string;
    paidCents: string;
    paidFormatted: string;
    ggrCents: string;
    ggrFormatted: string;
    count: number;
    rtpPercent: number;
  };

  battleGgr: {
    removedToPoolCents: string;
    removedToPoolFormatted: string;
    addedFromPoolCents: string;
    addedFromPoolFormatted: string;
    ggrCents: string;
    ggrFormatted: string;
    count: number;
  };

  period: string;
}

Exemplo de Response:

json
{
  "totalGgrCents": "1250000",
  "totalGgrFormatted": "R$ 12.500,00",
  "caseOpeningGgr": {
    "wageredCents": "6807990",
    "wageredFormatted": "R$ 68.079,90",
    "paidCents": "4776328",
    "paidFormatted": "R$ 47.763,28",
    "ggrCents": "2031662",
    "ggrFormatted": "R$ 20.316,62",
    "count": 3421,
    "rtpPercent": 70.16
  },
  "upgradeGgr": {
    "wageredCents": "1200000",
    "wageredFormatted": "R$ 12.000,00",
    "paidCents": "960000",
    "paidFormatted": "R$ 9.600,00",
    "ggrCents": "240000",
    "ggrFormatted": "R$ 2.400,00",
    "count": 890,
    "rtpPercent": 80.0
  },
  "battleGgr": {
    "removedToPoolCents": "500000",
    "removedToPoolFormatted": "R$ 5.000,00",
    "addedFromPoolCents": "420000",
    "addedFromPoolFormatted": "R$ 4.200,00",
    "ggrCents": "80000",
    "ggrFormatted": "R$ 800,00",
    "count": 156
  },
  "period": "month"
}

Calculo por Tipo de Jogo:

  • Caixas: GGR = soma(pricePaidCents) - soma(itemValueCents)
  • Upgrades: GGR = soma(valueInvestedCents + balanceUsedCents) - soma(targetValueCents dos sucedidos)
  • Batalhas (global): GGR = REMOVED_TO_POOL - ADDED_FROM_POOL (via BattleSettlement)
  • Batalhas (por usuario): GGR = -(totalProfitCents do jogador)

Quando userId e fornecido, as batalhas sao calculadas pela perspectiva do jogador usando battlePlayer.totalProfitCents.


GET /admin/analytics/ggr-by-user

Retorna o ranking de GGR por usuario, mostrando quanto cada jogador contribuiu de receita para a plataforma por tipo de jogo.

Query Parameters:

ParametroTipoObrigatorioDefaultValores
periodstringNaomonthday, week, month, year
pagenumberNao1Min: 1
limitnumberNao20Min: 1, Max: 100
sortOrderstringNaodescasc, desc

Response (200):

typescript
interface PaginatedResponse<GgrByUserItem> {
  data: GgrByUserItem[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}

interface GgrByUserItem {
  userId: string;
  username: string;
  avatarUrl?: string;
  caseGgrCents: string;
  caseGgrFormatted: string;
  upgradeGgrCents: string;
  upgradeGgrFormatted: string;
  battleGgrCents: string;
  battleGgrFormatted: string;
  totalGgrCents: string;
  totalGgrFormatted: string;
}

Exemplo de Response:

json
{
  "data": [
    {
      "userId": "1",
      "username": "HighRoller",
      "avatarUrl": "https://avatars.steamstatic.com/abc.jpg",
      "caseGgrCents": "350000",
      "caseGgrFormatted": "R$ 3.500,00",
      "upgradeGgrCents": "120000",
      "upgradeGgrFormatted": "R$ 1.200,00",
      "battleGgrCents": "80000",
      "battleGgrFormatted": "R$ 800,00",
      "totalGgrCents": "550000",
      "totalGgrFormatted": "R$ 5.500,00"
    },
    {
      "userId": "5",
      "username": "LuckyPlayer",
      "caseGgrCents": "-250000",
      "caseGgrFormatted": "-R$ 2.500,00",
      "upgradeGgrCents": "50000",
      "upgradeGgrFormatted": "R$ 500,00",
      "battleGgrCents": "-100000",
      "battleGgrFormatted": "-R$ 1.000,00",
      "totalGgrCents": "-300000",
      "totalGgrFormatted": "-R$ 3.000,00"
    }
  ],
  "total": 412,
  "page": 1,
  "limit": 20,
  "totalPages": 21
}

Observacoes:

  • GGR negativo indica que o jogador ganhou mais do que apostou (prejuizo para a casa)
  • Bots (steamId com prefixo BOT_) sao excluidos do ranking
  • A query usa CTEs (Common Table Expressions) com JOINs entre case_openings, upgrades e battle_settlements para calcular o GGR por tipo
  • Somente usuarios que participaram de pelo menos uma atividade no periodo sao listados
  • Ordenacao padrao e por total_ggr DESC (maiores geradores de receita primeiro)

DTOs e Interfaces de Resposta

Paginacao Padrao

Todos os endpoints de listagem seguem o mesmo padrao de paginacao:

typescript
class PaginationQueryDto {
  page?: number = 1;    // Min: 1
  limit?: number = 20;  // Min: 1, Max: 100
}

interface PaginatedResponse<T> {
  data: T[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}

Periodos de Analise

Os endpoints de analytics aceitam o parametro period que determina o intervalo de dados analisados:

typescript
class AnalyticsQueryDto {
  period?: 'day' | 'week' | 'month' | 'year' = 'month';
}
PeriodoIntervalo
dayUltimas 24 horas
weekUltimos 7 dias
monthUltimos 30 dias
yearUltimos 12 meses

Formatacao Monetaria

Todos os valores monetarios sao retornados em dois formatos:

  • *Cents (string): Valor inteiro em centavos de real (BRL), como string para preservar precisao de BigInt
  • *Formatted (string): Valor formatado em reais via MoneyService.format(), no formato R$ X.XXX,XX

Exemplo: "balanceCents": "186321" e "balanceFormatted": "R$ 1.863,21"

Enums Utilizados

Status do Usuario:

typescript
type UserStatus = 'ACTIVE' | 'SUSPENDED' | 'BANNED';

Role do Usuario:

typescript
type UserRole = 'USER' | 'ADMIN' | 'SUPER_ADMIN';

Raridade do Item:

typescript
type ItemRarity = 'CONSUMER' | 'INDUSTRIAL' | 'MIL_SPEC' | 'RESTRICTED'
                | 'CLASSIFIED' | 'COVERT' | 'EXTRAORDINARY';

Categoria de Arma:

typescript
type WeaponCategory = 'PISTOL' | 'RIFLE' | 'SMG' | 'SNIPER'
                    | 'SHOTGUN' | 'HEAVY' | 'KNIFE' | 'GLOVE' | 'OTHER';

Exterior do Item:

typescript
type ItemExterior = 'FACTORY_NEW' | 'MINIMAL_WEAR' | 'FIELD_TESTED'
                  | 'WELL_WORN' | 'BATTLE_SCARRED' | 'NOT_PAINTED';

Nivel de Risco:

typescript
type RiskLevel = 'LOW' | 'MEDIUM' | 'HIGH' | 'MAXIMUM';

Status da Caixa:

typescript
type CaseStatus = 'ACTIVE' | 'INACTIVE' | 'COMING_SOON';

Status da Batalha:

typescript
type BattleStatus = 'WAITING' | 'IN_PROGRESS' | 'FINISHED' | 'CANCELLED';

Tipo de Batalha:

typescript
type BattleType = 'ONE_VS_ONE' | 'TWO_VS_TWO' | 'THREE_VS_THREE' | 'FOUR_VS_FOUR'
                | 'ONE_VS_ONE_VS_ONE' | 'ONE_VS_ONE_VS_ONE_VS_ONE';

Modo de Batalha:

typescript
type BattleMode = 'NORMAL' | 'FLIP' | 'SHARED';

Status do Deposito:

typescript
type DepositStatus = 'PENDING' | 'CONFIRMED' | 'FAILED' | 'EXPIRED';

Metodo de Deposito:

typescript
type DepositMethod = 'PIX' | 'CREDIT_CARD' | 'CRYPTO';

Status do Saque:

typescript
type WithdrawalStatus = 'PENDING_2FA' | 'PROCESSING' | 'COMPLETED' | 'FAILED' | 'CANCELLED';

Metodo de Saque:

typescript
type WithdrawalMethod = 'STEAM_TRADE' | 'PIX';

Tipo de Transacao:

typescript
type TransactionType = 'CREDIT' | 'DEBIT';

Codigos de Erro HTTP

CodigoDescricaoQuando
200SucessoOperacao realizada
201CriadoNovo recurso criado (POST)
400Bad RequestValidacao falhou, dados invalidos
401UnauthorizedSessao invalida ou expirada
403ForbiddenUsuario sem role ADMIN/SUPER_ADMIN
404Not FoundRecurso nao encontrado
409ConflictConflito de dados (ex: item duplicado em caixa)
500Internal Server ErrorErro inesperado no servidor

Arquivos Relacionados

ArquivoDescricao
src/presentation/controllers/admin.controller.tsController com 44 endpoints
src/application/dto/admin.dto.tsDTOs de request e interfaces de response
src/presentation/guards/admin.guard.tsGuard de autorizacao admin
src/presentation/guards/auth.guard.tsGuard de autenticacao por sessao
src/application/services/money.service.tsFormatacao monetaria BRL
src/application/services/transaction.service.tsCredito/debito com double-entry
src/application/services/period.service.tsCalculo de datas por periodo
src/infrastructure/locks/lock.service.tsDistributed locks via Redlock
src/application/use-cases/admin/*.ts44 use cases individuais

Documentação Técnica - ProCases