Solucionado (ver solução)
Solucionado
(ver solução)
5
respostas

[Bug] Chamada não faz mais que uma requisição

Oi!

Após eu fazer os ajustes na minha aplicação, percebi que executa corretamente somente quando eu vou criar uma transação, fazendo todos os cálculos necessários, pela forma_pagamento desejada. O problema está ocorrendo quando eu tento criar uma nova transação, retornando o erro abaixo:

Error: could not execute statement [Referential integrity constraint violation: "FK_TRANSACOES_CONTAS_ID: PUBLIC.TRANSACOES FOREIGN KEY(ID) REFERENCES PUBLIC.CONTAS(ID) (CAST(2 AS BIGINT))"; SQL statement: insert into transacoes (forma_pagamento,numero_conta,saldo,valor,id) values (?,?,?,?,default) [23506-220]] [insert into transacoes (forma_pagamento,numero_conta,saldo,valor,id) values (?,?,?,?,default)]; SQL [insert into transacoes (forma_pagamento,numero_conta,saldo,valor,id) values (?,?,?,?,default)]; constraint [FK_TRANSACOES_CONTAS_ID]

Pergunta. Esse problema acontece por influência do tipo de relacionamento que apliquei na classe de entidade (Transaction)? Vou passar como estão as classes de entidade que criei, pois eu coloquei o relacionamento 1 para 1 (@OneToOne). Poderiam ajudar, por favor?

