diff --git a/pom.xml b/pom.xml index a8ffa1a..ae961b9 100644 --- a/pom.xml +++ b/pom.xml @@ -38,6 +38,16 @@ ${junit.version} test + + io.springfox + springfox-swagger2 + 2.9.2 + + + io.springfox + springfox-swagger-ui + 2.9.2 + org.junit.jupiter junit-jupiter-engine diff --git a/src/main/java/com/devexperts/ApplicationRunner.java b/src/main/java/com/devexperts/ApplicationRunner.java index b6400a4..c251cf1 100644 --- a/src/main/java/com/devexperts/ApplicationRunner.java +++ b/src/main/java/com/devexperts/ApplicationRunner.java @@ -5,8 +5,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; +import springfox.documentation.swagger2.annotations.EnableSwagger2; @SpringBootApplication +@EnableSwagger2 public class ApplicationRunner { public static void main(String[] args) { SpringApplication.run(ApplicationRunner.class, args); diff --git a/src/main/java/com/devexperts/account/Account.java b/src/main/java/com/devexperts/account/Account.java index fb2a3af..d0f18e0 100644 --- a/src/main/java/com/devexperts/account/Account.java +++ b/src/main/java/com/devexperts/account/Account.java @@ -1,5 +1,7 @@ package com.devexperts.account; +import java.util.Objects; + public class Account { private final AccountKey accountKey; private final String firstName; @@ -17,6 +19,10 @@ public AccountKey getAccountKey() { return accountKey; } + public long getAccountKeyId() { + return accountKey.getAccountId(); + } + public String getFirstName() { return firstName; } @@ -32,4 +38,29 @@ public Double getBalance() { public void setBalance(Double balance) { this.balance = balance; } + + public void withdraw(Double amount) { + this.balance -= amount; + } + + public void deposit(Double amount) { + this.balance += amount; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Account account = (Account) o; + return accountKey.equals(account.accountKey); + } + + @Override + public int hashCode() { + return Objects.hash(accountKey); + } } diff --git a/src/main/java/com/devexperts/account/AccountKey.java b/src/main/java/com/devexperts/account/AccountKey.java index 1b0a233..b03d939 100644 --- a/src/main/java/com/devexperts/account/AccountKey.java +++ b/src/main/java/com/devexperts/account/AccountKey.java @@ -1,20 +1,45 @@ package com.devexperts.account; +import java.util.Objects; + /** * Unique Account identifier * *

- * 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. - * */ + * 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 { - private final long accountId; - private AccountKey(long accountId) { - this.accountId = accountId; - } + private final long accountId; + + private AccountKey(long accountId) { + this.accountId = accountId; + } + + public static AccountKey valueOf(long accountId) { + return new AccountKey(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/AccountController.java b/src/main/java/com/devexperts/rest/AccountController.java index b300282..a0ccb0f 100644 --- a/src/main/java/com/devexperts/rest/AccountController.java +++ b/src/main/java/com/devexperts/rest/AccountController.java @@ -1,14 +1,66 @@ package com.devexperts.rest; +import com.devexperts.account.Account; +import com.devexperts.service.AccountServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +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; +import org.springframework.web.server.ResponseStatusException; @RestController @RequestMapping("/api") public class AccountController extends AbstractAccountController { - public ResponseEntity transfer(long sourceId, long targetId, double amount) { - return null; + private AccountServiceImpl accountService; + + @Autowired + AccountController(AccountServiceImpl accountService) { + this.accountService = accountService; + } + + /** + * Details: The operation should be available at POST localhost:8080/api/operations/transfer with + * required query parameters: source_id, target_id, amount. Response codes are the following: + *

+ * 200 (OK) - successful transfer 400 (Bad Request) - one of the parameters in not present or + * amount is invalid 404 (Not Found) - account is not found 500 (Internal Server Error) - + * insufficient account balance + * + * @param sourceId + * @param targetId + * @param amount + * @return + */ + @PostMapping(value = "/operations/transfer") + @Override + public ResponseEntity transfer(@RequestParam long sourceId, @RequestParam long targetId, + @RequestParam double amount) { + + if (amount < 0) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST); + } + + Account sourceAccount = accountService.getAccount(sourceId); + Account targetAccount = accountService.getAccount(targetId); + + if (sourceAccount == null || targetAccount == null) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND); + } + + if (sourceAccount.getBalance() < amount) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR); + } + + try { + accountService.transfer(sourceAccount, targetAccount, amount); + + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + throw new RuntimeException(e); } + } } diff --git a/src/main/java/com/devexperts/service/AccountServiceImpl.java b/src/main/java/com/devexperts/service/AccountServiceImpl.java index 91261ba..964819b 100644 --- a/src/main/java/com/devexperts/service/AccountServiceImpl.java +++ b/src/main/java/com/devexperts/service/AccountServiceImpl.java @@ -2,36 +2,46 @@ import com.devexperts.account.Account; import com.devexperts.account.AccountKey; +import java.util.HashMap; +import java.util.Map; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.List; - @Service public class AccountServiceImpl implements AccountService { - private final List accounts = new ArrayList<>(); - - @Override - public void clear() { - accounts.clear(); - } - - @Override - public void createAccount(Account account) { - accounts.add(account); - } - - @Override - public Account getAccount(long id) { - return accounts.stream() - .filter(account -> account.getAccountKey() == AccountKey.valueOf(id)) - .findAny() - .orElse(null); - } - - @Override - public void transfer(Account source, Account target, double amount) { - //do nothing for now + private final Map accounts; + + public AccountServiceImpl() { + accounts = new HashMap<>(); + } + + @Override + public void clear() { + accounts.clear(); + } + + @Override + public void createAccount(Account account) { + accounts.put(account.getAccountKeyId(), account); + } + + @Override + public Account getAccount(long id) { + return accounts.get(id); + } + + @Override + public void transfer(Account source, Account target, double amount) { + // In order to avoid a deadlock we acquire locks in the same order always. + final Account lock1 = + source.getAccountKeyId() < target.getAccountKeyId() ? source : target; + final Account lock2 = + source.getAccountKeyId() < target.getAccountKeyId() ? target : source; + synchronized (lock1) { + synchronized (lock2) { + source.withdraw(amount); + target.deposit(amount); + } } + } } diff --git a/src/main/java/com/devexperts/swagger/SpringFoxConfig.java b/src/main/java/com/devexperts/swagger/SpringFoxConfig.java new file mode 100644 index 0000000..d599830 --- /dev/null +++ b/src/main/java/com/devexperts/swagger/SpringFoxConfig.java @@ -0,0 +1,29 @@ +package com.devexperts.swagger; + + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +@Configuration +@EnableSwagger2 +public class SpringFoxConfig { + + /** + * Docket configuration bean. + * + * @return Docket + */ + @Bean + public Docket api() { + return new Docket(DocumentationType.SWAGGER_2) + .select() + .apis(RequestHandlerSelectors.basePackage("com.devexperts.rest")) + .paths(PathSelectors.any()) + .build(); + } +} diff --git a/src/main/resources/sql/data/accounts.sql b/src/main/resources/sql/data/accounts.sql new file mode 100644 index 0000000..4ed32cc --- /dev/null +++ b/src/main/resources/sql/data/accounts.sql @@ -0,0 +1,7 @@ +create table accounts +( + ID LONG AUTO_INCREMENT Primary Key, + FIRST_NAME varchar(255) NOT NULL, + LAST_NAME varchar(255) NOT NULL, + BALANCE DOUBLE 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..ffd28d7 --- /dev/null +++ b/src/main/resources/sql/data/select.sql @@ -0,0 +1,6 @@ +SELECT accounts.ID, accounts.FIRST_NAME, accounts.LAST_NAME +FROM accounts, transfers +WHERE transfers.SOURCE_ID = accounts.ID +AND transfers.TRANSFER_TIME >= 2019-01-01 +GROUP BY accounts.ID +HAVING SUM(transfers.AMOUNT) > 1000.00 diff --git a/src/main/resources/sql/data/transfers.sql b/src/main/resources/sql/data/transfers.sql new file mode 100644 index 0000000..f817a2b --- /dev/null +++ b/src/main/resources/sql/data/transfers.sql @@ -0,0 +1,10 @@ +create table transfers +( + ID LONG AUTO_INCREMENT Primary Key, + SOURCE_ID long NOT NULL, + TARGET_ID long NOT NULL, + AMOUNT DOUBLE NOT NULL, + TRANSFER_TIME DATE NOT NULL, + FOREIGN KEY (SOURCE_ID) REFERENCES accounts(ID), + FOREIGN KEY (TARGET_ID) REFERENCES accounts(ID) +); \ No newline at end of file