Fluxo de Depósitos
O sistema de depósitos suporta múltiplos métodos de pagamento com processamento via webhook.
Métodos Suportados
| Método | Status | Tempo | Taxa |
|---|---|---|---|
| PIX | ✅ Ativo | Instantâneo | 0% |
| Cartão de Crédito | ✅ Ativo | Instantâneo | 3.5% |
| Crypto (BTC/ETH) | ✅ Ativo | 10-60 min | 1% |
Estados do Depósito
PENDING → CONFIRMED
↘ FAILED
↘ EXPIRED
↘ CANCELLEDtypescript
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.tssrc/application/use-cases/payment/confirm-deposit.use-case.tssrc/presentation/controllers/webhook.controller.tssrc/infrastructure/payment/- Providers de pagamento
