From 9197019e1cf9714e997e1d73f90f8229c6072bcb Mon Sep 17 00:00:00 2001 From: 45OchilAZ Date: Sat, 23 Oct 2021 12:55:43 +0300 Subject: [PATCH 1/6] Task 1 --- .../com/devexperts/account/AccountKey.java | 16 ++++++++++++++++ .../service/AccountServiceImpl.java | 19 ++++++++++++++----- 2 files changed, 30 insertions(+), 5 deletions(-) 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/service/AccountServiceImpl.java b/src/main/java/com/devexperts/service/AccountServiceImpl.java index 91261ba..5acedae 100644 --- a/src/main/java/com/devexperts/service/AccountServiceImpl.java +++ b/src/main/java/com/devexperts/service/AccountServiceImpl.java @@ -5,12 +5,15 @@ import org.springframework.stereotype.Service; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; @Service public class AccountServiceImpl implements AccountService { - private final List accounts = new ArrayList<>(); + //private final List accounts = new ArrayList<>(); + private Map accounts = new HashMap<>(); @Override public void clear() { @@ -19,15 +22,21 @@ public void clear() { @Override public void createAccount(Account account) { - accounts.add(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)) + // переопределил метод equals в классе AccountKey + // сложность метода O(N) где N размер списка аккаунтов + /*return accounts.stream() + .filter(account -> account.getAccountKey().equals(AccountKey.valueOf(id))) .findAny() - .orElse(null); + .orElse(null);*/ + + // если использовать HashMap вместо ArrayList, то сложность этого метода будет O(1) + return accounts.get(AccountKey.valueOf(id)); } @Override From cc427b2f20e665777e19ed5f75f9da8025db0fb1 Mon Sep 17 00:00:00 2001 From: 45OchilAZ Date: Sat, 23 Oct 2021 14:47:38 +0300 Subject: [PATCH 2/6] Task 2 --- pom.xml | 5 ++++ .../service/AccountServiceImpl.java | 7 ++--- .../service/AccountServiceImplTest.java | 26 +++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 src/test/java/com/devexperts/service/AccountServiceImplTest.java diff --git a/pom.xml b/pom.xml index a8ffa1a..cb46c06 100644 --- a/pom.xml +++ b/pom.xml @@ -95,6 +95,11 @@ spring-boot-starter-test test + + org.springframework + spring-tx + + diff --git a/src/main/java/com/devexperts/service/AccountServiceImpl.java b/src/main/java/com/devexperts/service/AccountServiceImpl.java index 5acedae..779d78e 100644 --- a/src/main/java/com/devexperts/service/AccountServiceImpl.java +++ b/src/main/java/com/devexperts/service/AccountServiceImpl.java @@ -3,10 +3,9 @@ import com.devexperts.account.Account; import com.devexperts.account.AccountKey; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; @Service @@ -39,8 +38,10 @@ public Account getAccount(long id) { return accounts.get(AccountKey.valueOf(id)); } + @Transactional @Override public void transfer(Account source, Account target, double amount) { - //do nothing for now + source.setBalance(source.getBalance() - amount); + target.setBalance(target.getBalance() + amount); } } 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..e893995 --- /dev/null +++ b/src/test/java/com/devexperts/service/AccountServiceImplTest.java @@ -0,0 +1,26 @@ +package com.devexperts.service; + +import com.devexperts.account.Account; +import com.devexperts.account.AccountKey; +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.*; + +@SpringBootTest +class AccountServiceImplTest { + + @Autowired + AccountService accountService; + + @Test + void transfer() { + Account ac1 = new Account(AccountKey.valueOf(1L), "Abdusame", "Ochil-zoda", 100.00); + Account ac2 = new Account(AccountKey.valueOf(1L), "Alex", "Ivanov", 33.00); + accountService.transfer(ac1, ac2, 19.22); + + assertEquals(100.00 - 19.22, ac1.getBalance()); + assertEquals(33.00 + 19.22, ac2.getBalance()); + } +} \ No newline at end of file From e1745058a865a2d90b6a12260cb0324810fb9a21 Mon Sep 17 00:00:00 2001 From: 45OchilAZ Date: Mon, 25 Oct 2021 13:50:13 +0300 Subject: [PATCH 3/6] Task 4 --- pom.xml | 6 + .../devexperts/rest/AccountController.java | 35 ++++- .../rest/AccountControllerTest.java | 124 ++++++++++++++++++ 3 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 src/test/java/com/devexperts/rest/AccountControllerTest.java diff --git a/pom.xml b/pom.xml index cb46c06..2b2c675 100644 --- a/pom.xml +++ b/pom.xml @@ -100,6 +100,12 @@ spring-tx + + org.mockito + mockito-junit-jupiter + + test + diff --git a/src/main/java/com/devexperts/rest/AccountController.java b/src/main/java/com/devexperts/rest/AccountController.java index b300282..b0b8da8 100644 --- a/src/main/java/com/devexperts/rest/AccountController.java +++ b/src/main/java/com/devexperts/rest/AccountController.java @@ -1,14 +1,45 @@ package com.devexperts.rest; +import com.devexperts.account.Account; +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) { + // one of the parameters in not present or amount is invalid + if (amount <= 0) { + return ResponseEntity.badRequest().build(); + } + + // account is not found + Account ac1 = accountService.getAccount(sourceId); + Account ac2 = accountService.getAccount(targetId); + if (ac1 == null || ac2 == null) { + return ResponseEntity.notFound().build(); + } + + // insufficient account balance + if (ac1.getBalance() < amount) { + return ResponseEntity.status(500).build(); + } + + accountService.transfer(ac1, ac2, amount); + + //successful transfer + return ResponseEntity.ok().build(); } } 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..79a81ca --- /dev/null +++ b/src/test/java/com/devexperts/rest/AccountControllerTest.java @@ -0,0 +1,124 @@ +package com.devexperts.rest; + +import com.devexperts.account.Account; +import com.devexperts.account.AccountKey; +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.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 { + doReturn(new Account(AccountKey.valueOf(1L),"Bill", "Gates", 222.00)) + .when(accountService).getAccount(1L); + doReturn(new Account(AccountKey.valueOf(2L),"Steve", "Jobs", 133.00)) + .when(accountService).getAccount(2L); + 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 { + 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 { + 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 { + doReturn(new Account(AccountKey.valueOf(1L),"Bill", "Gates", 222.00)) + .when(accountService).getAccount(1L); + doReturn(new Account(AccountKey.valueOf(2L),"Steve", "Jobs", 133.00)) + .when(accountService).getAccount(2L); + 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 From f9b9f6b198c056b3568e023bc43e3a47219672c4 Mon Sep 17 00:00:00 2001 From: 45OchilAZ Date: Mon, 25 Oct 2021 22:27:11 +0300 Subject: [PATCH 4/6] Task 5 --- .../java/com/devexperts/service/AccountServiceImpl.java | 2 +- src/main/resources/sql.data/accounts.sql | 7 +++++++ src/main/resources/sql.data/select.sql | 5 +++++ src/main/resources/sql.data/transfers.sql | 8 ++++++++ 4 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/sql.data/accounts.sql create mode 100644 src/main/resources/sql.data/select.sql create mode 100644 src/main/resources/sql.data/transfers.sql diff --git a/src/main/java/com/devexperts/service/AccountServiceImpl.java b/src/main/java/com/devexperts/service/AccountServiceImpl.java index 779d78e..cc41881 100644 --- a/src/main/java/com/devexperts/service/AccountServiceImpl.java +++ b/src/main/java/com/devexperts/service/AccountServiceImpl.java @@ -12,7 +12,7 @@ public class AccountServiceImpl implements AccountService { //private final List accounts = new ArrayList<>(); - private Map accounts = new HashMap<>(); + private final Map accounts = new HashMap<>(); @Override public void clear() { 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) +); From 9777ef51c9491bb646ae898a5eaff29b8bb2b61c Mon Sep 17 00:00:00 2001 From: 45OchilAZ Date: Tue, 26 Oct 2021 01:54:24 +0300 Subject: [PATCH 5/6] update for task 4 and solution of task 3 --- .../exceptions/AccountNotFoundException.java | 11 ++++ .../exceptions/AmountIsInvalidException.java | 11 ++++ .../InsufficientAccountBalanceException.java | 11 ++++ .../devexperts/rest/AccountController.java | 24 +++------ .../devexperts/service/AccountService.java | 6 +++ .../service/AccountServiceImpl.java | 33 +++++++++++- .../rest/AccountControllerTest.java | 18 ++++--- .../service/AccountServiceImplTest.java | 53 ++++++++++++++++++- 8 files changed, 140 insertions(+), 27 deletions(-) create mode 100644 src/main/java/com/devexperts/exceptions/AccountNotFoundException.java create mode 100644 src/main/java/com/devexperts/exceptions/AmountIsInvalidException.java create mode 100644 src/main/java/com/devexperts/exceptions/InsufficientAccountBalanceException.java 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 b0b8da8..02158e9 100644 --- a/src/main/java/com/devexperts/rest/AccountController.java +++ b/src/main/java/com/devexperts/rest/AccountController.java @@ -1,6 +1,9 @@ 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; @@ -20,26 +23,15 @@ public class AccountController extends AbstractAccountController { public ResponseEntity transfer(@RequestParam long sourceId, @RequestParam long targetId, @RequestParam double amount) { - // one of the parameters in not present or amount is invalid - if (amount <= 0) { + try { + accountService.transferWithChecks(sourceId, targetId, amount); + } catch (AmountIsInvalidException e) { return ResponseEntity.badRequest().build(); - } - - // account is not found - Account ac1 = accountService.getAccount(sourceId); - Account ac2 = accountService.getAccount(targetId); - if (ac1 == null || ac2 == null) { + } catch (AccountNotFoundException e) { return ResponseEntity.notFound().build(); - } - - // insufficient account balance - if (ac1.getBalance() < amount) { + } catch (InsufficientAccountBalanceException e) { return ResponseEntity.status(500).build(); } - - accountService.transfer(ac1, ac2, amount); - - //successful transfer 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..e9c1b89 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 { @@ -34,4 +37,7 @@ public interface AccountService { * @param amount dollar amount to transfer * */ 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 cc41881..d7a3029 100644 --- a/src/main/java/com/devexperts/service/AccountServiceImpl.java +++ b/src/main/java/com/devexperts/service/AccountServiceImpl.java @@ -2,8 +2,13 @@ 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.HashMap; import java.util.Map; @@ -40,8 +45,34 @@ public Account getAccount(long id) { @Transactional @Override - public void transfer(Account source, Account target, double amount) { + public synchronized void transfer(Account source, Account target, double amount) { source.setBalance(source.getBalance() - amount); target.setBalance(target.getBalance() + amount); } + + @Override + 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 (sourceAccount.getBalance() < amount) { + throw new InsufficientAccountBalanceException("Insufficient account balance"); + } + + transfer(sourceAccount, targetAccount, amount); + } } diff --git a/src/test/java/com/devexperts/rest/AccountControllerTest.java b/src/test/java/com/devexperts/rest/AccountControllerTest.java index 79a81ca..ed3542c 100644 --- a/src/test/java/com/devexperts/rest/AccountControllerTest.java +++ b/src/test/java/com/devexperts/rest/AccountControllerTest.java @@ -2,6 +2,9 @@ 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; @@ -17,6 +20,7 @@ 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.*; @@ -45,10 +49,6 @@ public void setup() { * */ @Test void transfer_0K() throws Exception { - doReturn(new Account(AccountKey.valueOf(1L),"Bill", "Gates", 222.00)) - .when(accountService).getAccount(1L); - doReturn(new Account(AccountKey.valueOf(2L),"Steve", "Jobs", 133.00)) - .when(accountService).getAccount(2L); MockHttpServletResponse response = mvc.perform( post("/api/operations/transfer") .param("sourceId", String.valueOf(1l)) @@ -64,6 +64,8 @@ void transfer_0K() throws Exception { * */ @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)) @@ -79,6 +81,8 @@ void transfer_NOT_FOUND() throws Exception { * */ @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)) @@ -108,10 +112,8 @@ void transfer_BAD_REQUEST2() throws Exception { * */ @Test void transfer_INTERNAL_SERVER_ERROR() throws Exception { - doReturn(new Account(AccountKey.valueOf(1L),"Bill", "Gates", 222.00)) - .when(accountService).getAccount(1L); - doReturn(new Account(AccountKey.valueOf(2L),"Steve", "Jobs", 133.00)) - .when(accountService).getAccount(2L); + doThrow(InsufficientAccountBalanceException.class).when(accountService) + .transferWithChecks(1L, 2L, 1000.0); MockHttpServletResponse response = mvc.perform( post("/api/operations/transfer") .param("sourceId", String.valueOf(1l)) diff --git a/src/test/java/com/devexperts/service/AccountServiceImplTest.java b/src/test/java/com/devexperts/service/AccountServiceImplTest.java index e893995..65a5621 100644 --- a/src/test/java/com/devexperts/service/AccountServiceImplTest.java +++ b/src/test/java/com/devexperts/service/AccountServiceImplTest.java @@ -2,11 +2,15 @@ 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 { @@ -16,11 +20,56 @@ class AccountServiceImplTest { @Test void transfer() { - Account ac1 = new Account(AccountKey.valueOf(1L), "Abdusame", "Ochil-zoda", 100.00); - Account ac2 = new Account(AccountKey.valueOf(1L), "Alex", "Ivanov", 33.00); + Account ac1 = new Account(AccountKey.valueOf(101L), "Abdusame", "Ochil-zoda", 100.00); + Account ac2 = new Account(AccountKey.valueOf(201L), "Alex", "Ivanov", 33.00); accountService.transfer(ac1, ac2, 19.22); assertEquals(100.00 - 19.22, ac1.getBalance()); assertEquals(33.00 + 19.22, ac2.getBalance()); + accountService.clear(); + } + + @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(); } } \ No newline at end of file From b2a0f3bfcbca2d3695f12bee9b3718ae71cf5052 Mon Sep 17 00:00:00 2001 From: 45OchilAZ Date: Thu, 28 Oct 2021 15:02:36 +0300 Subject: [PATCH 6/6] =?UTF-8?q?=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20Acco?= =?UTF-8?q?untServiceImpl=20=D0=BF=D0=BE=D1=82=D0=BE=D0=BA=D0=BE=D0=B1?= =?UTF-8?q?=D0=B5=D0=B7=D0=BE=D0=BF=D0=B0=D1=81=D0=BD=D1=8B=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devexperts/service/AccountService.java | 2 +- .../service/AccountServiceImpl.java | 47 +++++++++++-------- .../service/AccountServiceImplTest.java | 14 +----- 3 files changed, 29 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/devexperts/service/AccountService.java b/src/main/java/com/devexperts/service/AccountService.java index e9c1b89..0856cd9 100644 --- a/src/main/java/com/devexperts/service/AccountService.java +++ b/src/main/java/com/devexperts/service/AccountService.java @@ -36,7 +36,7 @@ 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 d7a3029..a8b119c 100644 --- a/src/main/java/com/devexperts/service/AccountServiceImpl.java +++ b/src/main/java/com/devexperts/service/AccountServiceImpl.java @@ -16,7 +16,6 @@ @Service public class AccountServiceImpl implements AccountService { - //private final List accounts = new ArrayList<>(); private final Map accounts = new HashMap<>(); @Override @@ -26,31 +25,25 @@ public void clear() { @Override public void createAccount(Account account) { - //accounts.add(account); - accounts.put(account.getAccountKey(), account); + if (!accounts.containsKey(account.getAccountKey())) { + accounts.put(account.getAccountKey(), account); + } } @Override public Account getAccount(long id) { - // переопределил метод equals в классе AccountKey - // сложность метода O(N) где N размер списка аккаунтов - /*return accounts.stream() - .filter(account -> account.getAccountKey().equals(AccountKey.valueOf(id))) - .findAny() - .orElse(null);*/ - - // если использовать HashMap вместо ArrayList, то сложность этого метода будет O(1) - return accounts.get(AccountKey.valueOf(id)); + synchronized (this) { + return accounts.get(AccountKey.valueOf(id)); + } } - @Transactional - @Override - public synchronized void transfer(Account source, Account target, double amount) { + private void transfer(Account source, Account target, double amount) { source.setBalance(source.getBalance() - amount); target.setBalance(target.getBalance() + amount); } @Override + @Transactional public void transferWithChecks(long sourceId, long targetId, double amount) throws AccountNotFoundException, InsufficientAccountBalanceException, AmountIsInvalidException { @@ -61,6 +54,7 @@ public void transferWithChecks(long sourceId, long targetId, double amount) Account sourceAccount = getAccount(sourceId); Account targetAccount = getAccount(targetId); + if (sourceAccount == null) { throw new AccountNotFoundException("Source account is not found."); } @@ -68,11 +62,24 @@ public void transferWithChecks(long sourceId, long targetId, double amount) if (targetAccount == null) { throw new AccountNotFoundException("Target account is not found."); } - - if (sourceAccount.getBalance() < amount) { - throw new InsufficientAccountBalanceException("Insufficient account balance"); + 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); + } + } } - - transfer(sourceAccount, targetAccount, amount); } } diff --git a/src/test/java/com/devexperts/service/AccountServiceImplTest.java b/src/test/java/com/devexperts/service/AccountServiceImplTest.java index 65a5621..b81c251 100644 --- a/src/test/java/com/devexperts/service/AccountServiceImplTest.java +++ b/src/test/java/com/devexperts/service/AccountServiceImplTest.java @@ -14,21 +14,9 @@ @SpringBootTest class AccountServiceImplTest { - @Autowired AccountService accountService; - @Test - void transfer() { - Account ac1 = new Account(AccountKey.valueOf(101L), "Abdusame", "Ochil-zoda", 100.00); - Account ac2 = new Account(AccountKey.valueOf(201L), "Alex", "Ivanov", 33.00); - accountService.transfer(ac1, ac2, 19.22); - - assertEquals(100.00 - 19.22, ac1.getBalance()); - assertEquals(33.00 + 19.22, ac2.getBalance()); - accountService.clear(); - } - @Test void transfer_OK() throws AccountNotFoundException, AmountIsInvalidException, InsufficientAccountBalanceException { accountService.createAccount(new Account(AccountKey.valueOf(1L), @@ -72,4 +60,4 @@ void transfer_InsufficientAccountBalance() { ); accountService.clear(); } -} \ No newline at end of file +}