Skip to content

Gerenciamento de Itens (Admin + Publico)

Visao Geral

O sistema de gerenciamento de itens do ProCases opera em duas frentes:

  1. Admin (WaxpeerController): 7 endpoints protegidos por AdminGuard para importacao, sincronizacao e gestao de itens. Rota base: /api/admin/items/*
  2. Publico (ItemsPublicController): 7 endpoints publicos para consulta de itens com margem de preco configuravel (PriceMarginService.getCurrent()). Rota base: /api/items/*

O fluxo de importacao de itens segue duas estrategias complementares:

  • Steam Database: importa o catalogo completo com raridades corretas a partir dos arquivos de jogo
  • Waxpeer API: atualiza precos em tempo real e fornece imagens

Arquitetura de Arquivos

CamadaArquivoResponsabilidade
Presentationsrc/presentation/controllers/waxpeer.controller.tsController admin (7 endpoints)
Presentationsrc/presentation/controllers/items-public.controller.tsController publico (7 endpoints)
Presentationsrc/presentation/modules/admin.module.tsModulo admin
Presentationsrc/presentation/modules/items-public.module.tsModulo publico
Applicationsrc/application/use-cases/admin/sync-steam-database.use-case.tsSync Steam Database
Applicationsrc/application/use-cases/admin/update-item-prices.use-case.tsAtualizar precos Waxpeer
Applicationsrc/application/use-cases/admin/sync-items-from-waxpeer.use-case.tsSync completo Waxpeer
Infrastructuresrc/infrastructure/cs2/steam-database.service.tsParser Steam Database
Infrastructuresrc/infrastructure/external-apis/waxpeer-snapshot.service.tsSnapshot de precos Waxpeer
Infrastructuresrc/infrastructure/external-apis/exchange-rate.service.tsCotacao USD/BRL
Infrastructuresrc/infrastructure/external-apis/item-name-parser.service.tsParser de nomes de itens

WaxpeerController (Admin - 7 Endpoints)

Todos os endpoints requerem AdminGuard.

POST /api/admin/items/sync-steam-database

Importa o catalogo completo de itens CS2 a partir dos arquivos de jogo hospedados no GitHub (SteamDatabase/GameTracking-CS2).

Body - SyncSteamDatabaseDto:

typescript
{
  weaponCategories?: WeaponCategory[];  // Filtrar por categoria
  includeDefaultWeapons?: boolean;      // Incluir armas sem skin (default: false)
  batchSize?: number;                   // Tamanho do batch (default: 100)
  updateExisting?: boolean;             // Atualizar existentes (default: false)
  forceRefresh?: boolean;               // Ignorar cache (default: false)
}

Response (200):

typescript
{
  success: true,
  source: "Steam Database",
  totalFetched: number,   // Total de itens buscados
  created: number,        // Novos itens criados
  updated: number,        // Itens atualizados
  skipped: number,        // Itens pulados (ja existentes)
  failed: number,         // Itens que falharam
  errors: string[]        // Lista de erros
}

POST /api/admin/items/update-prices

Atualiza precos dos itens existentes usando a API Waxpeer.

Body - UpdatePricesDto:

typescript
{
  game?: 'csgo' | 'dota2' | 'tf2';  // Jogo (default: 'csgo')
  batchSize?: number;                 // Tamanho do batch (default: 100)
  updateAll?: boolean;                // Atualizar todos (default: false)
  specificItems?: string[];           // Market hash names especificos
  minPriceCents?: number;             // Preco minimo em cents
  maxPriceCents?: number;             // Preco maximo em cents
}

Response (200):

typescript
{
  success: true,
  source: "Waxpeer",
  totalChecked: number,
  updated: number,
  notFound: number,
  failed: number,
  totalValueCents: number,
  errors: string[]
}

Logica de selecao:

  • Se specificItems fornecido: atualiza apenas esses itens
  • Se updateAll = true: atualiza todos os itens do banco
  • Se nenhum filtro: atualiza apenas itens com valueCents <= 100 (sem preco)

GET /api/admin/items/items

Retorna itens brutos da API Waxpeer sem salvar no banco.

Query Parameters:

ParamTipoDefaultDescricao
gameenumcsgocsgo, dota2, tf2
skipnumber0Paginacao
limitnumber-Itens por pagina (max 500)
brandstring-Categoria (rifle, pistol, knife...)
sortenum-asc, desc, profit, best_deals
searchstring-Busca por nome
max_pricenumber-Preco maximo (em coins)
min_pricenumber-Preco minimo (em coins)

GET /api/admin/items/steam-database/items

Retorna itens brutos do Steam Database sem salvar no banco.

Query Parameters:

ParamTipoDescricao
searchstringBusca por nome (opcional)

Sem search: retorna os primeiros 100 itens do cache. Com search: busca por nome ou nome da skin.


GET /api/admin/items/steam-database/collections

Retorna todas as colecoes de armas CS2.

Response (200):

typescript
{
  collections: string[]  // Lista de nomes de colecoes ordenados
}

POST /api/admin/items/steam-database/clear-cache

Limpa o cache local do Steam Database para forcar nova busca.

Response (200):

typescript
{
  success: true,
  message: "Steam Database cache cleared"
}

GET /api/admin/items/weapon-categories

Retorna o mapeamento entre categorias Waxpeer e o enum WeaponCategory.

Response (200):

typescript
{
  mappings: [
    { waxpeerBrand: "pistol", weaponCategory: "PISTOL", description: "Pistols (Glock, USP, Deagle, etc.)" },
    { waxpeerBrand: "rifle", weaponCategory: "RIFLE", description: "Rifles (AK-47, M4A4, M4A1-S, etc.)" },
    { waxpeerBrand: "smg", weaponCategory: "SMG", description: "SMGs (MP9, MAC-10, UMP-45, etc.)" },
    { waxpeerBrand: "shotgun", weaponCategory: "SHOTGUN", description: "Shotguns (Nova, XM1014, Mag-7, etc.)" },
    { waxpeerBrand: "sniper_rifle", weaponCategory: "SNIPER", description: "Sniper rifles (AWP, Scout, etc.)" },
    { waxpeerBrand: "machinegun", weaponCategory: "HEAVY", description: "Machine guns (Negev, M249)" },
    { waxpeerBrand: "knife", weaponCategory: "KNIFE", description: "Knives" },
    { waxpeerBrand: "gloves", weaponCategory: "GLOVE", description: "Gloves" },
    { waxpeerBrand: "key", weaponCategory: "OTHER", description: "Keys" },
    { waxpeerBrand: "pass", weaponCategory: "OTHER", description: "Operation passes" },
    { waxpeerBrand: "sticker", weaponCategory: "OTHER", description: "Stickers" },
  ]
}

ItemsPublicController (Publico - 7 Endpoints)

Todos os endpoints sao marcados com @Public() (sem autenticacao), exceto POST /api/items/sync que requer AdminGuard.

Margem de Preco (PriceMarginService)

REGRA CRITICA: Todos os precos publicos incluem uma margem configuravel. A margem efetiva vem de PriceMarginService.getCurrent() (ajustavel pelo admin) — o valor no codigo (ex.: 1.2) e apenas o default. NUNCA assuma um percentual fixo.

typescript
private async applyMargin(priceCents: number): Promise<number> {
  const margin = await this.priceMarginService.getCurrent(); // ex.: 1.2 (default)
  return Math.round(priceCents * margin);
}

Exemplo (com margem corrente = 1.2):

  • Preco Waxpeer: $1.00 (100 cents)
  • Preco com margem: $1.20 (120 cents)
  • Preco em BRL (cotacao 5.25): R$ 6,30

GET /api/items

Retorna todos os itens com a margem corrente (PriceMarginService.getCurrent()) aplicada e conversao BRL.

Query Parameters:

ParamTipoDescricao
searchstringBusca por nome
categoryWeaponCategoryFiltrar por categoria
rarityRarityFiltrar por raridade
minPricenumberPreco minimo em cents (USD)
maxPricenumberPreco maximo em cents (USD)
limitnumberLimitar resultados

Response (200):

typescript
{
  success: true,
  count: number,
  exchangeRate: number,    // Cotacao USD->BRL atual
  marginPercent: 20,
  items: [
    {
      name: "AK-47 | Redline (Field-Tested)",
      price: 12000,           // Waxpeer coins com margem (1000 = $1)
      originalPriceCents: 100, // Preco original em cents (sem margem)
      priceCents: 120,        // Preco em cents com margem
      priceBrl: 6.30,         // Preco em Reais
      steamPrice: 10000,      // Preco Steam original
      imageUrl: "https://...",
      rarity: "CLASSIFIED",
      weaponCategory: "RIFLE",
      marginPercent: 20
    }
  ]
}

GET /api/items/categories

Retorna categorias disponiveis com contagem de itens.

Response (200):

typescript
{
  success: true,
  categories: [
    { name: "RIFLE", count: 450 },
    { name: "PISTOL", count: 320 },
    { name: "KNIFE", count: 180 },
    // ...
  ]
}

GET /api/items/rarities

Retorna raridades disponiveis com contagem de itens.

Response (200):

typescript
{
  success: true,
  rarities: [
    { name: "CONSUMER", count: 200 },
    { name: "MIL_SPEC", count: 800 },
    { name: "COVERT", count: 50 },
    // ...
  ]
}

GET /api/items/cheap

Retorna itens baratos ($0-$0.50) com a margem corrente aplicada.

Query Parameters:

ParamTipoDefaultDescricao
limitnumber100Limitar resultados

Itens sao filtrados por priceCents >= 0 && priceCents <= 50 (antes da margem) e ordenados por preco ascendente.


GET /api/items/stats

Retorna estatisticas do snapshot atual com a margem corrente aplicada.

Response (200):

typescript
{
  success: true,
  exchangeRate: {
    usdToBrl: 5.25,
    lastUpdated: "2025-06-15T14:00:00.000Z",
    source: "AwesomeAPI (economia.awesomeapi.com.br)"
  },
  marginPercent: 20,
  stats: {
    totalItems: 5432,
    totalValueUsd: "$1,234,567.89",
    totalValueBrl: "R$ 6,481,481.42",
    averagePriceUsd: "$227.28",
    averagePriceBrl: "R$ 1,193.22",
    cheapestItem: {
      name: "P250 | Sand Dune (Battle-Scarred)",
      priceUsd: "$0.04",
      priceBrl: "R$ 0.20"
    },
    mostExpensiveItem: {
      name: "AK-47 | Case Hardened (Factory New)",
      priceUsd: "$12,000.00",
      priceBrl: "R$ 63,000.00"
    }
  }
}

GET /api/items/exchange-rate

Retorna a cotacao atual USD->BRL.

Response (200):

typescript
{
  success: true,
  rate: 5.25,
  lastUpdated: "2025-06-15T14:00:00.000Z",
  source: "AwesomeAPI (economia.awesomeapi.com.br)"
}

POST /api/items/sync

Requer AdminGuard. Sincroniza itens do Waxpeer para o banco de dados.

Body - SyncItemsOptions:

typescript
{
  weaponCategories?: WeaponCategory[];  // Filtrar por categoria
  skipExisting?: boolean;               // Pular existentes (default: false)
  batchSize?: number;                   // Tamanho do batch (default: 100)
  maxItems?: number;                    // Limitar quantidade
}

Response (200):

typescript
{
  success: true,
  totalFetched: number,
  created: number,
  updated: number,
  skipped: number,
  failed: number,
  errors: string[]
}

SteamDatabaseService

Servico que busca e parseia os arquivos de jogo CS2 para extrair o catalogo completo de itens com raridades corretas.

Fontes de Dados

ArquivoURLConteudo
items_game.txthttps://raw.githubusercontent.com/SteamDatabase/GameTracking-CS2/master/game/csgo/pak01_dir/scripts/items/items_game.txtDefinicoes de itens, paint kits, colecoes
csgo_english.txthttps://raw.githubusercontent.com/SteamDatabase/GameTracking-CS2/master/game/csgo/pak01_dir/resource/csgo_english.txtTraducoes de nomes

Cache

typescript
private cache: Map<string, SteamDatabaseItem> | null = null;
private lastFetchTime: Date | null = null;
private readonly cacheTTL = 24 * 60 * 60 * 1000; // 24 horas

O cache dura 24 horas. Pode ser limpo manualmente via clearCache() ou via endpoint POST /api/admin/items/steam-database/clear-cache.

Parser VDF (Valve Data Format)

O arquivo items_game.txt usa o formato proprietario VDF da Valve. O parser extrai:

  1. Paint Kits: Skins disponiveis (ID, nome, raridade)
  2. Weapons: Armas base (defindex, nome, prefab)
  3. Collections: Colecoes que mapeiam armas + paint kits + raridades
  4. Rarities: Definicoes de raridade

Interface do Item

typescript
export interface SteamDatabaseItem {
  marketHashName: string;     // Nome completo (ex: "AK-47 | Redline")
  name: string;               // Mesmo que marketHashName
  weaponType: string;         // Tipo da arma
  skinName?: string;          // Nome da skin (ex: "Redline")
  rarity: Rarity;             // Raridade Prisma enum
  weaponCategory: WeaponCategory; // Categoria Prisma enum
  collection?: string;        // Colecao (ex: "Phoenix Collection")
  isStatTrak: boolean;        // Se e StatTrak
  isSouvenir: boolean;        // Se e Souvenir
  imageUrl?: string;          // URL da imagem (opcional)
}

Mapeamento de Categorias

typescript
private mapWeaponTypeToCategory(weaponType: string): WeaponCategory {
  const lower = weaponType.toLowerCase();
  if (lower.includes('pistol') || lower.includes('glock')...) return WeaponCategory.PISTOL;
  if (lower.includes('smg') || lower.includes('mac10')...) return WeaponCategory.SMG;
  if (lower.includes('rifle') || lower.includes('ak47')...) return WeaponCategory.RIFLE;
  if (lower.includes('shotgun') || lower.includes('nova')...) return WeaponCategory.SHOTGUN;
  if (lower.includes('sniper') || lower.includes('awp')...) return WeaponCategory.SNIPER;
  if (lower.includes('machinegun') || lower.includes('m249')...) return WeaponCategory.HEAVY;
  if (lower.includes('knife')) return WeaponCategory.KNIFE;
  if (lower.includes('glove')) return WeaponCategory.GLOVE;
  return WeaponCategory.OTHER;
}

Metodos Publicos

MetodoRetornoDescricao
fetchAllItems()Map<string, SteamDatabaseItem>Catalogo completo (com cache 24h)
getItem(name)SteamDatabaseItem | nullBusca por market hash name
searchItems(term)SteamDatabaseItem[]Busca por nome ou skin name
getItemsByCollection(name)SteamDatabaseItem[]Itens de uma colecao
clearCache()voidLimpa cache para forcar nova busca

WaxpeerSnapshotService

Servico que busca precos em tempo real da API Waxpeer.

Endpoint

GET https://api.waxpeer.com/v1/prices?api={API_KEY}&game=csgo

Cache

typescript
private cache: FilteredItem[] | null = null;
private cacheTime: Date | null = null;
private readonly cacheTTL = 5 * 60 * 1000; // 5 minutos

Cache de 5 minutos. Se a API falhar e houver cache expirado, o cache expirado e retornado como fallback.

Conversao de Precos

A API Waxpeer usa "coins" onde 1000 coins = $1.00 USD:

typescript
private mapToFilteredItem(item: WaxpeerPriceItem): FilteredItem {
  return {
    name: item.name,
    price: item.min,                          // coins (ex: 1000)
    priceCents: Math.round(item.min / 10),    // cents USD (ex: 100)
    steamPrice: item.min,
    imageUrl: item.img || WaxpeerSnapshotService.buildWaxpeerImageUrl(item.name),
    rarity: this.inferRarity(item.rarity_color, item.name),
    weaponCategory: this.inferWeaponCategory(item.name),
  };
}

Inferencia de Raridade por Cor Hex

typescript
private inferRarity(rarityColor?: string, name?: string): Rarity {
  const colorMap: Record<string, Rarity> = {
    '#b0c3d9': Rarity.CONSUMER,        // Cinza
    '#5e98d9': Rarity.INDUSTRIAL,      // Azul claro
    '#4b69ff': Rarity.MIL_SPEC,        // Azul
    '#8847ff': Rarity.RESTRICTED,      // Roxo
    '#d32ce6': Rarity.CLASSIFIED,      // Rosa
    '#eb4b4b': Rarity.COVERT,          // Vermelho
    '#ffd700': Rarity.EXTRAORDINARY,   // Dourado
    '#f8ae2a': Rarity.EXTRAORDINARY,   // Dourado variante
    '#e4ae39': Rarity.EXTRAORDINARY,   // Dourado variante
  };

  const mapped = colorMap[rarityColor?.toLowerCase()];
  if (mapped) return mapped;

  // Fallback por nome
  if (name?.toLowerCase().includes('knife') ||
      name?.toLowerCase().includes('karambit') ||
      name?.toLowerCase().includes('bayonet')) {
    return Rarity.EXTRAORDINARY;
  }
  if (name?.toLowerCase().includes('gloves')) return Rarity.EXTRAORDINARY;

  return Rarity.CONSUMER;
}

Metodos Publicos

MetodoRetornoDescricao
getFilteredItems()FilteredItem[]Todos os itens (cache 5min)
getItemsByCategory(cat)FilteredItem[]Filtro por categoria
getItemsByRarity(rar)FilteredItem[]Filtro por raridade
getItemsByPriceRange(min, max)FilteredItem[]Filtro por faixa de preco
searchItems(query)FilteredItem[]Busca por nome
clearCache()voidLimpa cache

ExchangeRateService

Servico que busca a cotacao USD->BRL em tempo real.

Fonte de Dados

GET https://economia.awesomeapi.com.br/json/last/USD-BRL

Cache

typescript
private cachedRate: number | null = null;
private cacheTime: Date | null = null;
private readonly cacheTTL = 30 * 60 * 1000; // 30 minutos

Fallback

Se a API falhar:

  1. Retorna cache expirado se disponivel
  2. Se nao houver cache, usa fallback fixo de 5.00
typescript
async getUsdToBrlRate(): Promise<number> {
  try {
    const response = await firstValueFrom(
      this.httpService.get<ExchangeRateResponse>(this.apiUrl, { timeout: 10000 })
    );
    const rate = parseFloat(response.data.USDBRL.bid);
    if (isNaN(rate) || rate <= 0) throw new Error('Cotacao invalida');

    this.cachedRate = rate;
    this.cacheTime = new Date();
    return rate;
  } catch (error) {
    if (this.cachedRate) return this.cachedRate; // cache expirado
    return 5.00; // fallback
  }
}

Metodos Publicos

MetodoRetornoDescricao
getUsdToBrlRate()numberCotacao atual (cache 30min)
convertCentsToBrl(cents)numberConverte cents USD para BRL
convertUsdToBrl(usd)numberConverte dolares para BRL
getExchangeRateInfo(){rate, lastUpdated, source}Info completa
clearCache()voidLimpa cache

ItemNameParserService

Servico que parseia nomes de itens CS2 no formato market hash name.

Formatos Suportados

"StatTrak(TM) M4A4 | Poly Mag (Factory New)"
"Souvenir M4A4 | Poly Mag (Field-Tested)"
"M4A4 | Poly Mag (Minimal Wear)"
"AK-47 | Redline"
"Glock-18"

Interface de Retorno

typescript
export interface ParsedItemName {
  fullName: string;        // Nome completo original
  weaponName: string;      // Nome da arma (ex: "M4A4")
  skinName: string;        // Nome da skin (ex: "Poly Mag")
  exterior?: ItemExterior; // Exterior enum (FACTORY_NEW, etc.)
  isStatTrak: boolean;     // Se e StatTrak
  isSouvenir: boolean;     // Se e Souvenir
}

Logica de Parsing

typescript
parse(marketHashName: string): ParsedItemName {
  let workingName = marketHashName;
  let isStatTrak = false;
  let isSouvenir = false;

  // 1. Verificar e remover prefixos
  if (workingName.includes('StatTrak(TM)')) {
    isStatTrak = true;
    workingName = workingName.replace('StatTrak(TM) ', '');
  }
  if (workingName.includes('Souvenir')) {
    isSouvenir = true;
    workingName = workingName.replace('Souvenir ', '');
  }

  // 2. Extrair exterior dos parenteses
  const exteriorMatch = workingName.match(/\s*\(([^)]+)\)\s*$/);
  // "Factory New" -> FACTORY_NEW
  // "Minimal Wear" -> MINIMAL_WEAR
  // "Field-Tested" -> FIELD_TESTED
  // "Well-Worn" -> WELL_WORN
  // "Battle-Scarred" -> BATTLE_SCARRED

  // 3. Separar arma e skin pelo "|"
  const parts = workingName.split('|').map(p => p.trim());
  // parts[0] = "M4A4", parts[1] = "Poly Mag"
}

Metodos Publicos

MetodoRetornoDescricao
parse(name)ParsedItemNameParse completo
extractSkinName(name)stringApenas o nome da skin
isDefaultItem(name)booleanSe e item sem skin

SyncSteamDatabaseUseCase

Use case que coordena a importacao de itens do Steam Database para o banco.

Fluxo

typescript
async execute(options: SyncSteamDatabaseOptions): Promise<SyncSteamDatabaseResult> {
  // 1. Limpar cache se forceRefresh
  if (options.forceRefresh) this.steamDatabaseService.clearCache();

  // 2. Buscar todos os itens do Steam Database
  const steamItems = await this.steamDatabaseService.fetchAllItems();

  // 3. Filtrar por categoria e default weapons
  let itemsToProcess = Array.from(steamItems.values());
  if (options.weaponCategories) {
    itemsToProcess = itemsToProcess.filter(i => options.weaponCategories.includes(i.weaponCategory));
  }
  if (!options.includeDefaultWeapons) {
    itemsToProcess = itemsToProcess.filter(i => i.skinName);
  }

  // 4. Buscar itens existentes no banco
  const existingItems = await this.itemRepository.findAll();
  const existingByMarketHash = new Map(existingItems.map(i => [i.marketHashName, i]));

  // 5. Processar em batches
  for (const batch of batches) {
    await this.processBatch(batch, existingByMarketHash, options.updateExisting, result);
    await this.sleep(50); // evitar sobrecarga
  }
}

Itens criados sem preco: valueCents = BigInt(0). O preco e preenchido depois pelo UpdateItemPricesUseCase ou SyncItemsFromWaxpeerUseCase.


UpdateItemPricesUseCase

Use case que atualiza precos dos itens existentes a partir da API Waxpeer.

Conversao de Preco

typescript
// Waxpeer usa coins: 1000 coins = $1.00
// Convertemos para cents: 1000 / 10 = 100 cents
const priceCents = Math.round(waxpeerItem.price / 10);

Fluxo

typescript
async execute(options: UpdateItemPricesOptions): Promise<UpdateItemPricesResult> {
  // 1. Selecionar itens para atualizar
  // 2. Buscar precos da Waxpeer
  const waxpeerItems = await this.waxpeerService.getAllItems(game);
  const waxpeerByName = new Map(waxpeerItems.map(i => [i.name, i]));

  // 3. Para cada item, buscar preco e atualizar
  for (const item of itemsToUpdate) {
    const waxpeerItem = waxpeerByName.get(item.marketHashName);
    if (!waxpeerItem) { result.notFound++; continue; }

    const priceCents = Math.round(waxpeerItem.price / 10);

    await this.itemRepository.update(item.id, {
      valueCents: BigInt(priceCents),
      imageUrl: waxpeerItem.image,
    });
  }
}

SyncItemsFromWaxpeerUseCase

Use case completo que busca itens do Waxpeer, parseia nomes, converte precos com a margem corrente e salva no banco com precos em USD e BRL.

Margem de preco (dinamica)

A margem vem de PriceMarginService.getCurrent() (ajustavel pelo admin), nao de uma constante fixa. O exemplo abaixo assume a margem corrente = 1.2:

typescript
const priceMargin = await this.priceMarginService.getCurrent(); // ex.: 1.2

// Preco original: 100 cents USD
const priceCentsWithMargin = Math.round(100 * priceMargin); // 120 cents USD

// Conversao para BRL (cotacao 5.25):
const priceUsd = 120 / 100; // $1.20
const priceBrl = 1.20 * 5.25; // R$ 6.30
const priceCentsBrl = Math.round(6.30 * 100); // 630 centavos BRL

Fluxo

typescript
async execute(options: SyncItemsOptions): Promise<SyncItemsResult> {
  // 1. Buscar itens do Waxpeer (ja filtrados)
  let items = await this.waxpeerSnapshotService.getFilteredItems();
  items = items.filter(i => i.weaponCategory !== WeaponCategory.OTHER);

  // 2. Buscar cotacao USD->BRL e margem corrente
  const exchangeRate = await this.exchangeRateService.getUsdToBrlRate();
  const priceMargin = await this.priceMarginService.getCurrent();

  // 3. Buscar itens existentes
  const existingByMarketHash = new Map(...);

  // 4. Para cada item:
  for (const waxpeerItem of items) {
    const parsed = this.itemNameParser.parse(waxpeerItem.name);
    if (!parsed.skinName) { result.skipped++; continue; } // pula default

    const priceCentsWithMargin = Math.round(waxpeerItem.priceCents * priceMargin);
    const priceUsd = priceCentsWithMargin / 100;
    const priceBrl = priceUsd * exchangeRate;
    const priceCentsBrl = Math.round(priceBrl * 100);

    if (existingItem) {
      await this.itemRepository.update(existingItem.id, {
        valueCents: BigInt(priceCentsWithMargin),
        valueCentsBrl: BigInt(priceCentsBrl),
        imageUrl: waxpeerItem.imageUrl,
      });
    } else {
      await this.itemRepository.create({
        name: parsed.skinName,
        marketHashName: waxpeerItem.name,
        imageUrl: waxpeerItem.imageUrl,
        rarity: waxpeerItem.rarity,
        valueCents: BigInt(priceCentsWithMargin),
        valueCentsBrl: BigInt(priceCentsBrl),
        weaponCategory: waxpeerItem.weaponCategory,
        exterior: parsed.exterior,
        isStatTrak: parsed.isStatTrak,
      });
    }
  }
}

Fluxo de Importacao Recomendado

Importacao Inicial

1. POST /api/admin/items/sync-steam-database
   - Importa catalogo completo com raridades corretas
   - Itens criados com valueCents = 0 (sem preco)

2. POST /api/admin/items/update-prices { updateAll: true }
   - Atualiza valueCents (USD) de todos os itens via Waxpeer

3. POST /api/items/sync
   - Sincroniza com a margem corrente (PriceMarginService) e calcula valueCentsBrl

Atualizacao de Precos (Periodica)

1. POST /api/admin/items/update-prices { updateAll: true }
   - Atualiza precos USD

2. POST /api/items/sync { skipExisting: false }
   - Recalcula BRL com cotacao atual e margem

Debugging

Verificar precos de um item

sql
SELECT name, market_hash_name, value_cents, value_cents_brl, rarity
FROM items
WHERE market_hash_name = 'AK-47 | Redline (Field-Tested)';

Verificar cotacao atual

bash
curl -s https://economia.awesomeapi.com.br/json/last/USD-BRL | jq '.USDBRL.bid'

Problemas Comuns

ProblemaCausaSolucao
WAXPEER_API_KEY nao configuradaVariavel de ambiente ausenteSetar WAXPEER_API_KEY no .env
Precos zeradosSteam Database nao traz precosExecutar update-prices apos sync
Raridade CONSUMER em tudoCor hex nao mapeadaVerificar colorMap no WaxpeerSnapshotService
Cotacao fixa em 5.00API AwesomeAPI indisponivelVerificar conectividade, cache expirado
Itens duplicadosmarketHashName diferenteVerificar encoding de caracteres especiais
Cache nao limpaTTL nao expiradoUsar clear-cache endpoint ou clearCache()

Regras Importantes

  1. A margem corrente e aplicada em TODOS os precos publicos - vem de PriceMarginService.getCurrent() (configuravel pelo admin); a constante no codigo e so o default
  2. valueCents e USD, valueCentsBrl e BRL - nunca confundir
  3. Steam Database e a fonte de raridades - Waxpeer usa cores aproximadas
  4. Cache de precos Waxpeer dura 5 minutos - nao bombardear a API
  5. Cache de cotacao dura 30 minutos - com fallback para 5.00
  6. Cache do Steam Database dura 24 horas - pode ser limpado manualmente
  7. Itens OTHER sao excluidos do sync (stickers, keys, passes)
  8. Itens default (sem skin) sao excluidos do sync por padrao
  9. Endpoints admin requerem AdminGuard - endpoints publicos sao @Public()
  10. Sleep de 50ms entre batches para evitar sobrecarga no banco

Documentação Técnica - ProCases