back
Project

Esse projeto foi criado com o objetivo de estudar e demonstrar diferentes padroes de design.

O Desafio do PicPay é um desafio técnico clássico de backend: construir uma API de transferências entre usuários, com regras de negócio que escondem várias decisões. Peguei para treinar Java + Spring Boot com foco em organizar o código de um jeito que eu defenderia numa code review.

A regra central: um usuário transfere dinheiro para outro. O sistema precisa checar saldo, garantir que o tipo de usuário pode enviar dinheiro, e bater num autorizador externo. Se algo falhar, nada acontece.

A regra de negócio mora na entidade

Coloquei a lógica de saldo dentro da entidade User, não espalhada no service. Quem sabe somar e subtrair saldo é o usuário, e ele se recusa a ficar negativo:

public void subtractBalance(BigDecimal amount) {
    if (getBalance().compareTo(amount) < 0) {
        throw new InsufficientFundsException(getId(), amount, getBalance());
    }
    setBalance(getBalance().subtract(amount));
}

Duas decisões: usar BigDecimal para dinheiro, nunca double, e deixar a entidade proteger seu próprio estado. O service não consegue zerar o saldo de alguém por engano. A entidade não deixa.

O service orquestra, não decide tudo

O TransactionService valida o que é dele (tipos de usuário) e delega o resto:

public Transaction saveTransaction(TransactionDTO transaction) {
    if (transaction.buyer().getType() == UserType.SELLER)
        throw new WrongUserTypeException(transaction.buyer().getId());
    if (transaction.seller().getType() != UserType.SELLER)
        throw new WrongUserTypeException(transaction.seller().getId());

    if (validateTransaction()) { // bate no autorizador externo
        transaction.buyer().subtractBalance(transaction.amount());
        transaction.seller().addBalance(transaction.amount());
        // ... monta e salva a transação
    }
}

A regra "lojista não envia dinheiro, só recebe" fica explícita antes de qualquer débito.

O Gateway: trocando dependência externa

O autorizador externo fica atrás de uma interface, não chamado direto no service:

public interface ApiGateway {
    <T extends ApiDTO> T call();
}

Duas implementações. A MockyGateway faz a chamada HTTP real via RestTemplate, apontando para um endpoint do Mocky.io configurável por @Value:

@Override
public APIGatewayDTO call() {
    ResponseEntity<APIGatewayDTO> response = template.getForEntity(url, APIGatewayDTO.class);
    if (response == null || response.getStatusCode() != HttpStatus.OK || response.getBody() == null) {
        throw new MockyDefaultExceptin("Erro na transação");
    }
    if (!response.getBody().message().equals("Autorizado")) {
        throw new MockyAuthException("Você não está autorizado");
    }
    return response.getBody();
}

A FakeGateway devolve "Autorizado" sempre:

@Override
public APIGatewayDTO call() {
    return new APIGatewayDTO("Autorizado");
}

Padrão Strategy/Adapter: o service depende da abstração. Em desenvolvimento injeto a FakeGateway e não dependo de internet nem de serviço de terceiro estar de pé. Trocar de provedor de autorização vira criar uma nova implementação.

Segurança e estrutura

Autenticação JWT: a entidade User implementa UserDetails do Spring Security, e um JwtAuthenticationFilter valida o token a cada requisição. O projeto segue camadas no estilo clean architecture: domain (entidades e regras), application (services, DTOs, gateways) e presentation (controllers).

Faltou idempotência: se o cliente reenvia a mesma requisição por timeout ou retry, dá para duplicar uma transferência. Uma chave de idempotência por requisição resolveria. E testes de integração cobrindo o fluxo completo (saldo insuficiente, lojista enviando dinheiro, autorizador recusando), que são os caminhos onde bug de dinheiro dói.