@Table(name = "contas")
@Entity(name = "Account")
@Setter
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class Account {

    @Id
    @Column(name = "id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String numero_conta;

    private Double saldo;

    private Boolean ativo;

    public Account(DataRegistrationAccount data){
        this.numero_conta = data.numero_conta();
        this.saldo = data.saldo();
        this.ativo = true;
    }

    public Account(DataDetailingAccount data){
        this.numero_conta = data.numero_conta();
        this.saldo = data.saldo();
    }

    public void deleteOrInvalidateInformations(){
        this.ativo = false;
    }
}
@Table(name = "transacoes")
@Entity(name = "Transaction")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class Transaction {

    @Id
    @Column(name = "id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Enumerated(EnumType.STRING)
    private PaymentForm forma_pagamento;

    private String numero_conta;

    private Double valor;

    private Double saldo;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id")
    private Account account;

    public Transaction(DataRegistrationTransaction data) {
        this.forma_pagamento = data.forma_pagamento();
        this.numero_conta = data.numero_conta();
        this.valor = data.valor();
    }

    public Transaction(DataDetailingAccount data) {
        this.numero_conta = data.numero_conta();
        this.saldo = data.saldo();
    }
}

Classe onde eu coloquei todas as regras de negócio (service):

@Service
@Transactional
@RequiredArgsConstructor
public class TransactionService {

    @Autowired
    private AccountRepository accountRepository;

    @Autowired
    private TransactionRepository transactionRepository;

    public DataDetailingTransaction saveTransaction(Transaction transactions) {
        if (transactions.getNumero_conta() == null) {
            throw new ValidationException("Conta não cadastrada na base de dados.");
        }

        Optional<Transaction> optTransaction = Optional.ofNullable(transactionRepository.findTransactionByAtivoTrue(transactions.getNumero_conta()));

        var valorCalculado = computeTaxTransaction(transactions);

        Optional<Account> optAccount = Optional.ofNullable(accountRepository.findAccountByAtivoTrue(transactions.getNumero_conta()));

        double saldoInicial = 0.0;

        saldoInicial = optAccount.get().getSaldo();

        double saldo = 0.0;
        saldo = saldoInicial - valorCalculado;

        var account = new Account(optAccount.get().getId(), transactions.getNumero_conta(), saldo, true);
        accountRepository.save(account);

        Transaction saveTransaction;

        var transaction = new Transaction(transactions.getId(), transactions.getForma_pagamento(), transactions.getNumero_conta(), transactions.getValor(), saldo, account);
        saveTransaction = transactionRepository.save(transaction);

        return new DataDetailingTransaction(saveTransaction);
    }

    private double computeTaxTransaction(Transaction transactions) {
        if (transactions.getForma_pagamento() == null) {
            throw new ValidationException("É obrigatório informar a forma de pagamento para validar o processo de transação.");
        }
        double taxTransaction = 0.0;
        PaymentForm paymentForm = transactions.getForma_pagamento();

        taxTransaction = paymentForm.computeTaxTransaction(transactions.getValor());

        return taxTransaction;
    }
}
5 respostas

Oi!

Acho que você pode melhorar a configuração do relacionamento nas suas entidades e no banco de dados também.

Pelo que eu entendi do seu código, você tem uma entididade que representa uma transação e ela está vinculada a uma conta. Porém, na sua entidade Transacao, você está repetindo informações da conta, sendo que apenas deveria ter o atributo do relacionamento com a conta.

Como eu faria o mapeamento:

@Table(name = "transacoes")
@Entity(name = "Transaction")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class Transaction {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Enumerated(EnumType.STRING)
    private PaymentForm formaPagamento;

    private BigDecimal valor;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "conta_id")
    private Account account;
    
    public Transaction(DataRegistrationTransaction data, Account account) {
        this.formaPagamento = data.formaPagamento();
        this.valor = data.valor();
        this.account = account;
    }
}
@Table(name = "contas")
@Entity(name = "Account")
@Setter
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class Account {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String numeroConta;

    private BigDecimal saldo;

    private Boolean ativo;
    
    public Account(DataDetailingAccount data) {
        this.numeroConta = data.numeroConta();
        this.saldo = data.saldo();
    }

    public void deleteOrInvalidateInformations() {
        this.ativo = false;
    }
}

Repare que agora a entidade Transaction não tem os atributo numero_conta e nem saldo, pois eles já estão na entidade Account. Outra coisa, o seu relacionamento estava anotado com @JoinColumn(name = "id"), sendo que a coluna id é o id da transação em si. Alterei para conta_id, para que seja uma chave estrangeira que aponta para o id da tabela de Account.

A estrutura das suas tabelas deveria ser assim:

create table contas(

    id bigint not null auto_increment,
    numero_conta varchar not null,
    saldo bigdecimal(8,2) not null,
    ativo boolean not null default true,

    primary key(id),
);
create table transacoes(

    id bigint not null auto_increment,
    forma_pagamento enum('CREDITO', 'DEBITO', 'PIX') not null, --Coloque no enum() as constantes do seu enum PaymentForm
    valor bigdecimal(8,2) not null,
    conta_id bigint not null,

    primary key(id),
    constraint fk_transacoes_conta_id foreign key(conta_id) references contas(id)
);

Observações:

  1. Nunca utilize Double para representar valores monetários, mas sim BigDecimal
  2. Siga a convenção de nomenclatura CamelCase do Java nos seus atributos (numeroConta ao invés de numero_conta)
  3. Seu relacionamento está como @OneToOne. Dessa forma, cada conta poderá ter apenas uma única transação. Acredito que deveria ser ManyToOne, para que cada conta possa ter múltiplas transações

Oi, professor. Primeiro, venho agradecer pela ajuda nas configurações, que acabei fazendo na aplicação e no relacionamento entre as tabelas do banco (@ManyToOne), tudo certo até aqui. Porém, tenho o segundo cenário, que é a criação de transações, pela entidade Transaction. Para a criação, vou utilizar os campos abaixo:

  1. formaPagamento
  2. valor
  3. numeroConta

O campo numeroConta já existe na entidade Account e nela, eu vou utilizar na requisição POST, para verificar se a conta existe para eu criar a transação, que, por enquanto, não está fazendo. Insira aqui a descrição dessa imagem para ajudar na acessibilidade

A classe Transaction, atualmente, após o ajuste, ficou dessa forma:

@Table(name = "transacoes")
@Entity(name = "Transaction")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class Transaction {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Enumerated(EnumType.STRING)
    @Column(name = "forma_pagamento")
    private PaymentForm formaPagamento;

    @Column(name = "valor")
    private BigDecimal valor;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "conta_id")
    private Account account;

    public Transaction(DataRegistrationTransaction request, Account account) {
        this.formaPagamento = request.formaPagamento();
        this.valor = request.valor();
        this.account = account;
    }
}

Como pode observar, no caso, na classe DTO DataRegistrationTransaction, não preciso enviar o id, como eu tinha feito, ajustando dessa forma:

public record DataRegistrationTransaction(
        @NotNull
        PaymentForm formaPagamento,
        @NotNull
        BigDecimal valor) {
}

Pergunta: Como que eu faço para incluir o campo numeroConta para ser utilizado na requisição, para verificar no banco? Insira aqui a descrição dessa imagem para ajudar na acessibilidade

No seu caso então a conta já deve estar previamente cadastrada no banco de dados, e no cadastro de transação deve ser indicado a conta que essa transação pertence, certo?

Então, na requisição para cadastrar uma transação você deve enviar alguma informação única da conta (id ou número). Exemplo:

POST / transacao

{
    "forma_pagamento": "CREDITO",
    "valor": 300.99,
    "numero_conta": "1234"
}

DTO que representa esses dados:

