Skip to content

Commit

Permalink
feature#25: Hexagonal architecture improvement and refactoring respon…
Browse files Browse the repository at this point in the history
…sabilities of services
  • Loading branch information
ClementGib committed Feb 23, 2024
1 parent 783be73 commit 6b72829
Show file tree
Hide file tree
Showing 38 changed files with 1,033 additions and 608 deletions.
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
package com.cdx.bas.application.bank.account;

import com.cdx.bas.domain.bank.account.*;
import com.cdx.bas.domain.bank.account.BankAccount;
import com.cdx.bas.domain.bank.account.BankAccountPersistencePort;
import com.cdx.bas.domain.bank.account.BankAccountServicePort;
import com.cdx.bas.domain.bank.account.BankAccountValidator;
import com.cdx.bas.domain.money.Money;
import com.cdx.bas.domain.transaction.Transaction;
import com.cdx.bas.domain.transaction.TransactionException;
import com.cdx.bas.domain.transaction.TransactionServicePort;
import com.cdx.bas.domain.utils.ExchangeRateUtils;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;

import static jakarta.transaction.Transactional.TxType.REQUIRES_NEW;

@RequestScoped
public class BankAccountServiceImpl implements BankAccountServicePort {

Expand All @@ -35,74 +33,44 @@ public class BankAccountServiceImpl implements BankAccountServicePort {
TransactionServicePort transactionService;

@Override
@Transactional(value = REQUIRES_NEW)
public Transaction deposit(Transaction transaction) {
Map<String, String> metadata = new HashMap<>();
try {
BankAccount senderBankAccount = bankAccountRepository.findById(transaction.getSenderAccountId())
.orElseThrow(() -> new NoSuchElementException("Sender bank account " + transaction.getSenderAccountId() + " is not found."));

BankAccount receiverBankAccount = bankAccountRepository.findById(transaction.getReceiverAccountId())
.orElseThrow(() -> new NoSuchElementException("Receiver bank account " + transaction.getReceiverAccountId() + " is not found."));

Transaction currentTransaction = transactionService.setAsOutstanding(transaction);

logger.info("Bank account " + currentTransaction.getSenderAccountId()
+ " process deposit transaction " + currentTransaction.getId()
+ " with amount " + currentTransaction.getAmount()
+ " to bank account " + currentTransaction.getReceiverAccountId());

metadata.put("sender_amount_before", senderBankAccount.getBalance().getAmount().toString());
metadata.put("receiver_amount_before", receiverBankAccount.getBalance().getAmount().toString());
processCreditTransaction(senderBankAccount, receiverBankAccount, currentTransaction);
bankAccountValidator.validateBankAccount(senderBankAccount);
bankAccountValidator.validateBankAccount(receiverBankAccount);
metadata.put("sender_amount_after", senderBankAccount.getBalance().getAmount().toString());
metadata.put("receiver_amount_after", receiverBankAccount.getBalance().getAmount().toString());

Transaction completedTransaction = transactionService.setAsCompleted(currentTransaction, metadata);
BankAccount updatedSenderBankAccount = addTransaction(senderBankAccount, completedTransaction);
bankAccountRepository.update(receiverBankAccount);

logger.info("Bank account " + updatedSenderBankAccount.getId() + " deposit transaction " + + currentTransaction.getId() + " completed.");
return completedTransaction;
} catch (NoSuchElementException exception) {
logger.error("Transaction " + transaction.getId() + " deposit error for amount "+ transaction.getAmount() + ": " + exception.getMessage());
metadata.put("error", exception.getMessage());
return transactionService.setAsError(transaction, metadata);
} catch (TransactionException exception) {
logger.error("Transaction " + transaction.getId() + " of " + transaction.getAmount() + " is invalid.");
metadata.put("error", exception.getMessage());
return transactionService.setAsRefused(transaction, metadata);
} catch (BankAccountException exception) {
logger.error("Transaction " + transaction.getId() + " deposit refused for amount "+ transaction.getAmount() + ": " + exception.getMessage());
metadata.put("error", exception.getMessage());
return transactionService.setAsRefused(transaction, metadata);
}
public BankAccount findBankAccount(Long bankAccountId){
return bankAccountRepository.findById(bankAccountId)
.orElseThrow(() -> new NoSuchElementException("Sender bank account " + bankAccountId + " is not found."));
}

private void processCreditTransaction(BankAccount senderBankAccount, BankAccount receiverBankAccount, Transaction transactionToCredit) {
BigDecimal euroAmount = ExchangeRateUtils.getEuroAmountFrom(transactionToCredit.getCurrency(), transactionToCredit.getAmount());
@Override
public void postTransaction(Transaction transactionToPost, BankAccount emitterBankAccount, BankAccount receiverBankAccount) {
BigDecimal euroAmount = ExchangeRateUtils.getEuroAmountFrom(transactionToPost.getCurrency(), transactionToPost.getAmount());
if (euroAmount.signum() < 0) {
throw new TransactionException("Credit transaction " + transactionToCredit.getId() + " should have positive value, actual value: " + euroAmount);
throw new TransactionException("Credit transaction " + transactionToPost.getId() + " should have positive value, actual value: " + euroAmount);
}
senderBankAccount.getBalance().minus(Money.of(euroAmount));
emitterBankAccount.getBalance().minus(Money.of(euroAmount));
receiverBankAccount.getBalance().plus(Money.of(euroAmount));
logger.debug("add amount " + emitterBankAccount.getBalance() + " " + transactionToPost.getCurrency()
+ " to bank account" + receiverBankAccount.getId() + " from bank account " + emitterBankAccount.getId());
}

private BankAccount addTransaction(BankAccount currentSenderBankAccount, Transaction newTransaction) {
Optional<Transaction> optionalStoredTransaction = currentSenderBankAccount.getIssuedTransactions().stream()
.filter(transaction -> transaction.getId().equals(newTransaction.getId()))
@Override
public BankAccount addTransaction(Transaction transaction, BankAccount bankAccount) {
Optional<Transaction> optionalStoredTransaction = bankAccount.getIssuedTransactions().stream()
.filter(actualTransaction -> actualTransaction.getId().equals(transaction.getId()))
.findFirst();

if (optionalStoredTransaction.isPresent()) {
Transaction mergedTransaction = transactionService.mergeTransactions(optionalStoredTransaction.get(), newTransaction);
currentSenderBankAccount.getIssuedTransactions()
Transaction mergedTransaction = transactionService.mergeTransactions(optionalStoredTransaction.get(), transaction);
bankAccount.getIssuedTransactions()
.removeIf(existingTransaction -> existingTransaction.getId().equals(mergedTransaction.getId()));
currentSenderBankAccount.addTransaction(mergedTransaction);
bankAccount.addTransaction(mergedTransaction);
} else {
currentSenderBankAccount.addTransaction(newTransaction);
bankAccount.addTransaction(transaction);
}
return bankAccountRepository.update(currentSenderBankAccount);
return bankAccount;
}

@Override
public BankAccount updateBankAccount(BankAccount bankAccount) {
logger.debug("update bank account" + bankAccount.getId());
bankAccountValidator.validateBankAccount(bankAccount);
return bankAccountRepository.update(bankAccount);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.cdx.bas.application.transaction;

import com.cdx.bas.domain.transaction.Transaction;
import com.cdx.bas.domain.transaction.TransactionException;
import com.cdx.bas.domain.transaction.TransactionPersistencePort;
import com.cdx.bas.domain.transaction.TransactionStatus;
import com.cdx.bas.domain.transaction.validation.TransactionValidator;
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
import io.quarkus.panache.common.Parameters;
import io.quarkus.panache.common.Sort;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,81 +1,50 @@
package com.cdx.bas.application.transaction;

import com.cdx.bas.domain.bank.account.BankAccountServicePort;
import com.cdx.bas.application.bank.account.BankAccountServiceImpl;
import com.cdx.bas.domain.bank.account.*;
import com.cdx.bas.domain.transaction.*;
import com.cdx.bas.domain.transaction.validation.TransactionValidator;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.*;

import static com.cdx.bas.domain.transaction.TransactionStatus.*;
import static jakarta.transaction.Transactional.TxType.MANDATORY;
import static jakarta.transaction.Transactional.TxType.REQUIRES_NEW;

@RequestScoped
public class TransactionServiceImpl implements TransactionServicePort {

private static final Logger logger = LoggerFactory.getLogger(TransactionServiceImpl.class);

@Inject
TransactionPersistencePort transactionRepository;
TransactionPersistencePort transactionPersistencePort;

@Inject
BankAccountServicePort bankAccountService;

public List<Transaction> findAll() {
// return transactionRepository.findAll().stream().map(null);
return new ArrayList<>();
}

@Override
public void process(Transaction transaction) {
if (TransactionType.CREDIT.equals(transaction.getType())) {
logger.info("Transaction " + transaction.getId() + " processing...");
bankAccountService.deposit(transaction);
}
}
TransactionValidator transactionValidator;

@Override
@Transactional(value = MANDATORY)
public Transaction setAsOutstanding(Transaction transaction) {
if (UNPROCESSED.equals(transaction.getStatus())) {
transaction.setStatus(OUTSTANDING);
} else {
throw new TransactionException("Transaction is not longer unprocessed.");
}
return transactionRepository.update(transaction);
}

@Override
@Transactional(value = MANDATORY)
public Transaction setAsCompleted(Transaction completedTransaction, Map<String, String> metadata) {
setState(completedTransaction, metadata, COMPLETED);
return transactionRepository.update(completedTransaction);
}
@Inject
TransactionProcessingServicePort transactionProcessing;

@Override
@Transactional(value = MANDATORY)
public Transaction setAsError(Transaction erroredTransaction, Map<String, String> metadata) {
setState(erroredTransaction, metadata, ERROR);
return transactionRepository.update(erroredTransaction);
public Set<Transaction> getAll() {
return transactionPersistencePort.getAll();
}

@Override
@Transactional(value = MANDATORY)
public Transaction setAsRefused(Transaction refusedTransaction, Map<String, String> metadata) {
setState(refusedTransaction, metadata, REFUSED);
return transactionRepository.update(refusedTransaction);
public Set<Transaction> findAllByStatus(String status) throws IllegalArgumentException {
TransactionStatus transactionStatus = TransactionStatus.fromString(status);
return transactionPersistencePort.findAllByStatus(transactionStatus);
}

private void setState(Transaction refusedTransaction, Map<String, String> metadata, TransactionStatus status) {
refusedTransaction.setMetadata(metadata);
refusedTransaction.setStatus(status);
}
@Override
public void createTransaction(Transaction transaction) throws TransactionException {
transactionValidator.validateNewTransaction(transaction);
transactionPersistencePort.create(transaction);
}

public Transaction mergeTransactions(Transaction oldTransaction, Transaction newTransaction){
oldTransaction.setId(newTransaction.getId());
oldTransaction.setSenderAccountId(newTransaction.getSenderAccountId());
Expand All @@ -89,4 +58,11 @@ public Transaction mergeTransactions(Transaction oldTransaction, Transaction new
oldTransaction.setMetadata(newTransaction.getMetadata());
return oldTransaction;
}

@Override
public void process(Transaction transaction) {
if (TransactionType.CREDIT.equals(transaction.getType())) {
transactionProcessing.credit(transaction);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.cdx.bas.application.transaction;

import com.cdx.bas.domain.transaction.Transaction;
import com.cdx.bas.domain.transaction.TransactionException;
import com.cdx.bas.domain.transaction.TransactionStatus;
import com.cdx.bas.domain.transaction.TransactionStatusServicePort;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;

import java.util.Map;

import static com.cdx.bas.domain.transaction.TransactionStatus.OUTSTANDING;
import static com.cdx.bas.domain.transaction.TransactionStatus.UNPROCESSED;
import static jakarta.transaction.Transactional.TxType.MANDATORY;

@RequestScoped
public class TransactionStatusServiceImpl implements TransactionStatusServicePort {

@Inject
TransactionRepository transactionPersistencePort;

@Override
@Transactional(value = MANDATORY)
public Transaction setAsOutstanding(Transaction transaction) throws TransactionException {
if (UNPROCESSED.equals(transaction.getStatus())) {
transaction.setStatus(OUTSTANDING);
} else {
throw new TransactionException("Transaction is not longer unprocessed.");
}
return transactionPersistencePort.update(transaction);
}

@Override
@Transactional(value = MANDATORY)
public Transaction setStatus(Transaction transaction, TransactionStatus status, Map<String, String> metadata) throws TransactionException {
if (transaction == null) {
throw new TransactionException("Transaction is null.");
}
transaction.setStatus(status);
transaction.setMetadata(metadata);
return transactionPersistencePort.update(transaction);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.cdx.bas.application.transaction;

import com.cdx.bas.application.bank.account.BankAccountServiceImpl;
import com.cdx.bas.domain.bank.account.BankAccount;
import com.cdx.bas.domain.bank.account.BankAccountException;
import com.cdx.bas.domain.bank.account.BankAccountPersistencePort;
import com.cdx.bas.domain.bank.account.BankAccountServicePort;
import com.cdx.bas.domain.transaction.Transaction;
import com.cdx.bas.domain.transaction.TransactionException;
import com.cdx.bas.domain.transaction.TransactionProcessingServicePort;
import com.cdx.bas.domain.transaction.TransactionStatusServicePort;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;

import static com.cdx.bas.domain.transaction.TransactionStatus.*;
import static com.cdx.bas.domain.transaction.TransactionStatus.REFUSED;
import static jakarta.transaction.Transactional.TxType.REQUIRES_NEW;

@RequestScoped
public class TransactionProcessingServiceImpl implements TransactionProcessingServicePort {

private static final Logger logger = LoggerFactory.getLogger(BankAccountServiceImpl.class);

@Inject
TransactionStatusServicePort transactionStatusService;

@Inject
BankAccountServicePort bankAccountService;


@Override
@Transactional(value = REQUIRES_NEW)
public Transaction credit(Transaction transaction) {
logger.info("Transaction " + transaction.getId() + " processing...");

try {
BankAccount emitterBankAccount = bankAccountService.findBankAccount(transaction.getSenderAccountId());
BankAccount receiverBankAccount = bankAccountService.findBankAccount(transaction.getReceiverAccountId());
Transaction currentTransaction = transactionStatusService.setAsOutstanding(transaction);

logger.info("Process credit transaction " + currentTransaction.getId()
+ " from bank account " + currentTransaction.getSenderAccountId()
+ " with amount " + currentTransaction.getAmount()
+ " to bank account " + currentTransaction.getReceiverAccountId());

Map<String, String> metadata = new HashMap<>();
metadata.put("sender_amount_before", emitterBankAccount.getBalance().getAmount().toString());
metadata.put("receiver_amount_before", receiverBankAccount.getBalance().getAmount().toString());
bankAccountService.postTransaction(currentTransaction, emitterBankAccount, receiverBankAccount);
metadata.put("sender_amount_after", emitterBankAccount.getBalance().getAmount().toString());
metadata.put("receiver_amount_after", receiverBankAccount.getBalance().getAmount().toString());
Transaction completedTransaction = transactionStatusService.setStatus(currentTransaction, COMPLETED, metadata);
BankAccount updatedEmitterBankAccount = bankAccountService.addTransaction(completedTransaction, emitterBankAccount);
bankAccountService.updateBankAccount(receiverBankAccount);

logger.info("Bank account " + updatedEmitterBankAccount.getId() + " deposit transaction " + + currentTransaction.getId() + " completed.");
return completedTransaction;
} catch (NoSuchElementException exception) {
logger.error("Transaction " + transaction.getId() + " deposit error for amount "+ transaction.getAmount() + ": " + exception.getMessage());
Map<String, String> metadata = Map.of("error", exception.getMessage());
return transactionStatusService.setStatus(transaction, ERROR, metadata);
} catch (TransactionException exception) {
logger.error("Transaction " + transaction.getId() + " of " + transaction.getAmount() + " is invalid.");
Map<String, String> metadata = Map.of("error", exception.getMessage());
return transactionStatusService.setStatus(transaction, REFUSED, metadata);
} catch (BankAccountException exception) {
logger.error("Transaction " + transaction.getId() + " deposit refused for amount "+ transaction.getAmount() + ": " + exception.getMessage());
Map<String, String> metadata = Map.of("error", exception.getMessage());
return transactionStatusService.setStatus(transaction, REFUSED, metadata);
}
}

@Override
public Transaction debit(Transaction transaction) {
//TODO
return null;
}

@Override
public Transaction deposit(Transaction transaction) {
//TODO
return null;
}

@Override
public Transaction withdraw(Transaction transaction) {
//TODO
return null;
}
}
Loading

0 comments on commit 6b72829

Please sign in to comment.