Skip to content
This repository has been archived by the owner on Jan 17, 2022. It is now read-only.

VP - Implement Tasks 1-5; Add DB scripts; Expose Rest API; Refactor a… #40

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,13 @@ should be added:

**Goal:** Provide SQL to create both tables (`resources/sql/data/accounts.sql`) and (`resources/sql/data/transfers.sql`)
and sql query (`resources/sql/select.sql`) that finds all accounts
that in total transferred more than 1000$ to other people starting from 2019-01-01
that in total transferred more than 1000$ to other people starting from 2019-01-01

## Solution

Exposed REST API is accessible through Swagger API. On your local environment you can access it [on the following URL](http://localhost:8080/swagger-ui.html#/operations-controller/transferUsingPOST)

### Technical notes

- additional `insert-data.sql` script is provided which creates a few accounts into the database
- For the sake of the future integration between the Java POJOs and Database a JPA mapping is provided for `Account` and `AccountKey` entities
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ bootJar {
dependencies {
compile("org.springframework.boot:spring-boot-starter-web")
compile("org.springframework.boot:spring-boot-starter-actuator")
compile('io.springfox:springfox-swagger2:2.9.2')
compile('io.springfox:springfox-swagger-ui:2.9.2')
compile('javax.persistence:javax.persistence-api:2.2')

testCompile("org.springframework.boot:spring-boot-starter-test")
testCompile "org.mockito:mockito-core:2.23.0"
Expand Down
8 changes: 0 additions & 8 deletions src/main/java/com/devexperts/ApplicationRunner.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
package com.devexperts;

import com.devexperts.service.AccountService;
import com.devexperts.service.AccountServiceImpl;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class ApplicationRunner {
public static void main(String[] args) {
SpringApplication.run(ApplicationRunner.class, args);
}

@Bean
AccountService accountService() {
return new AccountServiceImpl();
}
}
9 changes: 9 additions & 0 deletions src/main/java/com/devexperts/account/Account.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
package com.devexperts.account;

import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;

@Entity
public class Account {
@Embedded
private final AccountKey accountKey;
@Column(name = "FIRST_NAME")
private final String firstName;
@Column(name = "LAST_NAME")
private final String lastName;
@Column(name = "BALANCE")
private Double balance;

public Account(AccountKey accountKey, String firstName, String lastName, Double balance) {
Expand Down
26 changes: 26 additions & 0 deletions src/main/java/com/devexperts/account/AccountKey.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package com.devexperts.account;

import javax.persistence.Embeddable;
import javax.persistence.Id;
import java.util.Objects;

/**
* Unique Account identifier
*
* <p>
* 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.
* */
@Embeddable
public class AccountKey {
@Id
private final long accountId;

private AccountKey(long accountId) {
Expand All @@ -17,4 +23,24 @@ private AccountKey(long accountId) {
public static AccountKey valueOf(long accountId) {
return new AccountKey(accountId);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof AccountKey)) return false;
AccountKey that = (AccountKey) o;
return accountId == that.accountId;
}

@Override
public int hashCode() {
return Objects.hash(accountId);
}

@Override
public String toString() {
return "AccountKey{" +
"accountId=" + accountId +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.devexperts.configuration;

import com.devexperts.service.AccountService;
import com.devexperts.service.AccountServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ApplicationConfiguration {
@Bean
public AccountService accountService() {
return new AccountServiceImpl();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.devexperts.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfiguration {
private final static String CONTROLLER_LAYER_PACKAGE = "com.devexperts.rest.controller";
private final static String API_VERSION = "1.0";
private final static String API_INFO_TITLE = "API which exposes a simple transfer operation";
private final static String API_INFO_DESCRIPTION = "Simple amount transfer operation";

@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.useDefaultResponseMessages(false)
.select()
.apis(RequestHandlerSelectors.basePackage(CONTROLLER_LAYER_PACKAGE))
.paths(PathSelectors.any())
.build();
}

@Bean
public ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title(API_INFO_TITLE)
.version(API_VERSION)
.description(API_INFO_DESCRIPTION)
.build();
}
}
7 changes: 7 additions & 0 deletions src/main/java/com/devexperts/exception/BalanceException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.devexperts.exception;

public class BalanceException extends RuntimeException {
public BalanceException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.devexperts.exception;

public class EntityNotFoundException extends RuntimeException {
public EntityNotFoundException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.devexperts.exception;

public class InvalidAccountException extends RuntimeException {
public InvalidAccountException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.devexperts.exception;

public class InvalidAmountException extends RuntimeException {
public InvalidAmountException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.devexperts.exception.handler;

import com.devexperts.rest.controller.OperationsController;
import com.devexperts.exception.BalanceException;
import com.devexperts.exception.EntityNotFoundException;
import com.devexperts.exception.InvalidAmountException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

@ControllerAdvice
public class GlobalExceptionHandler {
private final static Logger LOGGER = LoggerFactory.getLogger(OperationsController.class);

@ExceptionHandler
@ResponseBody
@ResponseStatus(code = HttpStatus.UNPROCESSABLE_ENTITY)
protected ResponseEntity handleBalanceException(BalanceException ex) {
return handleException(HttpStatus.INTERNAL_SERVER_ERROR, ex);
}

@ExceptionHandler
@ResponseBody
@ResponseStatus(code = HttpStatus.NOT_FOUND)
protected ResponseEntity handleEntityNotFoundException(EntityNotFoundException ex) {
return handleException(HttpStatus.NOT_FOUND, ex);
}

@ExceptionHandler
@ResponseBody
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
protected ResponseEntity handleInvalidAmountException(InvalidAmountException ex) {
return handleException(HttpStatus.BAD_REQUEST, ex);
}

private class ResponseEntity {
private int code;
private String message;

public ResponseEntity(HttpStatus code, String message){
this.code = code.value();
this.message = message;
}

public int getCode() {
return code;
}

public void setCode(int code) {
this.code = code;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}
}

private <T extends Exception> ResponseEntity handleException(HttpStatus statusCode, T exception) {
logException(exception);
return new ResponseEntity(HttpStatus.BAD_REQUEST, exception.getMessage());
}

private <T extends Exception> void logException(T exception) {
LOGGER.error(exception.getMessage(), exception);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.devexperts.rest.controller;

import com.devexperts.service.AccountService;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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/operations")
public class OperationsController {
private final static Logger LOGGER = LoggerFactory.getLogger(OperationsController.class);

private final AccountService accountService;

@Autowired
public OperationsController(final AccountService accountService) {
this.accountService = accountService;
}

@ApiResponses(
value = {
@ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 400, message = "BAD_REQUEST"),
@ApiResponse(code = 404, message = "NOT_FOUND"),
@ApiResponse(code = 500, message = "INTERNAL_SERVER_ERROR")})
@PostMapping("/transfer")
public void transfer(@RequestParam long sourceId,
@RequestParam long targetId,
@RequestParam long amount) {
LOGGER.trace("New incoming transfer request");

this.accountService.transfer(sourceId, targetId, amount);
}
}
9 changes: 9 additions & 0 deletions src/main/java/com/devexperts/service/AccountService.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,13 @@ public interface AccountService {
* @param amount dollar amount to transfer
* */
void transfer(Account source, Account target, double amount);

/**
* Transfers given amount of money from source account to target account by given ids
*
* @param sourceId account_id to transfer money from
* @param targetId account_id to transfer money to
* @param amount dollar amount to transfer
* */
void transfer(long sourceId, long targetId, double amount);
}
Loading