public record DataRegistrationTransaction(
        @NotNull
        @JsonProperty("forma_pagamento") //use essa anotação quando o campo no json tiver um nome diferente do campo no DTO
        PaymentForm formaPagamento,
        
        @NotNull
        BigDecimal valor,
        
        @NotNull
        @JsonProperty("numero_conta")
        String numeroConta
        ) {}

Exemplo de lógica para cadastrar a transação:

public DataDetailingTransaction saveTransaction(DataRegistrationTransaction data) {
    var account = accountRepository.findByNumeroConta(data.numeroConta());
    if (account == null) {
        throw new ValidationException("Conta não cadastrada na base de dados.");
    }
    
    // outras validações ou regras de negócio aqui...
    
    var transaction = new Transaction(data, account);
    transactionRepository.save(transaction);
    
    return new DataDetailingTransaction(transaction);
}

Oi!

Fiz os ajustes no DTO e na classe service. Só que na hora de testar, a requisição POST ficou diferente, com dois requests. Poderia me ajudar? Insira aqui a descrição dessa imagem para ajudar na acessibilidadeSeguem as classes controller, DTO (request) e service:

@RestController
@RequestMapping("transacao")
@SecurityRequirement(name = "bearer-key")
public class TransactController {

    @Autowired
    private TransactionService transactionService;

    @Autowired
    private AccountService accountService;

    @PostMapping
    @Transactional
    public ResponseEntity<DataDetailingTransaction> createTransaction(@RequestBody @Valid DataRegistrationTransaction request, Account account, UriComponentsBuilder uriBuilder) {
        DataRegistrationTransaction transaction = new DataRegistrationTransaction(request, account);

        Optional<Account> optionalAccount = accountService.findAccount(transaction.numeroConta());

        var dataDetailingTransaction = transactionService.saveTransaction(transaction);

        BigDecimal fundValue = optionalAccount.get().getSaldo();
        BigDecimal transactionValue = transaction.valor();
        int diferenca = transactionValue.compareTo(fundValue);

        if (diferenca < 0) {
            return ResponseEntity.notFound().build();
        }

        var uri = uriBuilder
                .path("/transacao/{id}")
                .buildAndExpand(dataDetailingTransaction.id())
                .toUri();

        return ResponseEntity.created(uri).body(dataDetailingTransaction);
    }
}
public record DataRegistrationTransaction(
        @NotNull
        @JsonProperty("forma_pagamento")
        PaymentForm formaPagamento,

        @NotNull
        BigDecimal valor,

        @NotNull
        @JsonProperty("numero_conta")
        String numeroConta) {
        public DataRegistrationTransaction(DataRegistrationTransaction request, Account account) {
                this(request.formaPagamento(), request.valor(), account.getNumeroConta());
        }
}
@Service
@Transactional
@RequiredArgsConstructor
public class TransactionService {

    @Autowired
    private AccountRepository accountRepository;

    @Autowired
    private TransactionRepository transactionRepository;

    public DataDetailingTransaction saveTransaction(DataRegistrationTransaction data) {
        var findAccount = accountRepository.findAccountByAtivoTrue(data.numeroConta());
        if (findAccount == null) {
            throw new ValidationException("Conta não cadastrada na base de dados.");
        }

        // Outras validações ou regras de negócio aqui...
        if (data.numeroConta() == null) {
            throw new ValidationException("Conta não cadastrada na base de dados.");
        }

        Optional<Transaction> optTransaction = Optional.ofNullable(transactionRepository.findTransactionByAtivoTrue(data.numeroConta()));

        BigDecimal valorCalculado = computeTaxTransaction(data);

        Optional<Account> optAccount = Optional.ofNullable(accountRepository.findAccountByAtivoTrue(data.numeroConta()));

        BigDecimal saldoInicial;

        saldoInicial = optAccount.get().getSaldo();

        BigDecimal saldo;
        saldo = saldoInicial.subtract(valorCalculado);

        var account = new Account(optAccount.get().getId(), data.numeroConta(), saldo, true);
        accountRepository.save(account);

        var transaction = new Transaction(data, findAccount);
        transactionRepository.save(transaction);
        return new DataDetailingTransaction(transaction);
    }

    private BigDecimal computeTaxTransaction(DataRegistrationTransaction transactions) {
        if (transactions.formaPagamento() == null) {
            throw new ValidationException("É obrigatório informar a forma de pagamento para validar o processo de transação.");
        }
        BigDecimal taxTransaction;
        PaymentForm paymentForm = transactions.formaPagamento();

        taxTransaction = paymentForm.computeTaxTransaction(transactions.valor());

        return taxTransaction;
    }
}
solução!

Seu método createTransaction no controller está com um parâmetro account que não deveria ter. Apague esse parâmetro do método.