diff --git a/infrastructure/pom.xml b/infrastructure/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..ed05eb43ce865f9e0cbaab0153ce06fad080b7db --- /dev/null +++ b/infrastructure/pom.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-parent</artifactId> + <version>3.2.4</version> + <relativePath/> + </parent> + + + <groupId>cz.muni.pa165.banking</groupId> + <artifactId>infrastructure</artifactId> + <version>1.0-SNAPSHOT</version> + + <properties> + <maven.compiler.source>21</maven.compiler.source> + <maven.compiler.target>21</maven.compiler.target> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + + <!-- Only exact dependencies to eliminate transitive dependencies --> + <dependencies> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-web</artifactId> + </dependency> + </dependencies> +</project> \ No newline at end of file diff --git a/infrastructure/src/main/java/cz/muni/pa165/banking/exception/CustomException.java b/infrastructure/src/main/java/cz/muni/pa165/banking/exception/CustomException.java new file mode 100644 index 0000000000000000000000000000000000000000..aa863e1e7e5e72aa895eacbf925d13528084b27c --- /dev/null +++ b/infrastructure/src/main/java/cz/muni/pa165/banking/exception/CustomException.java @@ -0,0 +1,33 @@ +package cz.muni.pa165.banking.exception; + +import org.springframework.http.HttpStatus; + +import java.util.LinkedHashMap; +import java.util.Map; + +public abstract class CustomException extends RuntimeException { + + public final Map<String, Object> getBody() { + Map<String, Object> body = new LinkedHashMap<>(); + body.put("status", getStatus()); + body.put("code", getStatus().value()); + body.put("exception", this.getClass().getSimpleName()); + body.put("message", getExceptionMessage()); + body.put("cause", getExceptionCause()); + + if (getDetail() != null) { + body.put("detail", getDetail()); + } + + return body; + } + + public abstract HttpStatus getStatus(); + + abstract String getExceptionMessage(); + + abstract String getExceptionCause(); + + abstract Object getDetail(); + +} diff --git a/infrastructure/src/main/java/cz/muni/pa165/banking/exception/CustomExceptionHandler.java b/infrastructure/src/main/java/cz/muni/pa165/banking/exception/CustomExceptionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7defee0249431b7aed2428fc6a415aedcc71dcf9 --- /dev/null +++ b/infrastructure/src/main/java/cz/muni/pa165/banking/exception/CustomExceptionHandler.java @@ -0,0 +1,20 @@ +package cz.muni.pa165.banking.exception; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +/** + * Handles custom exceptions thrown in application. The handler overrides the controller return value, + * evaluating the assigned status code and response body. + * The response body is evaluated as a JSON object, containing defined information such as the status code, + * exception type, message, cause and an optional detail regarding the exception. + */ +public class CustomExceptionHandler { + + @ExceptionHandler(CustomException.class) + public ResponseEntity<Object> handleEntityNotFound(CustomException ex) { + return new ResponseEntity<>(ex.getBody(), ex.getStatus()); + } + +} diff --git a/infrastructure/src/main/java/cz/muni/pa165/banking/exception/EntityNotFoundException.java b/infrastructure/src/main/java/cz/muni/pa165/banking/exception/EntityNotFoundException.java new file mode 100644 index 0000000000000000000000000000000000000000..a7c55b32422de0b3d188faf33dbca87b73aec7b2 --- /dev/null +++ b/infrastructure/src/main/java/cz/muni/pa165/banking/exception/EntityNotFoundException.java @@ -0,0 +1,42 @@ +package cz.muni.pa165.banking.exception; + +import org.springframework.http.HttpStatus; + +public class EntityNotFoundException extends CustomException { + + private final String cause; + + private final Object detail; + + public EntityNotFoundException(String cause) { + this.cause = cause; + detail = null; + } + + public EntityNotFoundException(String cause, Object detail) { + this.cause = cause; + this.detail = detail; + } + + @Override + public HttpStatus getStatus() { + return HttpStatus.NOT_FOUND; + } + + @Override + String getExceptionMessage() { + return "Entity not present in repository"; + } + + @Override + String getExceptionCause() { + return cause; + } + + @Override + Object getDetail() { + return detail; + } + + +} diff --git a/infrastructure/src/main/java/cz/muni/pa165/banking/exception/ServerError.java b/infrastructure/src/main/java/cz/muni/pa165/banking/exception/ServerError.java new file mode 100644 index 0000000000000000000000000000000000000000..701242d221f5c972a78bec6abb5ccf5a8678ffb7 --- /dev/null +++ b/infrastructure/src/main/java/cz/muni/pa165/banking/exception/ServerError.java @@ -0,0 +1,40 @@ +package cz.muni.pa165.banking.exception; + +import org.springframework.http.HttpStatus; + +public class ServerError extends CustomException { + + private final String cause; + + private final Object detail; + + public ServerError(String cause) { + this.cause = cause; + detail = null; + } + + public ServerError(String cause, Object detail) { + this.cause = cause; + this.detail = detail; + } + @Override + public HttpStatus getStatus() { + return HttpStatus.INTERNAL_SERVER_ERROR; + } + + @Override + String getExceptionMessage() { + return "Internal Server Error"; + } + + @Override + String getExceptionCause() { + return cause; + } + + @Override + Object getDetail() { + return detail; + } + +} diff --git a/infrastructure/src/main/java/cz/muni/pa165/banking/exception/UnexpectedValueException.java b/infrastructure/src/main/java/cz/muni/pa165/banking/exception/UnexpectedValueException.java new file mode 100644 index 0000000000000000000000000000000000000000..201cfeb2fc1b1e82fa7f5c685e57ab0ecb7baca0 --- /dev/null +++ b/infrastructure/src/main/java/cz/muni/pa165/banking/exception/UnexpectedValueException.java @@ -0,0 +1,41 @@ +package cz.muni.pa165.banking.exception; + +import org.springframework.http.HttpStatus; + +public class UnexpectedValueException extends CustomException { + + private final String cause; + + private final Object detail; + + public UnexpectedValueException(String cause, Object detail) { + this.cause = cause; + this.detail = detail; + } + + public UnexpectedValueException(String cause) { + this.cause = cause; + this.detail = null; + } + + @Override + public HttpStatus getStatus() { + return HttpStatus.CONFLICT; + } + + @Override + String getExceptionMessage() { + return "Unexpected value state"; + } + + @Override + String getExceptionCause() { + return cause; + } + + @Override + Object getDetail() { + return detail; + } + +} diff --git a/infrastructure/src/main/java/cz/muni/pa165/banking/exception/UnsupportedDataTypeException.java b/infrastructure/src/main/java/cz/muni/pa165/banking/exception/UnsupportedDataTypeException.java new file mode 100644 index 0000000000000000000000000000000000000000..68ca401726404452b180d1c8025f665029a01c00 --- /dev/null +++ b/infrastructure/src/main/java/cz/muni/pa165/banking/exception/UnsupportedDataTypeException.java @@ -0,0 +1,32 @@ +package cz.muni.pa165.banking.exception; + +import org.springframework.http.HttpStatus; + +public class UnsupportedDataTypeException extends CustomException { + + private final String cause; + + public UnsupportedDataTypeException(String cause) { + this.cause = cause; + } + + @Override + public HttpStatus getStatus() { + return HttpStatus.BAD_REQUEST; + } + + @Override + String getExceptionMessage() { + return "Unsupported data type"; + } + + @Override + String getExceptionCause() { + return cause; + } + + @Override + Object getDetail() { + return null; + } +} diff --git a/infrastructure/src/main/java/cz/muni/pa165/banking/security/AuthService.java b/infrastructure/src/main/java/cz/muni/pa165/banking/security/AuthService.java new file mode 100644 index 0000000000000000000000000000000000000000..2915be0bedadb215f563f066106c7b73f1225ce9 --- /dev/null +++ b/infrastructure/src/main/java/cz/muni/pa165/banking/security/AuthService.java @@ -0,0 +1,5 @@ +package cz.muni.pa165.banking.security; + +// TODO Milestone2 +public class AuthService { +} diff --git a/transaction-processor/pom.xml b/transaction-processor/pom.xml index b5c4c47404345a1ab4729d6d8172c16261bebef2..153a12cc2023a3e7c26e6a32d4006fb34c0ecb2c 100644 --- a/transaction-processor/pom.xml +++ b/transaction-processor/pom.xml @@ -21,6 +21,11 @@ <maven.compiler.target>21</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <!-- External project artifact versions --> + <banking-infrastructure.version>1.0-SNAPSHOT</banking-infrastructure.version> + <banking-openapi.version>1.0-SNAPSHOT</banking-openapi.version> + + <spring.version>3.2.4</spring.version> <openapi-generator.version>6.6.0</openapi-generator.version> <org.mapstruct.version>1.5.5.Final</org.mapstruct.version> @@ -28,6 +33,12 @@ </properties> <dependencies> + <dependency> + <groupId>cz.muni.pa165.banking</groupId> + <artifactId>infrastructure</artifactId> + <version>${banking-infrastructure.version}</version> + </dependency> + <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/configuration/BeanRegistry.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/configuration/BeanRegistry.java new file mode 100644 index 0000000000000000000000000000000000000000..b9f7585ae9645e710a3808dfb99344b635f3316f --- /dev/null +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/configuration/BeanRegistry.java @@ -0,0 +1,15 @@ +package cz.muni.pa165.banking.application.configuration; + +import cz.muni.pa165.banking.exception.CustomExceptionHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +@Component +public class BeanRegistry { + + @Bean + public CustomExceptionHandler exceptionHandler() { + return new CustomExceptionHandler(); + } + +} diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/facade/TransactionFacade.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/facade/TransactionFacade.java index 0d96a0232550fb78874430c360a843ebb0429006..f92c60da0259489b2e7a2d6e4dd9a1be6972cd1c 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/facade/TransactionFacade.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/facade/TransactionFacade.java @@ -4,11 +4,15 @@ import cz.muni.pa165.banking.application.mapper.DtoMapper; import cz.muni.pa165.banking.application.service.TransactionService; import cz.muni.pa165.banking.domain.process.Process; import cz.muni.pa165.banking.domain.transaction.Transaction; +import cz.muni.pa165.banking.exception.EntityNotFoundException; import cz.muni.pa165.banking.transaction.processor.dto.ProcessDto; import cz.muni.pa165.banking.transaction.processor.dto.TransactionDto; +import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; -@Service +import java.util.List; + +@Component public class TransactionFacade { private final TransactionService service; diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/messaging/ProcessProducer.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/messaging/ProcessProducer.java index b091645bf840af5106480d15cef5ec928fab8344..44cdc381f250ed25160a6138efbbb89cf074ac8a 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/messaging/ProcessProducer.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/messaging/ProcessProducer.java @@ -1,12 +1,14 @@ package cz.muni.pa165.banking.application.messaging; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import cz.muni.pa165.banking.domain.messaging.MessageProducer; import cz.muni.pa165.banking.domain.messaging.ProcessRequest; +import cz.muni.pa165.banking.exception.ServerError; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import java.util.Map; + @Service public class ProcessProducer implements MessageProducer { @@ -24,9 +26,20 @@ public class ProcessProducer implements MessageProducer { @Override - public void send(ProcessRequest data) throws JsonProcessingException { - String dataAsJsonString = mapper.writeValueAsString(data); -// template.convertAndSend(EXCHANGE_NAME, "", data); + public void send(ProcessRequest data) { + String dataAsJsonString; + try { + dataAsJsonString = mapper.writeValueAsString(data); + } catch (Exception e) { + throw new ServerError( + "Unable to map ProcessRequest to String", + Map.of( + "causeLocation", "cz.muni.pa165.banking.application.messaging.ProcessProducer.send", + "invalidObject", data.toString() + ) + ); + } +// template.convertAndSend(EXCHANGE_NAME, "", dataAsJsonString); } } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/TransactionService.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/TransactionService.java index 3376d51368934876900ba9223228f3e31fdb4952..51810f09bb4329b7bb53907e6ad56194fa5d5cf9 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/TransactionService.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/TransactionService.java @@ -27,11 +27,7 @@ public class TransactionService { @Transactional(rollbackFor = Exception.class) public Process createProcessForTransaction(Transaction newTransaction) { ProcessFactory factory = new ProcessFactory(processTransactionRepository, processRepository); - try { - return factory.create(newTransaction, processProducer); - } catch (Exception e) { - throw new RuntimeException(e.getMessage()); - } + return factory.create(newTransaction, processProducer); } } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/messaging/MessageProducer.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/messaging/MessageProducer.java index f5f870bb91ea51dc6460ad18d7d97e27c56f04b2..1359e6e58be7987dfea6132df75a8d888d604633 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/messaging/MessageProducer.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/messaging/MessageProducer.java @@ -1,9 +1,7 @@ package cz.muni.pa165.banking.domain.messaging; -import com.fasterxml.jackson.core.JsonProcessingException; - public interface MessageProducer { - void send(ProcessRequest data) throws JsonProcessingException; + void send(ProcessRequest data); } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/messaging/ProcessRequest.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/messaging/ProcessRequest.java index 79bf529ffaec69dd60d82b759b2e5c990998ec3d..dece4563799f49937d07416f49319310d3a9adf4 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/messaging/ProcessRequest.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/messaging/ProcessRequest.java @@ -5,4 +5,13 @@ import cz.muni.pa165.banking.domain.transaction.TransactionType; import java.util.UUID; public record ProcessRequest(UUID uuid, TransactionType type) { + + @Override + public String toString() { + return "ProcessRequest{" + + "uuid=" + uuid + + ", type=" + type + + '}'; + } + } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/money/CurrencyConverter.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/money/CurrencyConverter.java index b78bbba6bee233a732cd75acd47076e2455c8be5..e3b709e46080f864a053a855320c58643160b9a3 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/money/CurrencyConverter.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/money/CurrencyConverter.java @@ -1,6 +1,7 @@ package cz.muni.pa165.banking.domain.money; import cz.muni.pa165.banking.domain.money.exchange.ExchangeRateService; +import cz.muni.pa165.banking.exception.UnsupportedDataTypeException; import java.math.BigDecimal; import java.util.Currency; @@ -21,7 +22,7 @@ public class CurrencyConverter { */ public BigDecimal convertTo(Currency target, Money amount) { if (!Currency.getAvailableCurrencies().contains(target)) { - throw new RuntimeException("Unsupported target currency"); + throw new UnsupportedDataTypeException("Unsupported target currency"); } BigDecimal rate = BigDecimal.ONE; if (!amount.getCurrency().equals(target)) { diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/ProcessFactory.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/ProcessFactory.java index 09613c551ecc77be613d4b4e459769e1aae1b978..a30ddd28aa3d7f526057fac4ba0f62c0461bc243 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/ProcessFactory.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/ProcessFactory.java @@ -19,7 +19,7 @@ public class ProcessFactory { } - public Process create(Transaction transaction, MessageProducer messageProducer) throws Exception { + public Process create(Transaction transaction, MessageProducer messageProducer) { Process newProcess = new Process(); processRepository.save(newProcess); diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/DepositHandler.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/DepositHandler.java index 49ff9b92b0a3b712d896075e7e264ad62c383d85..10559e6cb9a54ca61f17070999592d7155489244 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/DepositHandler.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/DepositHandler.java @@ -6,6 +6,7 @@ import cz.muni.pa165.banking.domain.process.ProcessTransaction; import cz.muni.pa165.banking.domain.process.repository.HandlerMBeanRepository; import cz.muni.pa165.banking.domain.process.repository.ProcessRepository; import cz.muni.pa165.banking.domain.remote.AccountService; +import cz.muni.pa165.banking.exception.EntityNotFoundException; import java.math.BigDecimal; import java.util.Currency; @@ -23,7 +24,7 @@ class DepositHandler extends ProcessHandler { Account account = processTransaction.getSource(); AccountService accountService = beans.accountService(); if (!accountService.isValid(account)) { - throw new RuntimeException( + throw new EntityNotFoundException( String.format("Account with number {%s} does not exist", account.getAccountNumber()) ); } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/ProcessHandler.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/ProcessHandler.java index 8b614481907981e2da69ce85273470cb8e7e9550..51df0c42f4d89bc05fe8f3b757bcb0c24804f944 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/ProcessHandler.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/ProcessHandler.java @@ -6,6 +6,7 @@ import cz.muni.pa165.banking.domain.process.repository.HandlerMBeanRepository; import cz.muni.pa165.banking.domain.process.repository.ProcessRepository; import cz.muni.pa165.banking.domain.process.status.Status; import cz.muni.pa165.banking.domain.process.status.StatusInformation; +import cz.muni.pa165.banking.exception.UnexpectedValueException; import java.time.Instant; import java.util.UUID; @@ -39,10 +40,13 @@ abstract class ProcessHandler { private void validateProcess(Process process) { if (process.getStatus().equals(Status.FAILED)) { - throw new RuntimeException("Process already finalized, ended with failure: " + process.getStatusInformation()); + throw new UnexpectedValueException( + "Process already closed, ended with failure", + "Failure information: " + process.getStatusInformation() + ); } if (process.getStatus().equals(Status.PROCESSED)) { - throw new RuntimeException("Process already finalized"); + throw new UnexpectedValueException("Process already finalized, ended successfully", process.getStatusInformation()); } } diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/ProcessHandlerGateway.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/ProcessHandlerGateway.java index 2cbe773e372ef4124ac505f5b1d997816078474b..4b7dd6fa44cd8d25f238009a7b6948587c8c1e55 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/ProcessHandlerGateway.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/ProcessHandlerGateway.java @@ -3,6 +3,7 @@ package cz.muni.pa165.banking.domain.process.handler; import cz.muni.pa165.banking.domain.process.repository.HandlerMBeanRepository; import cz.muni.pa165.banking.domain.process.repository.ProcessRepository; import cz.muni.pa165.banking.domain.transaction.TransactionType; +import cz.muni.pa165.banking.exception.EntityNotFoundException; import java.util.UUID; @@ -10,7 +11,7 @@ public class ProcessHandlerGateway { public void handle(UUID processUuid, TransactionType type, ProcessRepository repository, HandlerMBeanRepository beans) { if (!repository.idExists(processUuid.toString())) { - throw new RuntimeException(String.format("Process with uuid {%s} not found", processUuid)); + throw new EntityNotFoundException(String.format("Process with uuid {%s} not found", processUuid)); } ProcessHandler handler = switch (type) { diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/WithdrawHandler.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/WithdrawHandler.java index 124edfe7120760f171a172d2dc9744ddf6350c4a..540bab51e0f89bbd28d51e792819903530abdc7d 100644 --- a/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/WithdrawHandler.java +++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/domain/process/handler/WithdrawHandler.java @@ -7,6 +7,7 @@ import cz.muni.pa165.banking.domain.process.ProcessTransaction; import cz.muni.pa165.banking.domain.process.repository.HandlerMBeanRepository; import cz.muni.pa165.banking.domain.process.repository.ProcessRepository; import cz.muni.pa165.banking.domain.remote.AccountService; +import cz.muni.pa165.banking.exception.EntityNotFoundException; import java.math.BigDecimal; import java.util.Currency; @@ -24,14 +25,14 @@ public class WithdrawHandler extends ProcessHandler { Account account = processTransaction.getSource(); AccountService accountService = beans.accountService(); if (!accountService.isValid(account)) { - throw new RuntimeException( + throw new EntityNotFoundException( String.format("Account with number {%s} does not exist", account.getAccountNumber()) ); } Money money = processTransaction.getAmount(); if (!accountService.accountHasSufficientFunds(account, money.getAmount())) { - throw new RuntimeException( + throw new EntityNotFoundException( String.format( "Account with number {%s} does not have sufficient funds for withdrawal of %s %s", account.getAccountNumber(),