diff --git a/pom.xml b/pom.xml
index a8ffa1a..090a46e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -95,6 +95,18 @@
spring-boot-starter-test
test
+
+ junit
+ junit
+ 4.13.1
+ compile
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.5.2
+ compile
+
diff --git a/src/main/java/com/devexperts/AccountServiceTest.java b/src/main/java/com/devexperts/AccountServiceTest.java
new file mode 100644
index 0000000..edd5ec5
--- /dev/null
+++ b/src/main/java/com/devexperts/AccountServiceTest.java
@@ -0,0 +1,59 @@
+package com.devexperts;
+
+import com.devexperts.account.Account;
+import com.devexperts.account.AccountKey;
+import com.devexperts.service.AccountService;
+import com.devexperts.service.AccountServiceImpl;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class AccountServiceTest {
+ private AccountService accountService;
+ private Account account;
+
+ @Before
+ public void setUp() {
+ accountService = new AccountServiceImpl();
+ account = new Account(AccountKey.valueOf(42), "FirstName", "LastName", 0.0);
+ }
+
+ @Test
+ public void testSingleAccountCreation() {
+ accountService.createAccount(account);
+ Account actual = accountService.getAccount(account.getAccountKey().getAccountId());
+ Assert.assertEquals(account, actual);
+ }
+
+ @Test
+ public void testAccountsCreation() {
+ accountService.createAccount(account);
+ Account actual = accountService.getAccount(account.getAccountKey().getAccountId());
+ Assert.assertEquals(account, actual);
+
+ Account otherAccount = new Account(AccountKey.valueOf(account.getAccountKey().getAccountId() + 1), "FirstName", "LastName", 0.0);
+ accountService.createAccount(otherAccount);
+ actual = accountService.getAccount(otherAccount.getAccountKey().getAccountId());
+ Assert.assertEquals(otherAccount, actual);
+ Assert.assertNotEquals(account, actual);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testExistingAccountCreation() {
+ Account account = new Account(AccountKey.valueOf(1), "FirstName", "LastName", 0.0);
+ accountService.createAccount(account);
+ accountService.createAccount(account);
+ }
+
+ @Test
+ public void testAccountsClearing() {
+ accountService.createAccount(account);
+ Account actual = accountService.getAccount(account.getAccountKey().getAccountId());
+ Assert.assertNotNull(actual);
+
+ accountService.clear();
+
+ actual = accountService.getAccount(account.getAccountKey().getAccountId());
+ Assert.assertNull(actual);
+ }
+}
diff --git a/src/main/java/com/devexperts/MultithreadTransferTest.java b/src/main/java/com/devexperts/MultithreadTransferTest.java
new file mode 100644
index 0000000..3badedc
--- /dev/null
+++ b/src/main/java/com/devexperts/MultithreadTransferTest.java
@@ -0,0 +1,43 @@
+package com.devexperts;
+
+import com.devexperts.account.Account;
+import com.devexperts.account.AccountKey;
+import com.devexperts.service.AccountServiceImpl;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class MultithreadTransferTest {
+ AccountServiceImpl service;
+ Account source, target;
+ AccountKey skey, tkey;
+
+ @BeforeEach
+ private void setUp() {
+ service = new AccountServiceImpl();
+ skey = AccountKey.valueOf(1);
+ source = new Account(skey, "One", "First", 100.0);
+
+ tkey = AccountKey.valueOf(2);
+ target = new Account(tkey, "Two", "Second", 200.0);
+ }
+
+ @Test
+ void multithreadTransfer() throws InterruptedException {
+ CountDownLatch latch = new CountDownLatch(10);
+ for (int i = 0; i < 10; i++) {
+ new Thread(()->{
+ service.transfer(source, target, 10.0);
+ latch.countDown();
+ }).start();
+ }
+ latch.await(1, TimeUnit.SECONDS);
+
+ assertEquals(0.0, source.getBalance());
+ assertEquals(300.0, target.getBalance());
+ }
+}
diff --git a/src/main/java/com/devexperts/TransferTest.java b/src/main/java/com/devexperts/TransferTest.java
new file mode 100644
index 0000000..b6429e1
--- /dev/null
+++ b/src/main/java/com/devexperts/TransferTest.java
@@ -0,0 +1,33 @@
+package com.devexperts;
+
+import com.devexperts.account.Account;
+import com.devexperts.account.AccountKey;
+import com.devexperts.service.AccountService;
+import com.devexperts.service.AccountServiceImpl;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class TransferTest {
+ private AccountService accountService;
+ private Account source;
+ private Account target;
+
+ @BeforeEach
+ public void setUp() {
+ accountService = new AccountServiceImpl();
+
+ source = new Account(AccountKey.valueOf(1), "One", "First", 100.0);
+ target = new Account(AccountKey.valueOf(2), "Two", "Second", 150.0);
+ accountService.createAccount(source);
+ accountService.createAccount(target);
+ }
+
+ @Test
+ public void transfer() {
+ accountService.transfer(source, target, 50.0);
+ assertEquals(source.getBalance(), 50.0);
+ assertEquals(target.getBalance(), 200.0);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/devexperts/account/AccountKey.java b/src/main/java/com/devexperts/account/AccountKey.java
index 1b0a233..1aae4af 100644
--- a/src/main/java/com/devexperts/account/AccountKey.java
+++ b/src/main/java/com/devexperts/account/AccountKey.java
@@ -1,5 +1,7 @@
package com.devexperts.account;
+import java.util.Objects;
+
/**
* Unique Account identifier
*
@@ -17,4 +19,21 @@ private AccountKey(long accountId) {
public static AccountKey valueOf(long accountId) {
return new AccountKey(accountId);
}
+
+ public long getAccountId() {
+ return 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);
+ }
}
diff --git a/src/main/java/com/devexperts/rest/AbstractAccountController.java b/src/main/java/com/devexperts/rest/AbstractAccountController.java
index dea5a3c..f230808 100644
--- a/src/main/java/com/devexperts/rest/AbstractAccountController.java
+++ b/src/main/java/com/devexperts/rest/AbstractAccountController.java
@@ -3,5 +3,5 @@
import org.springframework.http.ResponseEntity;
public abstract class AbstractAccountController {
- abstract ResponseEntity transfer(long sourceId, long targetId, double amount);
+ abstract ResponseEntity transfer(Long sourceId, Long targetId, Double amount);
}
diff --git a/src/main/java/com/devexperts/rest/AccountController.java b/src/main/java/com/devexperts/rest/AccountController.java
index b300282..e13ebe2 100644
--- a/src/main/java/com/devexperts/rest/AccountController.java
+++ b/src/main/java/com/devexperts/rest/AccountController.java
@@ -1,14 +1,44 @@
package com.devexperts.rest;
+import com.devexperts.account.Account;
+import com.devexperts.service.AccountService;
+import com.devexperts.service.exceptions.AccountNotFoundException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
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 transfer(long sourceId, long targetId, double amount) {
- return null;
+ private final AccountService service;
+
+ @Autowired
+ public AccountController (AccountService service) {
+ this.service = service;
+ }
+
+ @GetMapping("operations/transfer")
+ public ResponseEntity transfer(
+ @RequestParam("sourceId") Long sourceId,
+ @RequestParam("targetId") Long targetId,
+ @RequestParam("amount") Double amount) {
+ if (sourceId == null || targetId == null || amount < 0)
+ return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
+
+ try {
+ Account source = service.getAccount(sourceId);
+ Account target = service.getAccount(targetId);
+ service.transfer(source, target, amount);
+ return new ResponseEntity<>(HttpStatus.OK);
+ } catch (AccountNotFoundException e) {
+ return new ResponseEntity<>(HttpStatus.NOT_FOUND);
+ } catch (IllegalStateException e) {
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/devexperts/service/AccountServiceImpl.java b/src/main/java/com/devexperts/service/AccountServiceImpl.java
index 91261ba..1ced5a7 100644
--- a/src/main/java/com/devexperts/service/AccountServiceImpl.java
+++ b/src/main/java/com/devexperts/service/AccountServiceImpl.java
@@ -4,13 +4,15 @@
import com.devexperts.account.AccountKey;
import org.springframework.stereotype.Service;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.ReentrantLock;
@Service
public class AccountServiceImpl implements AccountService {
- private final List accounts = new ArrayList<>();
+ private final Map accounts = new ConcurrentHashMap<>();
+ private final ReentrantLock lock = new ReentrantLock();
@Override
public void clear() {
@@ -19,19 +21,38 @@ public void clear() {
@Override
public void createAccount(Account account) {
- accounts.add(account);
+ if (account.getAccountKey() == null) {
+ throw new NullPointerException();
+ }
+ if (accounts.containsKey(account.getAccountKey())) {
+ throw new IllegalArgumentException("This key is already used.");
+ }
+ 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.getOrDefault(AccountKey.valueOf(id), null);
}
@Override
public void transfer(Account source, Account target, double amount) {
- //do nothing for now
+ if (amount < 0 || source == target)
+ throw new IllegalArgumentException("Amount can't be negative OR Source can't be same as target.");
+
+ lock.lock();
+ try {
+ double sourceBalance = source.getBalance();
+ double targetBalance = target.getBalance();
+
+ if (sourceBalance < amount)
+ throw new IllegalStateException("Source balance is insufficient.");
+
+ source.setBalance(sourceBalance - amount);
+ target.setBalance(targetBalance + amount);
+
+ } finally {
+ lock.unlock();
+ }
}
}
diff --git a/src/main/java/com/devexperts/service/exceptions/AccountNotFoundException.java b/src/main/java/com/devexperts/service/exceptions/AccountNotFoundException.java
new file mode 100644
index 0000000..e0a217b
--- /dev/null
+++ b/src/main/java/com/devexperts/service/exceptions/AccountNotFoundException.java
@@ -0,0 +1,7 @@
+package com.devexperts.service.exceptions;
+
+public class AccountNotFoundException extends RuntimeException {
+ public AccountNotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/resources/data/accounts.sql b/src/main/java/resources/data/accounts.sql
new file mode 100644
index 0000000..29f4e9d
--- /dev/null
+++ b/src/main/java/resources/data/accounts.sql
@@ -0,0 +1,7 @@
+CREATE TABLE IF NOT EXISTS Accounts
+(
+ ID BIGINT AUTO_INCREMENT PRIMARY,
+ 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/java/resources/data/select.sql b/src/main/java/resources/data/select.sql
new file mode 100644
index 0000000..7c39e59
--- /dev/null
+++ b/src/main/java/resources/data/select.sql
@@ -0,0 +1,4 @@
+SELECT t.source_id FROM Transfers AS t
+WHERE t.transfer_time >= TIMESTAMP '2019-01-01' AND source_id != target_id AND amount > 0
+GROUP BY source_id
+HAVING SUM(amount) > 1000;
\ No newline at end of file
diff --git a/src/main/java/resources/transfers.sql b/src/main/java/resources/transfers.sql
new file mode 100644
index 0000000..1d477a4
--- /dev/null
+++ b/src/main/java/resources/transfers.sql
@@ -0,0 +1,10 @@
+CREATE TABLE IF NOT EXISTS Transfers
+(
+ ID BIGINT AUTO_INCREMENT PRIMARY,
+ SOURCE_ID BIGINT NOT NULL,
+ TARGET_ID BIGINT NOT NULL,
+ AMOUNT BIGINT NOT NULL,
+ TRANSFER_TIME TIMESTAMP NOT NULL,
+ FOREIGN KEY (SOURCE_ID) REFERENCES accounts (ID),
+ FOREIGN KEY (TARGET_ID) REFERENCES accounts (ID)
+ );
\ No newline at end of file