Skip to content
This repository has been archived by the owner on Jan 17, 2022. It is now read-only.

Andrey Bodyul tasks #39

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/java/com/devexperts/ApplicationRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public static void main(String[] args) {
SpringApplication.run(ApplicationRunner.class, args);
}

@Bean
@Bean(initMethod="init")
AccountService accountService() {
return new AccountServiceImpl();
}
Expand Down
17 changes: 16 additions & 1 deletion src/main/java/com/devexperts/account/AccountKey.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.devexperts.account;

import java.util.Objects;

/**
* Unique Account identifier
*
Expand All @@ -10,11 +12,24 @@
public class AccountKey {
private final long accountId;

private AccountKey(long accountId) {
public AccountKey(long accountId) {
this.accountId = accountId;
}

public static AccountKey valueOf(long accountId) {
return new AccountKey(accountId);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AccountKey that = (AccountKey) o;
return accountId == that.accountId;
}

@Override
public int hashCode() {
return Objects.hash(accountId);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.devexperts.rest;

import com.devexperts.service.exception.AccountBalanceException;
import com.devexperts.service.exception.AccountNotFoundException;

import org.springframework.http.ResponseEntity;

public abstract class AbstractAccountController {
abstract ResponseEntity<Void> transfer(long sourceId, long targetId, double amount);
abstract ResponseEntity<Void> transfer(long sourceId, long targetId, double amount)
throws AccountNotFoundException, AccountBalanceException;
}
23 changes: 21 additions & 2 deletions src/main/java/com/devexperts/rest/AccountController.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
package com.devexperts.rest;

import com.devexperts.account.Account;
import com.devexperts.service.AccountService;
import com.devexperts.service.exception.AccountBalanceException;
import com.devexperts.service.exception.AccountNotFoundException;

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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class AccountController extends AbstractAccountController {

public ResponseEntity<Void> transfer(long sourceId, long targetId, double amount) {
return null;
@Autowired
private AccountService accountService;

@PostMapping("/operations/transfer")
public ResponseEntity<Void> transfer(@RequestParam(value = "source_id") long sourceId,
@RequestParam(value = "target_id") long targetId,
@RequestParam(value = "amount") double amount)
throws AccountNotFoundException, AccountBalanceException {
Account source = accountService.getAndCheckAccount(sourceId);
Account target = accountService.getAndCheckAccount(targetId);
accountService.transfer(source, target, amount);
return new ResponseEntity("successful transfer", HttpStatus.OK);
}
}
27 changes: 27 additions & 0 deletions src/main/java/com/devexperts/rest/ExceptionControllerAdvice.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.devexperts.rest;

import com.devexperts.service.exception.AccountBalanceException;
import com.devexperts.service.exception.AccountNotFoundException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice
public class ExceptionControllerAdvice extends ResponseEntityExceptionHandler {

@ExceptionHandler(AccountBalanceException.class)
public ResponseEntity accountBalanceExceptionHandling(Throwable t) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(t.getMessage());
}

@ExceptionHandler(AccountNotFoundException.class)
public ResponseEntity accountNotFoundExceptionHandling(Throwable t) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(t.getMessage());
}

}
19 changes: 18 additions & 1 deletion src/main/java/com/devexperts/service/AccountService.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.devexperts.service;

import com.devexperts.account.Account;
import com.devexperts.service.exception.AccountBalanceException;
import com.devexperts.service.exception.AccountNotFoundException;
import com.devexperts.service.exception.AccountServiceException;

public interface AccountService {

Expand All @@ -26,12 +29,26 @@ public interface AccountService {
* */
Account getAccount(long id);

/**
* Get account from the cache
*
* @param id identification of an account to search for
* @return account associated with given id or throws AccountNotFoundException if account is
* not found in the cache
*/
Account getAndCheckAccount(long id) throws AccountNotFoundException;

/**
* 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
* */
void transfer(Account source, Account target, double amount);
void transfer(Account source, Account target, double amount) throws AccountBalanceException;

/**
* Just for database population on startup
*/
void init();
}
58 changes: 50 additions & 8 deletions src/main/java/com/devexperts/service/AccountServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@

