diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..6ea41f9 --- /dev/null +++ b/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.project b/.project new file mode 100644 index 0000000..da2f213 --- /dev/null +++ b/.project @@ -0,0 +1,28 @@ + + + interview + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.springframework.ide.eclipse.boot.validation.springbootbuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..36c90d8 --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF8 diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..2f5cc74 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/.settings/org.springframework.ide.eclipse.prefs b/.settings/org.springframework.ide.eclipse.prefs new file mode 100644 index 0000000..a12794d --- /dev/null +++ b/.settings/org.springframework.ide.eclipse.prefs @@ -0,0 +1,2 @@ +boot.validation.initialized=true +eclipse.preferences.version=1 diff --git a/pom.xml b/pom.xml index a8ffa1a..e0f256d 100644 --- a/pom.xml +++ b/pom.xml @@ -1,162 +1,178 @@ - - 4.0.0 + + 4.0.0 - com.devexperts - interview - 0.1.0 + com.devexperts + interview + 0.1.0 - - 1.8 - 3.8.0 - 2.22.2 - 2.1.6.RELEASE - 5.5.2 - 1.5.2 - 1.12.3 - + + 1.8 + 3.8.0 + 2.22.2 + 2.1.6.RELEASE + 5.5.2 + 1.5.2 + 1.12.3 + - - - - - org.springframework.boot - spring-boot-starter-parent - ${spring.boot.version} - import - pom - - - + + + + + org.springframework.boot + spring-boot-starter-parent + ${spring.boot.version} + import + pom + + + - - - - org.junit.jupiter - junit-jupiter-api - ${junit.version} - test - - - org.junit.jupiter - junit-jupiter-engine - ${junit.version} - test - - - org.junit.platform - junit-platform-commons - ${junit.platform.version} - test - - - org.junit.platform - junit-platform-engine - ${junit.platform.version} - test - - - org.junit.platform - junit-platform-launcher - ${junit.platform.version} - test - - - - org.testcontainers - junit-jupiter - ${testcontainers.version} - test - - - org.testcontainers - postgresql - ${testcontainers.version} - test - - - - org.postgresql - postgresql - 42.2.5 - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-actuator - - - org.springframework.boot - spring-boot-starter-test - test - - + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.junit.platform + junit-platform-commons + ${junit.platform.version} + test + + + org.junit.platform + junit-platform-engine + ${junit.platform.version} + test + + + org.junit.platform + junit-platform-launcher + ${junit.platform.version} + test + + + + org.testcontainers + junit-jupiter + ${testcontainers.version} + test + + + org.testcontainers + postgresql + ${testcontainers.version} + test + + + + org.postgresql + postgresql + 42.2.5 + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-log4j2 + + + org.projectlombok + lombok + provided + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-test + test + + - - - - org.apache.maven.plugins - maven-compiler-plugin - ${maven-compiler-plugin.version} - - ${java.version} - ${java.version} - true - UTF8 - false - - - - - default-compile - none - - - - default-testCompile - none - - - java-compile - compile - - compile - - - - java-test-compile - test-compile - - testCompile - - - - + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${java.version} + ${java.version} + true + UTF8 + false + + + + + default-compile + none + + + + default-testCompile + none + + + java-compile + compile + + compile + + + + java-test-compile + test-compile + + testCompile + + + + - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - - org.apache.maven.plugins - maven-failsafe-plugin - ${maven-surefire-plugin.version} - - - + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + org.apache.maven.plugins + maven-failsafe-plugin + ${maven-surefire-plugin.version} + + + - - - jcenter-snapshots - jcenter - https://jcenter.bintray.com/ - - + + + jcenter-snapshots + jcenter + https://jcenter.bintray.com/ + + diff --git a/src/main/java/com/devexperts/account/Account.java b/src/main/java/com/devexperts/account/Account.java index fb2a3af..a803611 100644 --- a/src/main/java/com/devexperts/account/Account.java +++ b/src/main/java/com/devexperts/account/Account.java @@ -1,35 +1,36 @@ package com.devexperts.account; +import javax.validation.constraints.NotBlank; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@ToString +@EqualsAndHashCode +@AllArgsConstructor +@Getter public class Account { - private final AccountKey accountKey; + private final long ID; + + @NotBlank private final String firstName; + + @NotBlank private final String lastName; + private Double balance; - - public Account(AccountKey accountKey, String firstName, String lastName, Double balance) { - this.accountKey = accountKey; - this.firstName = firstName; - this.lastName = lastName; - this.balance = balance; + + public void increaseBalance(double balance) { + this.balance += balance; } - - public AccountKey getAccountKey() { - return accountKey; - } - - public String getFirstName() { - return firstName; + + public void decreaseBalance(double balance) { + this.balance -= balance; } - public String getLastName() { - return lastName; - } - - public Double getBalance() { - return balance; - } - - public void setBalance(Double balance) { - this.balance = balance; - } + public void setBalance(Double balance) { + this.balance = balance; + } } diff --git a/src/main/java/com/devexperts/account/AccountKey.java b/src/main/java/com/devexperts/account/AccountKey.java index 1b0a233..e4e9414 100644 --- a/src/main/java/com/devexperts/account/AccountKey.java +++ b/src/main/java/com/devexperts/account/AccountKey.java @@ -1,5 +1,9 @@ package com.devexperts.account; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + /** * Unique Account identifier * @@ -7,6 +11,9 @@ * 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. * */ +@Getter +@ToString +@EqualsAndHashCode public class AccountKey { private final long accountId; diff --git a/src/main/java/com/devexperts/exception/AccountNotRegisteredException.java b/src/main/java/com/devexperts/exception/AccountNotRegisteredException.java new file mode 100644 index 0000000..4393a83 --- /dev/null +++ b/src/main/java/com/devexperts/exception/AccountNotRegisteredException.java @@ -0,0 +1,11 @@ +package com.devexperts.exception; + +public class AccountNotRegisteredException extends RuntimeException { + + private static final long serialVersionUID = -4875504762856220676L; + + public AccountNotRegisteredException(String message) { + super(message); + } + +} diff --git a/src/main/java/com/devexperts/exception/NotEnoughAmountException.java b/src/main/java/com/devexperts/exception/NotEnoughAmountException.java new file mode 100644 index 0000000..5fd6541 --- /dev/null +++ b/src/main/java/com/devexperts/exception/NotEnoughAmountException.java @@ -0,0 +1,11 @@ +package com.devexperts.exception; + +public class NotEnoughAmountException extends RuntimeException{ + + private static final long serialVersionUID = 8014104003977157184L; + + public NotEnoughAmountException(String message) { + super(message); + } + +} diff --git a/src/main/java/com/devexperts/exception/ParametersInvalidException.java b/src/main/java/com/devexperts/exception/ParametersInvalidException.java new file mode 100644 index 0000000..632a578 --- /dev/null +++ b/src/main/java/com/devexperts/exception/ParametersInvalidException.java @@ -0,0 +1,11 @@ +package com.devexperts.exception; + +public class ParametersInvalidException extends RuntimeException { + + private static final long serialVersionUID = 3414091812324627216L; + + public ParametersInvalidException(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..2b36c50 100644 --- a/src/main/java/com/devexperts/rest/AccountController.java +++ b/src/main/java/com/devexperts/rest/AccountController.java @@ -1,14 +1,28 @@ package com.devexperts.rest; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.devexperts.service.AccountService; + @RestController @RequestMapping("/api") public class AccountController extends AbstractAccountController { - public ResponseEntity transfer(long sourceId, long targetId, double amount) { - return null; - } + @Autowired + private AccountService accountService; + + @PutMapping("/options/transfer") + public ResponseEntity transfer(@RequestParam("source_id") long sourceId, + @RequestParam("target_id") long targetId, @RequestParam("amount") double amount) { + + accountService.transfer(accountService.getAccount(sourceId), accountService.getAccount(sourceId), amount); + + return ResponseEntity.ok().build(); + } + } diff --git a/src/main/java/com/devexperts/rest/ExceptionHandler.java b/src/main/java/com/devexperts/rest/ExceptionHandler.java new file mode 100644 index 0000000..0bd15c0 --- /dev/null +++ b/src/main/java/com/devexperts/rest/ExceptionHandler.java @@ -0,0 +1,50 @@ +package com.devexperts.rest; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import com.devexperts.exception.AccountNotRegisteredException; +import com.devexperts.exception.NotEnoughAmountException; +import com.devexperts.exception.ParametersInvalidException; + +@ControllerAdvice +public class ExceptionHandler extends ResponseEntityExceptionHandler{ + + @org.springframework.web.bind.annotation.ExceptionHandler(AccountNotRegisteredException.class) + protected ResponseEntity accountNotRegistered(AccountNotRegisteredException e, WebRequest request) { + return handleExceptionInternal( + e, + e.getMessage(), + HttpHeaders.EMPTY, + HttpStatus.NOT_FOUND, + request + ); + } + + @org.springframework.web.bind.annotation.ExceptionHandler(value = NotEnoughAmountException.class) + protected ResponseEntity amountNotEnough(NotEnoughAmountException e, WebRequest request) { + return handleExceptionInternal( + e, + e.getMessage(), + HttpHeaders.EMPTY, + HttpStatus.INTERNAL_SERVER_ERROR, + request + ); + } + + @org.springframework.web.bind.annotation.ExceptionHandler(value = ParametersInvalidException.class) + protected ResponseEntity invalidParams(ParametersInvalidException e, WebRequest request) { + return handleExceptionInternal( + e, + e.getMessage(), + HttpHeaders.EMPTY, + HttpStatus.BAD_REQUEST, + request + ); + } + +} diff --git a/src/main/java/com/devexperts/service/AccountServiceImpl.java b/src/main/java/com/devexperts/service/AccountServiceImpl.java index 91261ba..aefeab6 100644 --- a/src/main/java/com/devexperts/service/AccountServiceImpl.java +++ b/src/main/java/com/devexperts/service/AccountServiceImpl.java @@ -1,37 +1,67 @@ package com.devexperts.service; -import com.devexperts.account.Account; -import com.devexperts.account.AccountKey; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.List; +import com.devexperts.account.Account; +import com.devexperts.exception.AccountNotRegisteredException; +import com.devexperts.exception.NotEnoughAmountException; +import com.devexperts.exception.ParametersInvalidException; + +import lombok.extern.slf4j.Slf4j; @Service +@Slf4j public class AccountServiceImpl implements AccountService { + private final Map allAccounts = new ConcurrentHashMap<>(); + + @Override + public void clear() { + allAccounts.clear(); + } + + @Override + public void createAccount(Account account) { + allAccounts.putIfAbsent(account.getID(), account); + } + + @Override + public Account getAccount(long id) { + return allAccounts.get(id); + } - 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 - } + @Override + public void transfer(Account source, Account target, double amount) { + validateAndExecuteTransfer(source, target, amount); + } + + private void validateAndExecuteTransfer(Account source, Account target, double amount) { + + if(!(isAccountCreated(source) && isAccountCreated(target))) { + log.error("Cannot procede with transfer, one of the accounts is not registered"); + throw new AccountNotRegisteredException("Cannot procede with transfer, one of the accounts is not registered"); + } + + if(checkParams(source) && checkParams(target)) { + log.error("Invalid parameters"); + throw new ParametersInvalidException("Invalid parameters"); + } + + if(source.getBalance() < amount) { + log.error(String.format("% as balance is more than the balance of the sourse", amount)); + throw new NotEnoughAmountException(String.format("% as balance is more than the balance of the sourse", amount)); + } + allAccounts.get(source.getID()).decreaseBalance(amount); + allAccounts.get(target.getID()).increaseBalance(amount); + } + + private boolean isAccountCreated(Account account) { + return allAccounts.containsKey(account.getID()); + } + + private boolean checkParams(Account account) { + return account.getBalance() < 0 && account.getFirstName().isEmpty() && account.getLastName().isEmpty(); + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..a3ac65c --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ +server.port=8080 \ No newline at end of file diff --git a/src/main/resources/sql/data/accounts.sql b/src/main/resources/sql/data/accounts.sql new file mode 100644 index 0000000..608aff3 --- /dev/null +++ b/src/main/resources/sql/data/accounts.sql @@ -0,0 +1,9 @@ +CREATE TABLE accounts +( + id bigint NOT NULL, + firstName character varying(255), + lastName character varying(255), + balance DOUBLE PRECISION, + + CONSTRAINT accounts_pkey PRIMARY KEY (id) +); \ 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..9346599 --- /dev/null +++ b/src/main/resources/sql/data/transfers.sql @@ -0,0 +1,9 @@ +create table transfers +( + id bigint NOT NULL, + source_id id not null references accounts, + target_id id not null references accounts, + amount DOUBLE PRESCISION not null, + transfer_time TIMESTAMP not null + CONSTRAINT transfers_pkey PRIMARY KEY (id) +); \ No newline at end of file diff --git a/src/main/resources/sql/select.sql b/src/main/resources/sql/select.sql new file mode 100644 index 0000000..400a0e5 --- /dev/null +++ b/src/main/resources/sql/select.sql @@ -0,0 +1,7 @@ +SELECT * FROM accounts +WHERE id IN + (SELECT id FROM transfers + WHERE transfer_time >= '2019-01-01' + HAVING SUM(amount) >= 1000 + SORT BY transfer_time + ); \ No newline at end of file