diff --git a/pom.xml b/pom.xml
index a8ffa1a..2b2c675 100644
--- a/pom.xml
+++ b/pom.xml
@@ -95,6 +95,17 @@
spring-boot-starter-test
test
+
+ org.springframework
+ spring-tx
+
+
+
+ org.mockito
+ mockito-junit-jupiter
+
+ test
+
diff --git a/src/main/java/com/devexperts/account/AccountKey.java b/src/main/java/com/devexperts/account/AccountKey.java
index 1b0a233..e9d788c 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,18 @@ private AccountKey(long accountId) {
public static AccountKey valueOf(long accountId) {
return new AccountKey(accountId);
}
+
+ // переопределил equals и hashcode
+ @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/exceptions/AccountNotFoundException.java b/src/main/java/com/devexperts/exceptions/AccountNotFoundException.java
new file mode 100644
index 0000000..61b8b60
--- /dev/null
+++ b/src/main/java/com/devexperts/exceptions/AccountNotFoundException.java
@@ -0,0 +1,11 @@
+package com.devexperts.exceptions;
+
+public class AccountNotFoundException extends Exception {
+ public AccountNotFoundException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public AccountNotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/devexperts/exceptions/AmountIsInvalidException.java b/src/main/java/com/devexperts/exceptions/AmountIsInvalidException.java
new file mode 100644
index 0000000..5816133
--- /dev/null
+++ b/src/main/java/com/devexperts/exceptions/AmountIsInvalidException.java
@@ -0,0 +1,11 @@
+package com.devexperts.exceptions;
+
+public class AmountIsInvalidException extends Exception {
+ public AmountIsInvalidException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public AmountIsInvalidException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/devexperts/exceptions/InsufficientAccountBalanceException.java b/src/main/java/com/devexperts/exceptions/InsufficientAccountBalanceException.java
new file mode 100644
index 0000000..85f61b8
--- /dev/null
+++ b/src/main/java/com/devexperts/exceptions/InsufficientAccountBalanceException.java
@@ -0,0 +1,11 @@
+package com.devexperts.exceptions;
+
+public class InsufficientAccountBalanceException extends Exception {
+ public InsufficientAccountBalanceException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public InsufficientAccountBalanceException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/devexperts/rest/AccountController.java b/src/main/java/com/devexperts/rest/AccountController.java
index b300282..02158e9 100644
--- a/src/main/java/com/devexperts/rest/AccountController.java
+++ b/src/main/java/com/devexperts/rest/AccountController.java
@@ -1,14 +1,37 @@
package com.devexperts.rest;
+import com.devexperts.account.Account;
+import com.devexperts.exceptions.AccountNotFoundException;
+import com.devexperts.exceptions.AmountIsInvalidException;
+import com.devexperts.exceptions.InsufficientAccountBalanceException;
+import com.devexperts.service.AccountService;
+import org.springframework.beans.factory.annotation.Autowired;
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 transfer(long sourceId, long targetId, double amount) {
- return null;
+ @Autowired
+ private AccountService accountService;
+
+ @PostMapping("operations/transfer")
+ public ResponseEntity transfer(@RequestParam long sourceId,
+ @RequestParam long targetId,
+ @RequestParam double amount) {
+ try {
+ accountService.transferWithChecks(sourceId, targetId, amount);
+ } catch (AmountIsInvalidException e) {
+ return ResponseEntity.badRequest().build();
+ } catch (AccountNotFoundException e) {
+ return ResponseEntity.notFound().build();
+ } catch (InsufficientAccountBalanceException e) {
+ return ResponseEntity.status(500).build();
+ }
+ return ResponseEntity.ok().build();
}
}
diff --git a/src/main/java/com/devexperts/service/AccountService.java b/src/main/java/com/devexperts/service/AccountService.java
index f287597..0856cd9 100644
--- a/src/main/java/com/devexperts/service/AccountService.java
+++ b/src/main/java/com/devexperts/service/AccountService.java
@@ -1,6 +1,9 @@
package com.devexperts.service;
import com.devexperts.account.Account;
+import com.devexperts.exceptions.AccountNotFoundException;
+import com.devexperts.exceptions.AmountIsInvalidException;
+import com.devexperts.exceptions.InsufficientAccountBalanceException;
public interface AccountService {
@@ -33,5 +36,8 @@ public interface AccountService {
* @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);
+
+ void transferWithChecks(long sourceId, long targetId, double amount)
+ throws AccountNotFoundException, InsufficientAccountBalanceException, AmountIsInvalidException;
}
diff --git a/src/main/java/com/devexperts/service/AccountServiceImpl.java b/src/main/java/com/devexperts/service/AccountServiceImpl.java
index 91261ba..a8b119c 100644
--- a/src/main/java/com/devexperts/service/AccountServiceImpl.java
+++ b/src/main/java/com/devexperts/service/AccountServiceImpl.java
@@ -2,15 +2,21 @@
import com.devexperts.account.Account;
import com.devexperts.account.AccountKey;
+import com.devexperts.exceptions.AccountNotFoundException;
+import com.devexperts.exceptions.AmountIsInvalidException;
+import com.devexperts.exceptions.InsufficientAccountBalanceException;
+import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.RequestParam;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.HashMap;
+import java.util.Map;
@Service
public class AccountServiceImpl implements AccountService {
- private final List accounts = new ArrayList<>();
+ private final Map accounts = new HashMap<>();
@Override
public void clear() {
@@ -19,19 +25,61 @@ public void clear() {
@Override
public void createAccount(Account account) {
- accounts.add(account);
+ if (!accounts.containsKey(account.getAccountKey())) {
+ accounts.put(account.getAccountKey(), account);
+ }
}
@Override
public Account getAccount(long id) {
- return accounts.stream()
- .filter(account -> account.getAccountKey() == AccountKey.valueOf(id))
- .findAny()
- .orElse(null);
+ synchronized (this) {
+ return accounts.get(AccountKey.valueOf(id));
+ }
+ }
+
+ private void transfer(Account source, Account target, double amount) {
+ source.setBalance(source.getBalance() - amount);
+ target.setBalance(target.getBalance() + amount);
}
@Override
- public void transfer(Account source, Account target, double amount) {
- //do nothing for now
+ @Transactional
+ public void transferWithChecks(long sourceId, long targetId, double amount)
+ throws AccountNotFoundException, InsufficientAccountBalanceException,
+ AmountIsInvalidException {
+
+ if (amount <= 0) {
+ throw new AmountIsInvalidException("Amount is invalid");
+ }
+
+ Account sourceAccount = getAccount(sourceId);
+ Account targetAccount = getAccount(targetId);
+
+ if (sourceAccount == null) {
+ throw new AccountNotFoundException("Source account is not found.");
+ }
+
+ if (targetAccount == null) {
+ throw new AccountNotFoundException("Target account is not found.");
+ }
+ if (sourceId < targetId) {
+ synchronized (sourceAccount) {
+ synchronized (targetAccount) {
+ if (sourceAccount.getBalance() < amount) {
+ throw new InsufficientAccountBalanceException("Insufficient account balance");
+ }
+ transfer(sourceAccount, targetAccount, amount);
+ }
+ }
+ } else {
+ synchronized (targetAccount) {
+ synchronized (sourceAccount) {
+ if (sourceAccount.getBalance() < amount) {
+ throw new InsufficientAccountBalanceException("Insufficient account balance");
+ }
+ transfer(sourceAccount, targetAccount, 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..0d2ca5e
--- /dev/null
+++ b/src/main/resources/sql.data/accounts.sql
@@ -0,0 +1,7 @@
+create table accounts (
+ id int8 not null,
+ first_name varchar(250) not null,
+ last_name varchar(250),
+ balance money,
+ constraint accounts_pkey primary key (id)
+);
diff --git a/src/main/resources/sql.data/select.sql b/src/main/resources/sql.data/select.sql
new file mode 100644
index 0000000..eef64a8
--- /dev/null
+++ b/src/main/resources/sql.data/select.sql
@@ -0,0 +1,5 @@
+select a.id, a.first_name, sum(t.amount) total
+from accounts a join transfers t on a.id = t.source_id
+where t.transfer_time >= '2019-01-01'
+group by a.id
+having sum(t.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..b02f6f0
--- /dev/null
+++ b/src/main/resources/sql.data/transfers.sql
@@ -0,0 +1,8 @@
+create table transfers (
+ id int8 not null,
+ source_id int8 not null,
+ target_id int8 not null,
+ amount money not null,
+ transfer_time timestamp not null,
+ constraint transfers_pkey primary key (id)
+);
diff --git a/src/test/java/com/devexperts/rest/AccountControllerTest.java b/src/test/java/com/devexperts/rest/AccountControllerTest.java
new file mode 100644
index 0000000..ed3542c
--- /dev/null
+++ b/src/test/java/com/devexperts/rest/AccountControllerTest.java
@@ -0,0 +1,126 @@
+package com.devexperts.rest;
+
+import com.devexperts.account.Account;
+import com.devexperts.account.AccountKey;
+import com.devexperts.exceptions.AccountNotFoundException;
+import com.devexperts.exceptions.AmountIsInvalidException;
+import com.devexperts.exceptions.InsufficientAccountBalanceException;
+import com.devexperts.service.AccountService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+
+@ExtendWith(MockitoExtension.class)
+class AccountControllerTest {
+
+ private MockMvc mvc;
+
+ @InjectMocks
+ private AccountController accountController;
+
+ @Mock
+ private AccountService accountService;
+
+ @BeforeEach
+ public void setup() {
+ mvc = MockMvcBuilders.standaloneSetup(accountController)
+ .build();
+ }
+
+ /**
+ * successful transfer
+ * */
+ @Test
+ void transfer_0K() throws Exception {
+ MockHttpServletResponse response = mvc.perform(
+ post("/api/operations/transfer")
+ .param("sourceId", String.valueOf(1l))
+ .param("targetId", String.valueOf(2l))
+ .param("amount", String.valueOf(20.1)))
+ .andReturn().getResponse();
+
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
+ }
+
+ /**
+ * account is not found
+ * */
+ @Test
+ void transfer_NOT_FOUND() throws Exception {
+ doThrow(AccountNotFoundException.class).when(accountService)
+ .transferWithChecks(1L, 2L, 20.1);
+ MockHttpServletResponse response = mvc.perform(
+ post("/api/operations/transfer")
+ .param("sourceId", String.valueOf(1l))
+ .param("targetId", String.valueOf(2l))
+ .param("amount", String.valueOf(20.1)))
+ .andReturn().getResponse();
+
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
+ }
+
+ /**
+ * amount is invalid
+ * */
+ @Test
+ void transfer_BAD_REQUEST() throws Exception {
+ doThrow(AmountIsInvalidException.class).when(accountService)
+ .transferWithChecks(1L, 2L, -1.0);
+ MockHttpServletResponse response = mvc.perform(
+ post("/api/operations/transfer")
+ .param("sourceId", String.valueOf(1l))
+ .param("targetId", String.valueOf(2l))
+ .param("amount", String.valueOf(-1.0)))
+ .andReturn().getResponse();
+
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
+ }
+
+ /**
+ * one of the parameters in not present
+ * */
+ @Test
+ void transfer_BAD_REQUEST2() throws Exception {
+ MockHttpServletResponse response = mvc.perform(
+ post("/api/operations/transfer")
+ .param("sourceId", String.valueOf(1l))
+ .param("amount", String.valueOf(1.0)))
+ .andReturn().getResponse();
+
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
+ }
+
+ /**
+ * insufficient account balance
+ * */
+ @Test
+ void transfer_INTERNAL_SERVER_ERROR() throws Exception {
+ doThrow(InsufficientAccountBalanceException.class).when(accountService)
+ .transferWithChecks(1L, 2L, 1000.0);
+ MockHttpServletResponse response = mvc.perform(
+ post("/api/operations/transfer")
+ .param("sourceId", String.valueOf(1l))
+ .param("targetId", String.valueOf(2l))
+ .param("amount", String.valueOf(1000.0)))
+ .andReturn().getResponse();
+
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value());
+ }
+}
\ 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..b81c251
--- /dev/null
+++ b/src/test/java/com/devexperts/service/AccountServiceImplTest.java
@@ -0,0 +1,63 @@
+package com.devexperts.service;
+
+import com.devexperts.account.Account;
+import com.devexperts.account.AccountKey;
+import com.devexperts.exceptions.AccountNotFoundException;
+import com.devexperts.exceptions.AmountIsInvalidException;
+import com.devexperts.exceptions.InsufficientAccountBalanceException;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.doReturn;
+
+@SpringBootTest
+class AccountServiceImplTest {
+ @Autowired
+ AccountService accountService;
+
+ @Test
+ void transfer_OK() throws AccountNotFoundException, AmountIsInvalidException, InsufficientAccountBalanceException {
+ accountService.createAccount(new Account(AccountKey.valueOf(1L),
+ "Bill", "Gates", 222.00));
+ accountService.createAccount(new Account(AccountKey.valueOf(2L),
+ "Steve", "Jobs", 133.00));
+ accountService.transferWithChecks(1L, 2L, 10);
+ assertEquals(222.00 - 10, accountService.getAccount(1L).getBalance());
+ assertEquals(133.00 + 10, accountService.getAccount(2L).getBalance());
+ accountService.clear();
+ }
+
+
+ @Test
+ void transfer_AmountIsInvalid() {
+ accountService.createAccount(new Account(AccountKey.valueOf(1L),
+ "Bill", "Gates", 222.00));
+ accountService.createAccount(new Account(AccountKey.valueOf(2L),
+ "Steve", "Jobs", 133.00));
+ assertThrows(AmountIsInvalidException.class, () ->
+ accountService.transferWithChecks(1L, 2L, 0)
+ );
+ accountService.clear();
+ }
+
+ @Test
+ void transfer_AccountIsNotFound() {
+ assertThrows(AccountNotFoundException.class, () ->
+ accountService.transferWithChecks(33L, 22L, 10)
+ );
+ }
+
+ @Test
+ void transfer_InsufficientAccountBalance() {
+ accountService.createAccount(new Account(AccountKey.valueOf(1L),
+ "Bill", "Gates", 222.00));
+ accountService.createAccount(new Account(AccountKey.valueOf(2L),
+ "Steve", "Jobs", 133.00));
+ assertThrows(InsufficientAccountBalanceException.class, () ->
+ accountService.transferWithChecks(1L, 2L, 1000)
+ );
+ accountService.clear();
+ }
+}