Skip to content

Fluxo de Depósitos

O sistema de depósitos suporta múltiplos métodos de pagamento com processamento via webhook.

Métodos Suportados

MétodoStatusTempoTaxa
PIX✅ AtivoInstantâneo0%
Cartão de Crédito✅ AtivoInstantâneo3.5%
Crypto (BTC/ETH)✅ Ativo10-60 min1%

Estados do Depósito

PENDING → CONFIRMED
       ↘ FAILED
       ↘ EXPIRED
       ↘ CANCELLED
typescript
enum DepositStatus {
  PENDING    // Aguardando pagamento
  CONFIRMED  // Pagamento confirmado
  FAILED     // Falhou
  EXPIRED    // Expirou (não pago)
  CANCELLED  // Cancelado pelo usuário
}

Fluxo Completo

Implementação

Iniciar Depósito

typescript
// src/application/use-cases/payment/initiate-deposit.use-case.ts

@Injectable()
export class InitiateDepositUseCase {
  constructor(
    private readonly depositRepository: IDepositRepository,
    private readonly paymentGateway: IPaymentGateway,
    private readonly snowflake: SnowflakeService,
  ) {}

  async execute(userId: bigint, amount: bigint, method: PaymentMethod) {
    // 1. Validações
    if (amount < 500n) { // Mínimo R$ 5
      throw new BadRequestException('Minimum deposit is R$ 5.00');
    }

    if (amount > 10000000n) { // Máximo R$ 100.000
      throw new BadRequestException('Maximum deposit is R$ 100,000.00');
    }

    // 2. Cria cobrança no gateway
    const gatewayResponse = await this.paymentGateway.createCharge({
      amount: Number(amount) / 100,
      method,
      externalReference: this.snowflake.generate().toString(),
    });

    // 3. Salva depósito pendente
    const deposit = await this.depositRepository.create({
      id: this.snowflake.generate(),
      userId,
      amountCents: amount,
      method,
      status: 'PENDING',
      externalId: gatewayResponse.id,
      expiresAt: gatewayResponse.expiresAt,
      metadata: {
        qrCode: gatewayResponse.qrCode,
        qrCodeText: gatewayResponse.qrCodeText,
      },
    });

    return {
      depositId: deposit.id.toString(),
      qrCode: gatewayResponse.qrCode,
      qrCodeText: gatewayResponse.qrCodeText,
      expiresAt: gatewayResponse.expiresAt,
      amountCents: amount.toString(),
    };
  }
}

Webhook Handler

typescript
// src/presentation/controllers/webhook.controller.ts

@Controller('webhooks')
export class WebhookController {
  @Post('payment')
  @Public()
  async handlePaymentWebhook(
    @Body() body: PaymentWebhookDto,
    @Headers('x-webhook-signature') signature: string,
  ) {
    // 1. Valida assinatura
    const isValid = this.paymentGateway.verifyWebhookSignature(
      JSON.stringify(body),
      signature,
    );

    if (!isValid) {
      throw new UnauthorizedException('Invalid webhook signature');
    }

    // 2. Processa evento
    switch (body.event) {
      case 'charge.paid':
        await this.confirmDepositUseCase.execute(body.data.id);
        break;
      case 'charge.failed':
        await this.failDepositUseCase.execute(body.data.id, body.data.failReason);
        break;
      case 'charge.expired':
        await this.expireDepositUseCase.execute(body.data.id);
        break;
    }

    return { received: true };
  }
}

Confirmar Depósito

typescript
// src/application/use-cases/payment/confirm-deposit.use-case.ts

@Injectable()
export class ConfirmDepositUseCase {
  constructor(
    private readonly depositRepository: IDepositRepository,
    private readonly transactionService: TransactionService,
    private readonly websocketGateway: WebSocketGateway,
    private readonly notificationService: NotificationService,
    private readonly auditService: AuditService,
  ) {}

