Skip to content

Reconciliação Financeira

O sistema de reconciliação garante a integridade dos dados financeiros, detectando e corrigindo inconsistências entre diferentes fontes de dados.

Visão Geral

Tipos de Reconciliação

1. Reconciliação de Saldo

Compara o saldo cacheado (user.balanceCents) com o saldo real calculado das transações.

typescript
// balance-reconciliation.service.ts
async reconcileUserBalance(userId: bigint): Promise<ReconciliationResult> {
  // 1. Buscar saldo do cache
  const user = await this.userRepository.findById(userId);
  const cachedBalance = user.balanceCents;
  
  // 2. Calcular saldo real das transações (Source of Truth)
  const realBalance = await this.transactionRepository.getUserBalance(userId);
  
  // 3. Comparar
  const difference = realBalance - cachedBalance;
  
  if (difference !== 0n) {
    return {
      status: 'DISCREPANCY_FOUND',
      userId,
      cachedBalance,
      realBalance,
      difference,
      action: 'UPDATE_CACHE',
    };
  }
  
  return {
    status: 'OK',
    userId,
    cachedBalance,
    realBalance,
    difference: 0n,
  };
}

async reconcileAllUsers(): Promise<BatchReconciliationResult> {
  const results: ReconciliationResult[] = [];
  const discrepancies: ReconciliationResult[] = [];
  
  // Processar em batches para não sobrecarregar
  const batchSize = 100;
  let offset = 0;
  
  while (true) {
    const users = await this.userRepository.findMany({
      skip: offset,
      take: batchSize,
      select: { id: true },
    });
    
    if (users.length === 0) break;
    
    // Processar batch em paralelo
    const batchResults = await Promise.all(
      users.map((user) => this.reconcileUserBalance(user.id)),
    );
    
    results.push(...batchResults);
    discrepancies.push(
      ...batchResults.filter((r) => r.status === 'DISCREPANCY_FOUND'),
    );
    
    offset += batchSize;
  }
  
  return {
    totalUsers: results.length,
    discrepanciesFound: discrepancies.length,
    discrepancies,
    executedAt: new Date(),
  };
}

2. Reconciliação de Double-Entry

Verifica se todas as transações seguem o padrão de partida dobrada (débito = crédito).

typescript
// double-entry-reconciliation.service.ts
async verifyDoubleEntry(): Promise<DoubleEntryResult> {
  // 1. Verificar se todo débito tem crédito correspondente
  const orphanDebits = await this.prisma.$queryRaw<Transaction[]>`
    SELECT t1.*
    FROM "Transaction" t1
    WHERE t1.type IN ('DEBIT', 'WITHDRAWAL_PENDING')
    AND NOT EXISTS (
      SELECT 1 FROM "Transaction" t2
      WHERE t2."referenceId" = t1."referenceId"
      AND t2."referenceType" = t1."referenceType"
      AND t2.type IN ('CREDIT', 'WITHDRAWAL_CREDIT')
      AND t2."amountCents" = t1."amountCents"
    )
  `;
  
  // 2. Verificar soma total (deve ser zero)
  const totalSum = await this.prisma.transaction.aggregate({
    _sum: {
      amountCents: true,
    },
  });
  
  // Em double-entry, soma de todos os movimentos deve ser 0
  // (débitos são negativos, créditos são positivos)
  const isBalanced = totalSum._sum.amountCents === 0n;
  
  return {
    status: isBalanced && orphanDebits.length === 0 ? 'OK' : 'ERROR',
    isBalanced,
    totalSum: totalSum._sum.amountCents,
    orphanTransactions: orphanDebits.length,
    orphanDetails: orphanDebits,
  };
}

3. Reconciliação com Provedores de Pagamento

Compara transações internas com dados dos provedores externos.

