diff --git a/src/main/java/com/devexperts/ApplicationRunner.java b/src/main/java/com/devexperts/ApplicationRunner.java index b6400a4..8cf8f9f 100644 --- a/src/main/java/com/devexperts/ApplicationRunner.java +++ b/src/main/java/com/devexperts/ApplicationRunner.java @@ -1,7 +1,7 @@ package com.devexperts; import com.devexperts.service.AccountService; -import com.devexperts.service.AccountServiceImpl; +import com.devexperts.service.impl.AccountServiceImpl; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @@ -12,8 +12,4 @@ public static void main(String[] args) { SpringApplication.run(ApplicationRunner.class, args); } - @Bean - AccountService accountService() { - return new AccountServiceImpl(); - } } diff --git a/src/main/java/com/devexperts/account/Account.java b/src/main/java/com/devexperts/account/Account.java index fb2a3af..5b544d7 100644 --- a/src/main/java/com/devexperts/account/Account.java +++ b/src/main/java/com/devexperts/account/Account.java @@ -32,4 +32,5 @@ public Double getBalance() { public void setBalance(Double balance) { this.balance = balance; } + } diff --git a/src/main/java/com/devexperts/dto/ErrorDto.java b/src/main/java/com/devexperts/dto/ErrorDto.java new file mode 100644 index 0000000..136691b --- /dev/null +++ b/src/main/java/com/devexperts/dto/ErrorDto.java @@ -0,0 +1,44 @@ +package com.devexperts.dto; + +import org.springframework.http.HttpStatus; + +import java.time.LocalDateTime; + +public class ErrorDto { + + private String message; + + private HttpStatus status; + + private LocalDateTime time; + + public ErrorDto(String message, HttpStatus status, LocalDateTime time) { + this.message = message; + this.status = status; + this.time = time; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public HttpStatus getStatus() { + return status; + } + + public void setStatus(HttpStatus status) { + this.status = status; + } + + public LocalDateTime getTime() { + return time; + } + + public void setTime(LocalDateTime time) { + this.time = time; + } +} diff --git a/src/main/java/com/devexperts/dto/TransferMoneyDto.java b/src/main/java/com/devexperts/dto/TransferMoneyDto.java new file mode 100644 index 0000000..1e4d57c --- /dev/null +++ b/src/main/java/com/devexperts/dto/TransferMoneyDto.java @@ -0,0 +1,42 @@ +package com.devexperts.dto; + +import javax.validation.constraints.NotNull; + +public class TransferMoneyDto { + + @NotNull + private Long sourceId; + @NotNull + private Long targetId; + private double amount; + + public TransferMoneyDto(Long sourceId, Long targetId, double amount) { + this.sourceId = sourceId; + this.targetId = targetId; + this.amount = amount; + } + + public Long getSourceId() { + return sourceId; + } + + public void setSourceId(Long sourceId) { + this.sourceId = sourceId; + } + + public Long getTargetId() { + return targetId; + } + + public void setTargetId(Long targetId) { + this.targetId = targetId; + } + + public double getAmount() { + return amount; + } + + public void setAmount(double amount) { + this.amount = amount; + } +} diff --git a/src/main/java/com/devexperts/exception/BadRequestException.java b/src/main/java/com/devexperts/exception/BadRequestException.java new file mode 100644 index 0000000..16d952a --- /dev/null +++ b/src/main/java/com/devexperts/exception/BadRequestException.java @@ -0,0 +1,8 @@ +package com.devexperts.exception; + +public class BadRequestException extends RuntimeException { + + public BadRequestException(String msg) { + super(msg); + } +} \ No newline at end of file diff --git a/src/main/java/com/devexperts/exception/InsufficentFundsException.java b/src/main/java/com/devexperts/exception/InsufficentFundsException.java new file mode 100644 index 0000000..2fd99e2 --- /dev/null +++ b/src/main/java/com/devexperts/exception/InsufficentFundsException.java @@ -0,0 +1,8 @@ +package com.devexperts.exception; + +public class InsufficentFundsException extends RuntimeException { + + public InsufficentFundsException(String msg) { + super(msg); + } +} diff --git a/src/main/java/com/devexperts/exception/NotFoundException.java b/src/main/java/com/devexperts/exception/NotFoundException.java new file mode 100644 index 0000000..6c17f6a --- /dev/null +++ b/src/main/java/com/devexperts/exception/NotFoundException.java @@ -0,0 +1,9 @@ +package com.devexperts.exception; + +public class NotFoundException extends RuntimeException { + + public NotFoundException(String msg) { + super(msg); + } + +} diff --git a/src/main/java/com/devexperts/handler/GlobalExceptionHandler.java b/src/main/java/com/devexperts/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..6f14240 --- /dev/null +++ b/src/main/java/com/devexperts/handler/GlobalExceptionHandler.java @@ -0,0 +1,57 @@ +package com.devexperts.handler; + +import com.devexperts.dto.ErrorDto; +import com.devexperts.exception.BadRequestException; +import com.devexperts.exception.InsufficentFundsException; +import com.devexperts.exception.NotFoundException; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.HttpStatus; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.time.LocalDateTime; +import java.util.Map; +import java.util.stream.Collectors; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(BadRequestException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorDto handleBadRequests(BadRequestException exception) { + return convertExceptionToErrorDto(HttpStatus.BAD_REQUEST, exception.getMessage()); + } + + @ExceptionHandler(NotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ErrorDto handleBadRequests(NotFoundException exception) { + return convertExceptionToErrorDto(HttpStatus.NOT_FOUND, exception.getMessage()); + } + + @ExceptionHandler(InsufficentFundsException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorDto handleBadRequests(InsufficentFundsException exception) { + return convertExceptionToErrorDto(HttpStatus.INTERNAL_SERVER_ERROR, exception.getMessage()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorDto handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) { + final Map errorMessages = exception.getBindingResult().getFieldErrors().stream() + .collect(Collectors.toMap(FieldError::getField, DefaultMessageSourceResolvable::getDefaultMessage)); + return convertExceptionToErrorDto(HttpStatus.BAD_REQUEST, errorMessages.toString()); + } + + @ExceptionHandler(NumberFormatException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorDto handleMethodArgumentNotValidException(NumberFormatException exception) { + return convertExceptionToErrorDto(HttpStatus.BAD_REQUEST, exception.getMessage()); + } + + private static ErrorDto convertExceptionToErrorDto(final HttpStatus status, final String message) { + return new ErrorDto(message, status, LocalDateTime.now()); + } +} diff --git a/src/main/java/com/devexperts/rest/AbstractAccountController.java b/src/main/java/com/devexperts/rest/AbstractAccountController.java index dea5a3c..3691793 100644 --- a/src/main/java/com/devexperts/rest/AbstractAccountController.java +++ b/src/main/java/com/devexperts/rest/AbstractAccountController.java @@ -1,7 +1,8 @@ package com.devexperts.rest; +import com.devexperts.dto.TransferMoneyDto; import org.springframework.http.ResponseEntity; public abstract class AbstractAccountController { - abstract ResponseEntity transfer(long sourceId, long targetId, double amount); + abstract ResponseEntity transfer(TransferMoneyDto transferMoneyDto); } diff --git a/src/main/java/com/devexperts/rest/AccountController.java b/src/main/java/com/devexperts/rest/AccountController.java index b300282..963035a 100644 --- a/src/main/java/com/devexperts/rest/AccountController.java +++ b/src/main/java/com/devexperts/rest/AccountController.java @@ -1,14 +1,32 @@ package com.devexperts.rest; +import com.devexperts.dto.TransferMoneyDto; +import com.devexperts.service.AccountService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import javax.validation.Valid; + @RestController @RequestMapping("/api") public class AccountController extends AbstractAccountController { - public ResponseEntity transfer(long sourceId, long targetId, double amount) { - return null; + private final AccountService service; + + @Autowired + public AccountController(AccountService service) { + this.service = service; } + + @PostMapping("operations/transfer") + public ResponseEntity transfer(@Valid TransferMoneyDto transferMoneyDto) { + service.transfer(transferMoneyDto); + return new ResponseEntity<>(HttpStatus.OK); + } + } diff --git a/src/main/java/com/devexperts/service/AccountService.java b/src/main/java/com/devexperts/service/AccountService.java index f287597..788b825 100644 --- a/src/main/java/com/devexperts/service/AccountService.java +++ b/src/main/java/com/devexperts/service/AccountService.java @@ -1,6 +1,7 @@ package com.devexperts.service; import com.devexperts.account.Account; +import com.devexperts.dto.TransferMoneyDto; public interface AccountService { @@ -29,9 +30,7 @@ public interface AccountService { /** * Transfers given amount of money from source account to target account * - * @param source account to transfer money from - * @param target account to transfer money to - * @param amount dollar amount to transfer + * @param dto the ids of both accounts and the amount to be transferred * */ - void transfer(Account source, Account target, double amount); + void transfer(TransferMoneyDto dto); } diff --git a/src/main/java/com/devexperts/service/AccountServiceImpl.java b/src/main/java/com/devexperts/service/AccountServiceImpl.java deleted file mode 100644 index 91261ba..0000000 --- a/src/main/java/com/devexperts/service/AccountServiceImpl.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.devexperts.service; - -import com.devexperts.account.Account; -import com.devexperts.account.AccountKey; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.List; - -@Service -public class AccountServiceImpl implements AccountService { - - private final List accounts = new ArrayList<>(); - - @Override - public void clear() { - accounts.clear(); - } - - @Override - public void createAccount(Account account) { - accounts.add(account); - } - - @Override - public Account getAccount(long id) { - return accounts.stream() - .filter(account -> account.getAccountKey() == AccountKey.valueOf(id)) - .findAny() - .orElse(null); - } - - @Override - public void transfer(Account source, Account target, double amount) { - //do nothing for now - } -} diff --git a/src/main/java/com/devexperts/service/impl/AccountServiceImpl.java b/src/main/java/com/devexperts/service/impl/AccountServiceImpl.java new file mode 100644 index 0000000..d8770be --- /dev/null +++ b/src/main/java/com/devexperts/service/impl/AccountServiceImpl.java @@ -0,0 +1,63 @@ +package com.devexperts.service.impl; + +import com.devexperts.account.Account; +import com.devexperts.account.AccountKey; +import com.devexperts.dto.TransferMoneyDto; +import com.devexperts.exception.BadRequestException; +import com.devexperts.exception.NotFoundException; +import com.devexperts.service.AccountService; +import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpClientErrorException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class AccountServiceImpl implements AccountService { + + private final Map accounts = new HashMap<>(); + + @Override + public void clear() { + accounts.clear(); + } + + @Override + public void createAccount(Account account) { + accounts.put(account.getAccountKey(), account); + } + + @Override + public Account getAccount(long id) { + return accounts.get(AccountKey.valueOf(id)); + } + + @Override + public synchronized void transfer(TransferMoneyDto dto) { + Account source = accounts.get(AccountKey.valueOf(dto.getSourceId())); + Account target = accounts.get(AccountKey.valueOf(dto.getTargetId())); + transfer(source, target, dto.getAmount()); + } + + public void transfer(Account source, Account target, double amount) { + if(source == null) { + throw new NotFoundException("The source account doesnt exist"); + } + if(target == null) { + throw new NotFoundException("The target account doesnt exist"); + } + if(source.equals(target)) { + throw new BadRequestException("Its pointless to transfer money between the same account"); + } + if(amount < 0) { + throw new BadRequestException("The amount can't be a negative value"); + } + if(source.getBalance() < amount) { + throw new BadRequestException("The source account doesnt have sufficient amount"); + } + source.setBalance(source.getBalance() - amount); + target.setBalance(target.getBalance() + amount); + } +} diff --git a/src/main/resources/sql/data/accounts.sql b/src/main/resources/sql/data/accounts.sql new file mode 100644 index 0000000..394cb88 --- /dev/null +++ b/src/main/resources/sql/data/accounts.sql @@ -0,0 +1 @@ +CREATE TABLE IF NOT EXISTS accounts (ID int, FIRST_NAME varchar(255), LAST_NAME varchar(255), BALANCE DECIMAL); \ No newline at end of file diff --git a/src/main/resources/sql/data/select.sql b/src/main/resources/sql/data/select.sql new file mode 100644 index 0000000..c60a648 --- /dev/null +++ b/src/main/resources/sql/data/select.sql @@ -0,0 +1 @@ +SELECT * FROM accounts AS ACC INNER JOIN transfers AS TR on ACC.ID = TR.SOURCE_ID WHERE TRANSFER_TIME >= '2019-01-01' GROUP BY ACC.ID HAVING SUM(TR.AMOUNT) > 1000 \ No newline at end of file diff --git a/src/main/resources/sql/data/transfers.sql b/src/main/resources/sql/data/transfers.sql new file mode 100644 index 0000000..e181ab7 --- /dev/null +++ b/src/main/resources/sql/data/transfers.sql @@ -0,0 +1 @@ +CREATE TABLE IF NOT EXISTS transfers (ID int, SOURCE_ID int, TARGET_ID int, AMOUNT DECIMAL, TRANSFER_TIME TIMESTAMP); \ No newline at end of file