import com.devexperts.account.Account;
import com.devexperts.account.AccountKey;
import com.devexperts.service.exception.AccountBalanceException;
import com.devexperts.service.exception.AccountNotFoundException;

import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

@Service
public class AccountServiceImpl implements AccountService {

private final List<Account> accounts = new ArrayList<>();
private final Map<AccountKey, Account> accounts = new ConcurrentHashMap<>();

@Override
public void clear() {
Expand All @@ -19,19 +26,54 @@ public void clear() {

@Override
public void createAccount(Account account) {
accounts.add(account);
accounts.put(account.getAccountKey(), account);
}

@Override
public Account getAccount(long id) {
return accounts.stream()
.filter(account -> account.getAccountKey() == AccountKey.valueOf(id))
.findAny()
.orElse(null);
return accounts.get(AccountKey.valueOf(id));
}

public Account getAndCheckAccount(long id) throws AccountNotFoundException {
Account account = getAccount(id);
if (account == null) {
throw new AccountNotFoundException(String.format("account %d is not found", id));
}
return account;
}

@Override
public synchronized void transfer(Account source, Account target, double amount) throws
AccountBalanceException {
//I could introduce narrower synchronization there, but not with mutable Account
BigDecimal bDAmount = BigDecimal.valueOf(amount);
double newSourceBalance =
BigDecimal.valueOf(source.getBalance()).subtract(bDAmount).doubleValue();
if (Double.compare(newSourceBalance, 0.0) < 0) {
throw new AccountBalanceException(String.format("insufficient account balance"));
}
double newTargetBalance =
BigDecimal.valueOf(target.getBalance()).add(bDAmount).doubleValue();
if (isDoubleInfiniteOrNan(newSourceBalance) || isDoubleInfiniteOrNan(
newTargetBalance)) {
//TODO: some logging could be there
//TODO: better to propagate checked exception to interface method
throw new RuntimeException("Cannot transfer");
}
source.setBalance(newSourceBalance);
target.setBalance(newTargetBalance);

accounts.put(source.getAccountKey(), source);
accounts.put(target.getAccountKey(), target);
}

private boolean isDoubleInfiniteOrNan(Double value) {
return Double.isInfinite(value) || Double.isNaN(value);
}

@Override
public void transfer(Account source, Account target, double amount) {
//do nothing for now
public void init() {
createAccount(new Account(new AccountKey(1l), "SpongeBob", "SquarePants", (double) 0));
createAccount(new Account(new AccountKey(2l), "Patrick", "Star", (double) 10));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.devexperts.service.exception;

public class AccountBalanceException extends AccountServiceException{
public AccountBalanceException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.devexperts.service.exception;

public class AccountNotFoundException extends AccountServiceException{
public AccountNotFoundException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.devexperts.service.exception;

public class AccountServiceException extends Exception {
public AccountServiceException(String message) {
super(message);
}
}
7 changes: 7 additions & 0 deletions src/main/resources/sql/data/accounts.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE TABLE Accounts (
ID bigint,
FIRST_NAME varchar(255),
LAST_NAME varchar(255),
BALANCE double,
PRIMARY KEY (ID)
);
1 change: 1 addition & 0 deletions src/main/resources/sql/data/select.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT * FROM Accounts WHERE ID IN (SELECT ID FROM Transfers WHERE TRANSFER_TIME >= '2019-01-01' GROUP BY ID HAVING SUM(AMOUNT) >= 1000);
12 changes: 12 additions & 0 deletions src/main/resources/sql/data/transfers.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TABLE Transfers (
ID bigint NOT NULL,
SOURCE_ID bigint,
TARGET_ID bigint,
AMOUNT double,
TRANSFER_TIME date,
PRIMARY KEY (ID),
CONSTRAINT FK_SourceAccountTransfers FOREIGN KEY (SOURCE_ID)
REFERENCES Accounts(ID),
CONSTRAINT FK_TargetAccountTransfers FOREIGN KEY (TARGET_ID)
REFERENCES Accounts(ID)
);