typescript
// payment-reconciliation.service.ts
async reconcileWithProvider(
  startDate: Date,
  endDate: Date,
): Promise<PaymentReconciliationResult> {
  // 1. Buscar depósitos internos no período
  const internalDeposits = await this.depositRepository.findByPeriod(
    startDate,
    endDate,
  );
  
  // 2. Buscar transações do provedor
  const providerTransactions = await this.paymentProvider.getTransactions(
    startDate,
    endDate,
  );
  
  const discrepancies: PaymentDiscrepancy[] = [];
  
  // 3. Verificar cada depósito interno
  for (const deposit of internalDeposits) {
    const providerTx = providerTransactions.find(
      (tx) => tx.id === deposit.externalTransactionId,
    );
    
    if (!providerTx) {
      discrepancies.push({
        type: 'MISSING_IN_PROVIDER',
        depositId: deposit.id,
        internalStatus: deposit.status,
        amountCents: deposit.amountCents,
      });
      continue;
    }
    
    // Verificar valor
    if (providerTx.amountCents !== deposit.amountCents) {
      discrepancies.push({
        type: 'AMOUNT_MISMATCH',
        depositId: deposit.id,
        internalAmount: deposit.amountCents,
        providerAmount: providerTx.amountCents,
      });
    }
    
    // Verificar status
    if (this.mapProviderStatus(providerTx.status) !== deposit.status) {
      discrepancies.push({
        type: 'STATUS_MISMATCH',
        depositId: deposit.id,
        internalStatus: deposit.status,
        providerStatus: providerTx.status,
      });
    }
  }
  
  // 4. Verificar transações do provedor não encontradas internamente
  for (const providerTx of providerTransactions) {
    const internalDeposit = internalDeposits.find(
      (d) => d.externalTransactionId === providerTx.id,
    );
    
    if (!internalDeposit && providerTx.status === 'COMPLETED') {
      discrepancies.push({
        type: 'MISSING_INTERNALLY',
        externalId: providerTx.id,
        providerStatus: providerTx.status,
        amountCents: providerTx.amountCents,
      });
    }
  }
  
  return {
    period: { startDate, endDate },
    internalCount: internalDeposits.length,
    providerCount: providerTransactions.length,
    discrepancies,
    matchedCount: internalDeposits.length - discrepancies.length,
  };
}

Auto-Correção

Correção de Cache de Saldo

typescript
// auto-fix.service.ts
async fixBalanceCache(userId: bigint): Promise<FixResult> {
  const lockKey = `reconciliation:fix:${userId}`;
  
  return await this.redlock.using([lockKey], 30000, async () => {
    // 1. Calcular saldo real
    const realBalance = await this.transactionRepository.getUserBalance(userId);
    
    // 2. Atualizar cache
    await this.prisma.user.update({
      where: { id: userId },
      data: { balanceCents: realBalance },
    });
    
    // 3. Notificar via WebSocket
    this.webSocketGateway.emitBalanceUpdate(userId, realBalance);
    
    // 4. Registrar correção
    await this.auditService.log({
      action: 'BALANCE_CACHE_FIXED',
      resourceId: userId,
      resourceType: 'USER',
      metadata: {
        previousCache: await this.getPreviousCacheValue(userId),
        newValue: realBalance,
        fixedBy: 'RECONCILIATION_SYSTEM',
      },
    });
    
    return {
      status: 'FIXED',
      userId,
      newBalance: realBalance,
    };
  });
}

async fixAllDiscrepancies(
  discrepancies: ReconciliationResult[],
): Promise<BatchFixResult> {
  const fixes: FixResult[] = [];
  const errors: FixError[] = [];
  
  for (const discrepancy of discrepancies) {
    try {
      const fix = await this.fixBalanceCache(discrepancy.userId);
      fixes.push(fix);
    } catch (error) {
      errors.push({
        userId: discrepancy.userId,
        error: error.message,
      });
    }
  }
  
  return {
    totalProcessed: discrepancies.length,
    fixed: fixes.length,
    errors: errors.length,
    errorDetails: errors,
  };
}

Agendamento

Cron Jobs

typescript
// reconciliation.scheduler.ts
@Injectable()
export class ReconciliationScheduler {
  constructor(
    private reconciliationService: ReconciliationService,
    private alertService: AlertService,
  ) {}
  
  // Reconciliação de saldo a cada hora
  @Cron('0 * * * *')
  async hourlyBalanceReconciliation() {
    const result = await this.reconciliationService.reconcileAllUsers();
    
    if (result.discrepanciesFound > 0) {
      // Corrigir automaticamente
      await this.reconciliationService.fixAllDiscrepancies(result.discrepancies);
      
      // Alertar se muitas discrepâncias
      if (result.discrepanciesFound > 10) {
        await this.alertService.sendAlert({
          level: 'WARNING',
          title: 'Múltiplas discrepâncias de saldo detectadas',
          message: `${result.discrepanciesFound} usuários com saldo inconsistente`,
          metadata: result,
        });
      }
    }
  }
  
