diff --git a/src/main/java/com/devexperts/account/AccountKey.java b/src/main/java/com/devexperts/account/AccountKey.java
index 1b0a233..1038e7a 100644
--- a/src/main/java/com/devexperts/account/AccountKey.java
+++ b/src/main/java/com/devexperts/account/AccountKey.java
@@ -6,8 +6,8 @@
*
* 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 {
private final long accountId;
private AccountKey(long accountId) {
@@ -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);
+ }
}
diff --git a/src/main/java/com/devexperts/config/PopulationAccountService.java b/src/main/java/com/devexperts/config/PopulationAccountService.java
new file mode 100644
index 0000000..f52c052
--- /dev/null
+++ b/src/main/java/com/devexperts/config/PopulationAccountService.java
@@ -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));
+ };
+ }
+}
diff --git a/src/main/java/com/devexperts/rest/AbstractAccountController.java b/src/main/java/com/devexperts/rest/AbstractAccountController.java
index dea5a3c..7b1c4a4 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 org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PathVariable;
public abstract class AbstractAccountController {
- abstract ResponseEntity transfer(long sourceId, long targetId, double amount);
+ abstract ResponseEntity transfer(@PathVariable Long sourceId, @PathVariable Long targetId, @PathVariable Double amount);
}
diff --git a/src/main/java/com/devexperts/rest/AccountController.java b/src/main/java/com/devexperts/rest/AccountController.java
index b300282..52f24c6 100644
--- a/src/main/java/com/devexperts/rest/AccountController.java
+++ b/src/main/java/com/devexperts/rest/AccountController.java
@@ -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 transfer(long sourceId, long targetId, double amount) {
- return null;
+ public AccountController(AccountService accountService) {
+ this.accountService = accountService;
+ }
+
+ @PostMapping("/operations/transfer")
+ public ResponseEntity 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);
+ }
}
}
diff --git a/src/main/java/com/devexperts/service/AccountServiceImpl.java b/src/main/java/com/devexperts/service/AccountServiceImpl.java
index 91261ba..04e4dc6 100644
--- a/src/main/java/com/devexperts/service/AccountServiceImpl.java
+++ b/src/main/java/com/devexperts/service/AccountServiceImpl.java
@@ -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;
@@ -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);
}
}
diff --git a/src/main/java/com/devexperts/service/exception/InvalidAmountException.java b/src/main/java/com/devexperts/service/exception/InvalidAmountException.java
new file mode 100644
index 0000000..6bdd45f
--- /dev/null
+++ b/src/main/java/com/devexperts/service/exception/InvalidAmountException.java
@@ -0,0 +1,4 @@
+package com.devexperts.service.exception;
+
+public class InvalidAmountException extends RuntimeException {
+}
diff --git a/src/main/java/com/devexperts/service/exception/NotFoundAccountException.java b/src/main/java/com/devexperts/service/exception/NotFoundAccountException.java
new file mode 100644
index 0000000..2f72c98
--- /dev/null
+++ b/src/main/java/com/devexperts/service/exception/NotFoundAccountException.java
@@ -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");
+ }
+}
diff --git a/src/main/resources/sql/data/accounts.sql b/src/main/resources/sql/data/accounts.sql
new file mode 100644
index 0000000..1f07a0a
--- /dev/null
+++ b/src/main/resources/sql/data/accounts.sql
@@ -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
+);
\ 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..d1b5579
--- /dev/null
+++ b/src/main/resources/sql/data/select.sql
@@ -0,0 +1 @@
+SELECT SOURCE_ID FROM transfers WHERE TRANSFER_TIME > '2019-01-01'::timestamp GROUP BY SOURCE_ID HAVING sum(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..d3728be
--- /dev/null
+++ b/src/main/resources/sql/data/transfers.sql
@@ -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
+);
\ No newline at end of file
diff --git a/src/test/java/com/devexperts/service/AccountServiceImplTest.java b/src/test/java/com/devexperts/service/AccountServiceImplTest.java
new file mode 100644
index 0000000..3e92839
--- /dev/null
+++ b/src/test/java/com/devexperts/service/AccountServiceImplTest.java
@@ -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();
+ }
+}
\ No newline at end of file