Skip to content

Sistema de Transações

O sistema de transações implementa double-entry bookkeeping para garantir integridade financeira absoluta.

Estrutura da Transação

prisma
model Transaction {
  id                    BigInt            @id
  userId                BigInt            @map("user_id")
  type                  TransactionType   // DEBIT ou CREDIT
  amountCents           BigInt            @map("amount_cents")
  reason                TransactionReason
  status                TransactionStatus @default(COMPLETED)
  relatedTransactionId  BigInt?           @map("related_transaction_id")
  metadata              Json?
  createdAt             DateTime          @default(now())

  user                  User              @relation(fields: [userId])
  relatedTransaction    Transaction?      @relation("RelatedTransactions")
}

Tipos de Transação

typescript
enum TransactionType {
  DEBIT   // Saída de dinheiro (valor negativo)
  CREDIT  // Entrada de dinheiro (valor positivo)
}

Razões de Transação

typescript
enum TransactionReason {
  // Pagamentos
  DEPOSIT              // Depósito
  WITHDRAWAL           // Saque

  // Caixas
  CASE_OPENING         // Custo de abertura
  CASE_WIN             // Valor do item ganho

  // Batalhas
  BATTLE_ENTRY         // Entrada em batalha
  BATTLE_WIN           // Prêmio de batalha

  // Upgrades
  UPGRADE_COST         // Custo do upgrade
  UPGRADE_WIN          // Item ganho no upgrade

  // Swaps
  SWAP_OUT             // Itens enviados no swap
  SWAP_IN              // Itens recebidos no swap

  // Sorteios
  RAFFLE_TICKET        // Compra de ticket
  RAFFLE_WIN           // Prêmio de sorteio

  // Inventário
  ITEM_SOLD            // Venda de item

  // Admin
  ADMIN_ADJUSTMENT     // Ajuste manual do admin
  REFERRAL_BONUS       // Bônus de indicação
}

Status da Transação

typescript
enum TransactionStatus {
  PENDING    // Aguardando processamento
  COMPLETED  // Concluída
  FAILED     // Falhou
  CANCELLED  // Cancelada
}

Double-Entry Bookkeeping

Princípio

Toda movimentação financeira cria duas transações pareadas:

Depósito R$ 100:
┌──────────────────────────────────────────────┐
│ ID: 1 | User: GATEWAY | Type: DEBIT  | -100 │
│ ID: 2 | User: 123     | Type: CREDIT | +100 │
│ Related: 1 ↔ 2                               │
└──────────────────────────────────────────────┘

Soma total: -100 + 100 = 0 ✓

Implementação

typescript
// src/application/services/transaction.service.ts

async transfer(
  fromUserId: bigint,
  toUserId: bigint,
  amount: bigint,
  reason: TransactionReason,
): Promise<{ debit: Transaction; credit: Transaction }> {
  return this.prisma.$transaction(async (tx) => {
    // Cria DEBIT (saída)
    const debit = await tx.transaction.create({
      data: {
        userId: fromUserId,
        type: 'DEBIT',
        amountCents: -amount,
        reason,
        status: 'COMPLETED',
      },
    });

    // Cria CREDIT (entrada)
    const credit = await tx.transaction.create({
      data: {
        userId: toUserId,
        type: 'CREDIT',
        amountCents: amount,
        reason,
        status: 'COMPLETED',
        relatedTransactionId: debit.id,
      },
    });

    // Liga DEBIT ao CREDIT
    await tx.transaction.update({
      where: { id: debit.id },
      data: { relatedTransactionId: credit.id },
    });

    return { debit, credit };
  });
}

Contas Especiais

O sistema usa contas virtuais para controle:

ContaIDPropósito
HOUSE0Casa - lucros e perdas do site
GATEWAY-1Gateway de pagamento externo
POOL-2Pool de itens para settlement
typescript
export const HOUSE_ACCOUNT_ID = 0n;
export const GATEWAY_ACCOUNT_ID = -1n;
export const POOL_ACCOUNT_ID = -2n;

Fluxos de Transação

Depósito

typescript
async processDeposit(userId: bigint, amount: bigint) {
  // Gateway → Usuário
  await this.transfer(
    GATEWAY_ACCOUNT_ID,
    userId,
    amount,
    TransactionReason.DEPOSIT,
  );
}

Abertura de Caixa

typescript
async processCaseOpening(userId: bigint, cost: bigint, winValue: bigint) {
  // 1. Usuário paga a caixa → House
  await this.transfer(
    userId,
    HOUSE_ACCOUNT_ID,
    cost,
    TransactionReason.CASE_OPENING,
  );

  // 2. House paga o prêmio → Usuário
  await this.transfer(
    HOUSE_ACCOUNT_ID,
    userId,
    winValue,
    TransactionReason.CASE_WIN,
  );
}