  // Reconciliação double-entry diária
  @Cron('0 3 * * *')
  async dailyDoubleEntryCheck() {
    const result = await this.reconciliationService.verifyDoubleEntry();
    
    if (result.status === 'ERROR') {
      await this.alertService.sendCriticalAlert({
        title: 'CRÍTICO: Falha na verificação Double-Entry',
        message: 'Sistema de transações pode estar corrompido',
        metadata: result,
      });
    }
  }
  
  // Reconciliação com provedor de pagamento diária
  @Cron('0 4 * * *')
  async dailyPaymentReconciliation() {
    const yesterday = subDays(new Date(), 1);
    const today = new Date();
    
    const result = await this.reconciliationService.reconcileWithProvider(
      startOfDay(yesterday),
      endOfDay(yesterday),
    );
    
    if (result.discrepancies.length > 0) {
      await this.alertService.sendAlert({
        level: 'WARNING',
        title: 'Discrepâncias com provedor de pagamento',
        message: `${result.discrepancies.length} transações inconsistentes`,
        metadata: result,
      });
    }
  }
}

Relatórios

Dashboard de Reconciliação

typescript
// reconciliation-report.service.ts
async generateDailyReport(): Promise<DailyReconciliationReport> {
  const today = new Date();
  
  // 1. Estatísticas de saldo
  const balanceStats = await this.getBalanceStats();
  
  // 2. Estatísticas de transações
  const transactionStats = await this.getTransactionStats(today);
  
  // 3. Discrepâncias encontradas
  const discrepancies = await this.getDiscrepanciesHistory(today);
  
  // 4. Correções aplicadas
  const fixes = await this.getFixesHistory(today);
  
  return {
    date: today,
    summary: {
      totalUsersChecked: balanceStats.totalUsers,
      discrepanciesFound: discrepancies.total,
      discrepanciesFixed: fixes.total,
      pendingIssues: discrepancies.total - fixes.total,
    },
    balanceStats: {
      totalSystemBalance: balanceStats.totalBalance,
      totalCachedBalance: balanceStats.totalCached,
      difference: balanceStats.difference,
    },
    transactionStats: {
      totalTransactions: transactionStats.count,
      totalVolume: transactionStats.volume,
      averageTransaction: transactionStats.average,
    },
    discrepancies: discrepancies.details,
    fixes: fixes.details,
    health: this.calculateSystemHealth(balanceStats, discrepancies),
  };
}

calculateSystemHealth(
  balanceStats: BalanceStats,
  discrepancies: DiscrepancyStats,
): SystemHealth {
  let score = 100;
  const issues: string[] = [];
  
  // Penalizar por discrepâncias
  if (discrepancies.total > 0) {
    score -= Math.min(discrepancies.total * 2, 30);
    issues.push(`${discrepancies.total} discrepâncias de saldo`);
  }
  
  // Penalizar por diferença de cache
  if (balanceStats.difference !== 0n) {
    score -= 20;
    issues.push('Cache de saldo global inconsistente');
  }
  
  // Penalizar por transações órfãs
  if (discrepancies.orphanTransactions > 0) {
    score -= 30;
    issues.push(`${discrepancies.orphanTransactions} transações órfãs`);
  }
  
  return {
    score,
    status: score >= 90 ? 'HEALTHY' : score >= 70 ? 'WARNING' : 'CRITICAL',
    issues,
  };
}

Exportação de Relatórios

typescript
// report-export.service.ts
async exportReconciliationReport(
  startDate: Date,
  endDate: Date,
  format: 'CSV' | 'JSON' | 'PDF',
): Promise<ExportResult> {
  const data = await this.generatePeriodReport(startDate, endDate);
  
  switch (format) {
    case 'CSV':
      return this.exportToCsv(data);
    case 'JSON':
      return this.exportToJson(data);
    case 'PDF':
      return this.exportToPdf(data);
  }
}

Alertas e Notificações

Níveis de Alerta

typescript
enum AlertLevel {
  INFO = 'INFO',           // Informativo
  WARNING = 'WARNING',     // Atenção necessária
  ERROR = 'ERROR',         // Erro que precisa correção
  CRITICAL = 'CRITICAL',   // Crítico - ação imediata
}

interface ReconciliationAlert {
  level: AlertLevel;
  code: string;
  title: string;
  message: string;
  metadata: Record<string, any>;
  timestamp: Date;
  acknowledged: boolean;
  acknowledgedBy?: bigint;
  acknowledgedAt?: Date;
}