  async execute(externalId: string) {
    // 1. Busca depósito
    const deposit = await this.depositRepository.findByExternalId(externalId);

    if (!deposit) {
      throw new NotFoundException('Deposit not found');
    }

    if (deposit.status !== 'PENDING') {
      // Já processado (idempotência)
      return;
    }

    // 2. Atualiza status
    await this.depositRepository.update(deposit.id, {
      status: 'CONFIRMED',
      confirmedAt: new Date(),
    });

    // 3. Cria transação de crédito
    await this.transactionService.credit(
      deposit.userId,
      deposit.amountCents,
      TransactionReason.DEPOSIT,
    );

    // 4. Notifica via WebSocket
    const newBalance = await this.transactionService.getUserBalance(deposit.userId);
    this.websocketGateway.emitBalanceUpdate(deposit.userId, newBalance);

    // 5. Cria notificação
    await this.notificationService.create(deposit.userId, {
      type: 'DEPOSIT_CONFIRMED',
      title: 'Depósito confirmado!',
      message: `Seu depósito de ${this.formatBRL(deposit.amountCents)} foi confirmado.`,
    });

    // 6. Audit log
    await this.auditService.log({
      action: 'DEPOSIT_CONFIRMED',
      userId: deposit.userId,
      entityType: 'Deposit',
      entityId: deposit.id,
      metadata: {
        amount: deposit.amountCents.toString(),
        method: deposit.method,
        externalId,
      },
    });
  }
}

Segurança do Webhook

Validação de Assinatura

typescript
verifyWebhookSignature(payload: string, signature: string): boolean {
  const expectedSignature = crypto
    .createHmac('sha256', this.webhookSecret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature),
  );
}

IP Whitelist

typescript
@Injectable()
export class WebhookIpGuard implements CanActivate {
  private readonly allowedIps = [
    '52.67.xxx.xxx', // IP do gateway
    '18.231.xxx.xxx',
  ];

  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const clientIp = request.ip;

    return this.allowedIps.includes(clientIp);
  }
}

Confirmação Manual (Admin)

Para casos onde o webhook falha:

typescript
// src/application/use-cases/admin/confirm-deposit.use-case.ts

@Injectable()
export class AdminConfirmDepositUseCase {
  async execute(depositId: bigint, adminId: bigint) {
    const deposit = await this.depositRepository.findById(depositId);

    if (deposit.status !== 'PENDING') {
      throw new BadRequestException('Deposit is not pending');
    }

    // Confirma manualmente
    await this.confirmDepositUseCase.execute(deposit.externalId);

    // Log de ação admin
    await this.auditService.log({
      action: 'ADMIN_CONFIRM_DEPOSIT',
      userId: adminId,
      entityType: 'Deposit',
      entityId: depositId,
    });
  }
}

API Endpoints

POST /payment/deposit/initiate

json
// Request
{
  "amountCents": 10000,
  "method": "PIX"
}

// Response
{
  "depositId": "123456789",
  "qrCode": "data:image/png;base64,...",
  "qrCodeText": "00020126...",
  "expiresAt": "2024-01-15T15:30:00Z",
  "amountCents": "10000"
}

GET /payment/deposits

json
// Response
{
  "deposits": [
    {
      "id": "123456789",
      "amountCents": "10000",
      "amountFormatted": "R$ 100,00",
      "method": "PIX",
      "status": "CONFIRMED",
      "createdAt": "2024-01-15T14:30:00Z",
      "confirmedAt": "2024-01-15T14:31:00Z"
    }
  ]
}

Expiração de Depósitos

typescript
// Job que roda a cada minuto
@Cron('* * * * *')
async expirePendingDeposits() {
  const expiredDeposits = await this.depositRepository.findExpired();

  for (const deposit of expiredDeposits) {
    await this.depositRepository.update(deposit.id, {
      status: 'EXPIRED',
    });

    await this.notificationService.create(deposit.userId, {
      type: 'DEPOSIT_EXPIRED',
      title: 'Depósito expirado',
      message: 'Seu depósito expirou. Inicie um novo depósito se desejar.',
    });
  }
}

Arquivos Fonte

Principais Arquivos

  • src/application/use-cases/payment/initiate-deposit.use-case.ts
  • src/application/use-cases/payment/confirm-deposit.use-case.ts
  • src/presentation/controllers/webhook.controller.ts
  • src/infrastructure/payment/ - Providers de pagamento

Documentação Técnica CSGOFlip