Batalha

typescript
async processBattle(
  participants: bigint[],
  entryFee: bigint,
  winners: bigint[],
  totalPrize: bigint,
) {
  // 1. Todos pagam entrada
  for (const participantId of participants) {
    await this.transfer(
      participantId,
      HOUSE_ACCOUNT_ID,
      entryFee,
      TransactionReason.BATTLE_ENTRY,
    );
  }

  // 2. Vencedores recebem prêmio
  const prizePerWinner = totalPrize / BigInt(winners.length);
  for (const winnerId of winners) {
    await this.transfer(
      HOUSE_ACCOUNT_ID,
      winnerId,
      prizePerWinner,
      TransactionReason.BATTLE_WIN,
    );
  }
}

Saque

typescript
async processWithdrawal(userId: bigint, amount: bigint) {
  // Usuário → Gateway
  await this.transfer(
    userId,
    GATEWAY_ACCOUNT_ID,
    amount,
    TransactionReason.WITHDRAWAL,
  );
}

Histórico de Transações

API Endpoint

typescript
@Get('transactions')
async getTransactions(
  @CurrentUser() user: User,
  @Query('page') page: number = 1,
  @Query('limit') limit: number = 20,
  @Query('type') type?: TransactionType,
  @Query('reason') reason?: TransactionReason,
) {
  return this.transactionRepository.findByUser(user.id, {
    page,
    limit,
    type,
    reason,
  });
}

Response

json
{
  "transactions": [
    {
      "id": "123456789",
      "type": "CREDIT",
      "amountCents": "10000",
      "amountFormatted": "R$ 100,00",
      "reason": "DEPOSIT",
      "status": "COMPLETED",
      "createdAt": "2024-01-15T14:30:00Z"
    },
    {
      "id": "123456790",
      "type": "DEBIT",
      "amountCents": "-2500",
      "amountFormatted": "-R$ 25,00",
      "reason": "CASE_OPENING",
      "status": "COMPLETED",
      "createdAt": "2024-01-15T14:35:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 150,
    "totalPages": 8
  }
}

Verificação de Integridade

Soma Total = Zero

sql
-- Se double-entry está correto, soma deve ser 0
SELECT SUM(amount_cents) as net_balance
FROM transactions
WHERE status = 'COMPLETED';

-- Resultado esperado: 0

Pares Correspondem

sql
-- Verifica que todos os pares batem
SELECT t1.id, t1.amount_cents, t2.amount_cents
FROM transactions t1
JOIN transactions t2 ON t1.related_transaction_id = t2.id
WHERE t1.amount_cents + t2.amount_cents != 0;

-- Resultado esperado: 0 rows

Audit Script

typescript
async auditTransactions(): Promise<AuditResult> {
  // 1. Verifica soma total
  const totalSum = await this.prisma.$queryRaw<{ sum: bigint }[]>`
    SELECT COALESCE(SUM(amount_cents), 0) as sum
    FROM transactions
    WHERE status = 'COMPLETED'
  `;

  if (totalSum[0].sum !== 0n) {
    return { valid: false, error: 'Total sum is not zero' };
  }

  // 2. Verifica pares
  const mismatched = await this.prisma.$queryRaw`
    SELECT COUNT(*) as count
    FROM transactions t1
    JOIN transactions t2 ON t1.related_transaction_id = t2.id
    WHERE t1.amount_cents + t2.amount_cents != 0
  `;

  if (mismatched[0].count > 0) {
    return { valid: false, error: 'Mismatched pairs found' };
  }

  return { valid: true };
}

Relatórios

Resumo Diário

typescript
async getDailyReport(date: Date) {
  const startOfDay = new Date(date.setHours(0, 0, 0, 0));
  const endOfDay = new Date(date.setHours(23, 59, 59, 999));

  return this.prisma.transaction.groupBy({
    by: ['reason'],
    where: {
      createdAt: { gte: startOfDay, lte: endOfDay },
      status: 'COMPLETED',
    },
    _sum: { amountCents: true },
    _count: true,
  });
}

Resultado

json
[
  { "reason": "DEPOSIT", "total": 5000000, "count": 500 },
  { "reason": "WITHDRAWAL", "total": -3000000, "count": 100 },
  { "reason": "CASE_OPENING", "total": -2500000, "count": 10000 },
  { "reason": "CASE_WIN", "total": 2300000, "count": 10000 },
  { "reason": "BATTLE_ENTRY", "total": -500000, "count": 200 },
  { "reason": "BATTLE_WIN", "total": 480000, "count": 100 }
]

Arquivos Fonte

Principais Arquivos

  • src/application/services/transaction.service.ts - Serviço principal
  • src/infrastructure/database/repositories/transaction.repository.ts - Repository
  • prisma/schema.prisma - Model Transaction

Documentação Técnica CSGOFlip