Regras de Alerta

typescript
// alert-rules.config.ts
export const RECONCILIATION_ALERT_RULES = {
  // Discrepâncias de saldo
  BALANCE_DISCREPANCY: {
    threshold: 1,           // 1 usuário
    level: AlertLevel.INFO,
  },
  BALANCE_DISCREPANCY_MULTIPLE: {
    threshold: 10,          // 10+ usuários
    level: AlertLevel.WARNING,
  },
  BALANCE_DISCREPANCY_MASS: {
    threshold: 100,         // 100+ usuários
    level: AlertLevel.CRITICAL,
  },
  
  // Double-entry
  DOUBLE_ENTRY_ORPHAN: {
    threshold: 1,
    level: AlertLevel.ERROR,
  },
  DOUBLE_ENTRY_UNBALANCED: {
    threshold: 1,
    level: AlertLevel.CRITICAL,
  },
  
  // Provedores de pagamento
  PAYMENT_DISCREPANCY: {
    threshold: 1,
    level: AlertLevel.WARNING,
  },
  PAYMENT_MISSING_INTERNAL: {
    threshold: 1,           // Pagamento confirmado mas não creditado
    level: AlertLevel.CRITICAL,
  },
};

Endpoints da API

Executar Reconciliação Manual

http
POST /api/admin/reconciliation/run
Authorization: Bearer {adminSessionId}
Content-Type: application/json

{
  "type": "BALANCE",
  "scope": "ALL",
  "autoFix": true
}

Consultar Resultados

http
GET /api/admin/reconciliation/results?date=2024-01-15

Exportar Relatório

http
GET /api/admin/reconciliation/export?startDate=2024-01-01&endDate=2024-01-31&format=PDF

Troubleshooting

Discrepância Persistente

typescript
// Debug de discrepância
async debugDiscrepancy(userId: bigint) {
  // 1. Buscar todas as transações do usuário
  const transactions = await this.transactionRepository.findByUser(userId);
  
  // 2. Calcular saldo passo a passo
  let runningBalance = 0n;
  for (const tx of transactions) {
    if (tx.debitUserId === userId) {
      runningBalance -= tx.amountCents;
    }
    if (tx.creditUserId === userId) {
      runningBalance += tx.amountCents;
    }
    console.log(`Tx ${tx.id}: ${tx.type} | Balance: ${runningBalance}`);
  }
  
  // 3. Comparar com cache
  const user = await this.userRepository.findById(userId);
  console.log(`Cache: ${user.balanceCents}`);
  console.log(`Calculado: ${runningBalance}`);
  console.log(`Diferença: ${runningBalance - user.balanceCents}`);
}

Transação Órfã

typescript
// Encontrar e corrigir transação órfã
async fixOrphanTransaction(transactionId: bigint) {
  const tx = await this.transactionRepository.findById(transactionId);
  
  if (!tx) {
    throw new Error('Transação não encontrada');
  }
  
  // Verificar se é realmente órfã
  const pair = await this.transactionRepository.findPair(
    tx.referenceId,
    tx.referenceType,
  );
  
  if (pair.length === 2) {
    console.log('Transação tem par - não é órfã');
    return;
  }
  
  // Decidir ação baseada no tipo
  if (tx.type === TransactionType.CREDIT) {
    // Crédito sem débito - potencial problema
    console.log('ALERTA: Crédito sem débito correspondente');
    // Notificar admin para análise manual
  }
  
  if (tx.type === TransactionType.DEBIT) {
    // Débito sem crédito - reverter
    console.log('Revertendo débito órfão');
    await this.transactionService.createTransaction({
      creditUserId: tx.debitUserId,
      debitUserId: HOUSE_USER_ID,
      amountCents: tx.amountCents,
      type: TransactionType.REVERSAL,
      referenceId: tx.id,
      referenceType: 'ORPHAN_FIX',
    });
  }
}

Melhores Práticas

  1. Executar reconciliação regularmente - Mínimo a cada hora para saldos
  2. Manter logs detalhados - Toda correção deve ser auditada
  3. Alertar proativamente - Detectar problemas antes do usuário
  4. Nunca ignorar discrepâncias - Toda inconsistência tem uma causa
  5. Validar correções - Após corrigir, verificar se realmente foi resolvido
  6. Manter histórico - Guardar histórico de reconciliações para análise de padrões

Documentação Técnica CSGOFlip