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

Galina Isyanova #53

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
25 changes: 23 additions & 2 deletions src/main/java/com/devexperts/account/AccountKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
* <p>
* NOTE: we suspect that later {@link #accountId} is not going to be uniquely identifying an account,
* as we might add human-readable account representation and some clearing codes for partners.
* */
public class AccountKey {
*/
public class AccountKey implements Comparable<AccountKey> {
private final long accountId;

private AccountKey(long accountId) {
Expand All @@ -17,4 +17,25 @@ private AccountKey(long accountId) {
public static AccountKey valueOf(long accountId) {
return new AccountKey(accountId);
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof AccountKey))
return false;
AccountKey accountKey = (AccountKey) obj;
return accountId == accountKey.accountId;
}

@Override
public int hashCode() {
return Long.hashCode(accountId);
}

@Override
public int compareTo(AccountKey accountKey) {
return Long.compare(accountId, accountKey.accountId);
}
}
20 changes: 20 additions & 0 deletions src/main/java/com/devexperts/config/PopulationAccountService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.devexperts.config;

import com.devexperts.account.Account;
import com.devexperts.account.AccountKey;
import com.devexperts.service.AccountService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class PopulationAccountService {
@Bean
CommandLineRunner initClientDatabase(AccountService accountService) {
return args -> {
accountService.createAccount(new Account(AccountKey.valueOf(1L), "Ivan", "Ivanov", 500.0));
accountService.createAccount(new Account(AccountKey.valueOf(2L), "Petr", "Petrov", 350.0));
accountService.createAccount(new Account(AccountKey.valueOf(3L), "Igor", "Sidorov", 750.0));
};
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package com.devexperts.rest;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;

public abstract class AbstractAccountController {
abstract ResponseEntity<Void> transfer(long sourceId, long targetId, double amount);
abstract ResponseEntity<Void> transfer(@PathVariable Long sourceId, @PathVariable Long targetId, @PathVariable Double amount);
}
28 changes: 26 additions & 2 deletions src/main/java/com/devexperts/rest/AccountController.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,38 @@
package com.devexperts.rest;

import com.devexperts.account.Account;
import com.devexperts.service.AccountService;
import com.devexperts.service.exception.NotFoundAccountException;
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 {
private final AccountService accountService;

public ResponseEntity<Void> transfer(long sourceId, long targetId, double amount) {
return null;
public AccountController(AccountService accountService) {
this.accountService = accountService;
}

@PostMapping("/operations/transfer")
public ResponseEntity<Void> transfer(@RequestParam Long sourceId, @RequestParam Long targetId, @RequestParam Double amount) {
if (sourceId == null || targetId == null || amount == null || amount <= 0) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
try {
Account source = accountService.getAccount(sourceId);
Account target = accountService.getAccount(targetId);
accountService.transfer(source, target, amount);
return new ResponseEntity<>(HttpStatus.OK);
} catch (NotFoundAccountException e) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
} catch (Exception e) {
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
34 changes: 31 additions & 3 deletions src/main/java/com/devexperts/service/AccountServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.devexperts.account.Account;
import com.devexperts.account.AccountKey;
import com.devexperts.service.exception.InvalidAmountException;
import com.devexperts.service.exception.NotFoundAccountException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
Expand All @@ -25,13 +27,39 @@ public void createAccount(Account account) {
@Override
public Account getAccount(long id) {
return accounts.stream()
.filter(account -> account.getAccountKey() == AccountKey.valueOf(id))
.filter(account -> account.getAccountKey().equals(AccountKey.valueOf(id)))
.findAny()
.orElse(null);
.orElseThrow(() -> new NotFoundAccountException(id));
}

@Override
public void transfer(Account source, Account target, double amount) {
//do nothing for now
if (source.getAccountKey().compareTo(target.getAccountKey()) >= 0) {
synchronized (source) {
synchronized (target) {
doTransfer(source, target, amount);
}
}
} else {
synchronized (target) {
synchronized (source) {
doTransfer(source, target, amount);
}
}
}
}

private void doTransfer(Account source, Account target, double amount) {
if (!accounts.contains(source)) {
throw new NotFoundAccountException(source);
}
if (!accounts.contains(target)) {
throw new NotFoundAccountException(target);
}
if (source.getBalance() < amount) {
throw new InvalidAmountException();
}
source.setBalance(source.getBalance() - amount);
target.setBalance(target.getBalance() + amount);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.devexperts.service.exception;

public class InvalidAmountException extends RuntimeException {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.devexperts.service.exception;

import com.devexperts.account.Account;

public class NotFoundAccountException extends RuntimeException {
public NotFoundAccountException(Account account) {
super("Not found account " + account + " at AccountService");
}

public NotFoundAccountException(Long id) {
super("Not found account with id = " + id + " at AccountService");
}
}
9 changes: 9 additions & 0 deletions src/main/resources/sql/data/accounts.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
DROP TABLE IF EXISTS accounts;

CREATE TABLE IF NOT EXISTS accounts
(
ID BIGINT AUTO_INCREMENT PRIMARY KEY,
FIRST_NAME VARCHAR(255) NOT NULL,
LAST_NAME VARCHAR(255) NOT NULL,
BALANCE BIGINT NOT NULL
);
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 SOURCE_ID FROM transfers WHERE TRANSFER_TIME > '2019-01-01'::timestamp GROUP BY SOURCE_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 @@
DROP TABLE IF EXISTS transfers;

CREATE TABLE IF NOT EXISTS transfers
(
ID BIGINT AUTO_INCREMENT PRIMARY KEY,
SOURCE_ID BIGINT NOT NULL,
TARGET_ID BIGINT NOT NULL,
AMOUNT BIGINT NOT NULL,
TRANSFER_TIME TIMESTAMP(6) NOT NULL,
FOREIGN KEY (SOURCE_ID) REFERENCES accounts (ID) ON UPDATE CASCADE ON DELETE SET NULL,
FOREIGN KEY (TARGET_ID) REFERENCES accounts (ID) ON UPDATE CASCADE ON DELETE SET NULL
);
94 changes: 94 additions & 0 deletions src/test/java/com/devexperts/service/AccountServiceImplTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.devexperts.service;

import com.devexperts.account.Account;
import com.devexperts.account.AccountKey;
import com.devexperts.service.exception.InvalidAmountException;
import com.devexperts.service.exception.NotFoundAccountException;
import org.junit.Assert;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

class AccountServiceImplTest {
private static final double DELTA = 0.00001;

private AccountServiceImpl accountService = new AccountServiceImpl();
private volatile Account account1;
private volatile Account account2;
private Account account3;

@BeforeEach
void init() {
account1 = new Account(AccountKey.valueOf(1L), "Ivan", "Ivanov", 500.0);
account2 = new Account(AccountKey.valueOf(2L), "Petr", "Petrov", 350.0);
account3 = new Account(AccountKey.valueOf(3L), "Igor", "Sidorov", 750.0);
accountService.createAccount(account1);
accountService.createAccount(account2);
accountService.createAccount(account3);
}

@org.junit.jupiter.api.Test
void getAccountFromAccounts() {
Account account = accountService.getAccount(1L);
Assert.assertNotNull(account);
Assert.assertEquals("Ivan", account.getFirstName());
Assert.assertEquals("Ivanov", account.getLastName());
Assert.assertEquals(500.0, account.getBalance(), DELTA);
}

@org.junit.jupiter.api.Test
void getAccountNotFromAccounts() {
NotFoundAccountException exception = Assertions.assertThrows(NotFoundAccountException.class, () -> {
accountService.getAccount(5L);
});
Assert.assertEquals(exception.getMessage(), "Not found account with id = 5 at AccountService");
}

@org.junit.jupiter.api.Test
void validTransfer() {
accountService.transfer(account1, account2, 200);
Assert.assertEquals(300, account1.getBalance(), DELTA);
Assert.assertEquals(550, account2.getBalance(), DELTA);
}

@org.junit.jupiter.api.Test
void invalidAmountTransfer() {
Assertions.assertThrows(InvalidAmountException.class, () -> {
accountService.transfer(account2, account3, 500);
});
}

@org.junit.jupiter.api.Test
void transferFromAccountNotFromAccounts() {
Assertions.assertThrows(NotFoundAccountException.class, () -> {
accountService.transfer(new Account(AccountKey.valueOf(8L), "NoName", "NoSurname", 1500.0), account3, 500);
});
}

@org.junit.jupiter.api.Test
void transferMultiThread() throws InterruptedException {
int countTransfer = 5;
CountDownLatch countDownLatch = new CountDownLatch(countTransfer);
Runnable task = () -> {
try {
accountService.transfer(account1, account2, 100);
} finally {
countDownLatch.countDown();
}
};
for (int i = 0; i < countTransfer; i++) {
new Thread(task).start();
}
countDownLatch.await(5, TimeUnit.SECONDS);
Assert.assertEquals(0, account1.getBalance(), DELTA);
Assert.assertEquals(850, account2.getBalance(), DELTA);
}

@AfterEach
void tearDown() {
accountService.clear();
}
}