Publicado em

Processando Webhooks de Validação Facial com C# e Entity Framework

Autores
  • avatar
    Nome
    Maicon Oliveira
    Twitter
C# Webhook

O Cenário

Em ecossistemas fintech, validações assíncronas são padrão. Você envia a biometria do usuário para análise e precisa esperar um callback. Este artigo demonstra como arquitetar uma solução robusta em .NET para receber esses webhooks, atualizar dados sensíveis (Email, Telefone, Conta Bancária) e expor um endpoint de consulta de status.

Vamos seguir rigorosamente os princípios de Clean Architecture, separando responsabilidades em Models, DTOs, Services e Controllers.


🏗️ 1. Domain Models & DTOs

Primeiro, definimos o que esperamos receber do provedor externo e como é a nossa entidade interna.

Request DTO (Webhook Payload)

O payload do webhook geralmente contém um ID único da transação e o status da validação.

// Dtos/FacialValidationWebhookDto.cs
public class FacialValidationWebhookDto
{
    public Guid TransactionFaceId { get; set; }
    public string Status { get; set; } // "Approved", "Rejected", "AwaitingWebhook"
    public string? NewEmail { get; set; }
    public string? NewPhoneNumber { get; set; }
    public BankAccountDto? NewBankAccount { get; set; }
}

public class BankAccountDto
{
    public string Branch { get; set; }
    public string AccountNumber { get; set; }
    public string BankCode { get; set; }
}

Entity Model

Nossa entidade de banco de dados que representa o processo de validação do cliente.

// Models/CustomerValidation.cs
public class CustomerValidation
{
    public Guid Id { get; set; }
    public Guid CustomerId { get; set; }
    public Guid TransactionFaceId { get; set; } // External reference
    public string Status { get; set; } // "Pending", "Approved", "Rejected"
    public DateTime UpdatedAt { get; set; }

    // Navigation property to the actual Customer
    public virtual Customer Customer { get; set; }
}

⚙️ 2. The Service Layer

A lógica de negócio vive aqui. Precisamos processar o webhook (escrita) e verificar o status (leitura).

Interface

// Services/IFacialValidationService.cs
public interface IFacialValidationService
{
    Task ProcessWebhookAsync(FacialValidationWebhookDto dto);
    Task<string> GetValidationStatusAsync(Guid transactionFaceId);
}

Implementation (Entity Framework)

// Services/FacialValidationService.cs
public class FacialValidationService : IFacialValidationService
{
    private readonly AppDbContext _context;
    private readonly ILogger<FacialValidationService> _logger;

    public FacialValidationService(AppDbContext context, ILogger<FacialValidationService> logger)
    {
        _context = context;
        _logger = logger;
    }

    public async Task ProcessWebhookAsync(FacialValidationWebhookDto dto)
    {
        // 1. Find the validation record
        var validation = await _context.CustomerValidations
            .Include(v => v.Customer)
            .FirstOrDefaultAsync(v => v.TransactionFaceId == dto.TransactionFaceId);

        if (validation == null)
        {
            _logger.LogError($"Transaction {dto.TransactionFaceId} not found.");
            return;
        }

        // 2. Update status
        validation.Status = dto.Status;
        validation.UpdatedAt = DateTime.UtcNow;

        // 3. If Approved, update sensitive data
        if (dto.Status == "Approved")
        {
            if (dto.NewEmail != null)
                validation.Customer.Email = dto.NewEmail;

            if (dto.NewPhoneNumber != null)
                validation.Customer.PhoneNumber = dto.NewPhoneNumber;

            if (dto.NewBankAccount != null)
            {
                validation.Customer.BankBranch = dto.NewBankAccount.Branch;
                validation.Customer.BankAccount = dto.NewBankAccount.AccountNumber;
                validation.Customer.BankCode = dto.NewBankAccount.BankCode;
            }
        }

        // 4. Save changes (EF Core updates both tables in a transaction)
        await _context.SaveChangesAsync();
    }

    public async Task<string> GetValidationStatusAsync(Guid transactionFaceId)
    {
        var status = await _context.CustomerValidations
            .Where(v => v.TransactionFaceId == transactionFaceId)
            .Select(v => v.Status)
            .FirstOrDefaultAsync();

        return status ?? "NotFound";
    }
}

🎮 3. The Controller API

Com a lógica desacoplada, nosso Controller fica limpo e focado apenas nas preocupações HTTP.

// Controllers/FacialValidationController.cs
[ApiController]
[Route("api/v1/facial-validation")]
public class FacialValidationController : ControllerBase
{
    private readonly IFacialValidationService _service;

    public FacialValidationController(IFacialValidationService service)
    {
        _service = service;
    }

    // Endpoint 1: The Webhook Receiver
    [HttpPost("webhook")]
    public async Task<IActionResult> ReceiveWebhook([FromBody] FacialValidationWebhookDto payload)
    {
        if (payload == null) return BadRequest("Invalid payload");

        // Fire-and-forget or await depending on requirement.
        // Usually, webhooks expect a 200 OK quickly.
        await _service.ProcessWebhookAsync(payload);

        return Ok(new { message = "Webhook received and processed" });
    }

    // Endpoint 2: Status Polling
    [HttpGet("status/{transactionFaceId}")]
    public async Task<IActionResult> GetStatus(Guid transactionFaceId)
    {
        var status = await _service.GetValidationStatusAsync(transactionFaceId);

        if (status == "NotFound") return NotFound();

        return Ok(new { transactionId = transactionFaceId, status = status });
    }
}

Conclusão

Ao usar Entity Framework, ganhamos o benefício do gerenciamento automático de transações — a atualização do registro de Validação e do Cliente acontece atomicamente quando SaveChangesAsync() é chamado.

Esta arquitetura traz clareza:

  • Controllers apenas roteiam o tráfego.
  • Services detêm as regras de domínio (ex: só atualizar dados se "Approved").
  • DTOs protegem nossa estrutura interna de banco de dados contratos externos.