Skip to content

Commit

Permalink
add validate create order
Browse files Browse the repository at this point in the history
  • Loading branch information
azdanov committed Apr 28, 2024
1 parent 3b6484e commit ff6b5f9
Show file tree
Hide file tree
Showing 12 changed files with 254 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,33 @@
class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

private static final String SERVICE_NAME = "catalog-service";
public static final String CATEGORY_GENERIC = "Generic";
private static final URI INTERNAL_SERVER_ERROR_TYPE = URI.create("https://http.dev/500");
private static final URI NOT_FOUND_TYPE = URI.create("https://http.dev/404");

@ExceptionHandler(Exception.class)
ProblemDetail handleUnhandledException(Exception e) {
ProblemDetail problemDetail =
ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
problemDetail.setTitle("Internal Server Error");
problemDetail.setType(INTERNAL_SERVER_ERROR_TYPE);
problemDetail.setProperty("service", SERVICE_NAME);
problemDetail.setProperty("error_category", "Generic");
problemDetail.setProperty("timestamp", Instant.now());
return problemDetail;
return buildProblemDetail(
HttpStatus.INTERNAL_SERVER_ERROR,
"Internal Server Error",
INTERNAL_SERVER_ERROR_TYPE,
e.getMessage(),
CATEGORY_GENERIC);
}

@ExceptionHandler(ProductNotFoundException.class)
ProblemDetail handleProductNotFoundException(ProductNotFoundException e) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
problemDetail.setTitle("Product Not Found");
problemDetail.setType(NOT_FOUND_TYPE);
return buildProblemDetail(
HttpStatus.NOT_FOUND, "Product Not Found", NOT_FOUND_TYPE, e.getMessage(), CATEGORY_GENERIC);
}

private ProblemDetail buildProblemDetail(
HttpStatus status, String title, URI type, String detail, String errorCategory) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(status, detail);
problemDetail.setTitle(title);
problemDetail.setType(type);
problemDetail.setProperty("service", SERVICE_NAME);
problemDetail.setProperty("error_category", "Generic");
problemDetail.setProperty("error_category", errorCategory);
problemDetail.setProperty("timestamp", Instant.now());
return problemDetail;
}
Expand Down
23 changes: 22 additions & 1 deletion order-service/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
<resilience4j.version>2.2.0</resilience4j.version>
<spotless-maven-plugin.version>2.43.0</spotless-maven-plugin.version>
<palantirJavaFormat.version>2.44.0</palantirJavaFormat.version>
<instancio.version>4.5.0</instancio.version>
<instancio.version>4.5.1</instancio.version>
<wiremock.version>3.5.4</wiremock.version>
<wiremock-testcontainers.version>1.0-alpha-13</wiremock-testcontainers.version>
<dockerImageName>azdanov/bookstore-${project.artifactId}</dockerImageName>
</properties>

Expand Down Expand Up @@ -124,6 +126,18 @@
<version>${instancio.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock-standalone</artifactId>
<version>${wiremock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wiremock.integrations.testcontainers</groupId>
<artifactId>wiremock-testcontainers-module</artifactId>
<version>${wiremock-testcontainers.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down Expand Up @@ -192,5 +206,12 @@
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>maven_central</id>
<name>Maven Central</name>
<url>https://repo.maven.apache.org/maven2/</url>
</repository>
</repositories>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package dev.azdanov.orderservice.clients.catalog;

import dev.azdanov.orderservice.ApplicationProperties;
import java.time.Duration;
import org.springframework.boot.web.client.ClientHttpRequestFactories;
import org.springframework.boot.web.client.ClientHttpRequestFactorySettings;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;

@Configuration
class CatalogServiceClientConfig {

@Bean
RestClient restClient(ApplicationProperties properties) {
return RestClient.builder()
.baseUrl(properties.catalogServiceUrl())
.requestFactory(ClientHttpRequestFactories.get(ClientHttpRequestFactorySettings.DEFAULTS
.withConnectTimeout(Duration.ofSeconds(5))
.withReadTimeout(Duration.ofSeconds(5))))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package dev.azdanov.orderservice.clients.catalog;

import java.math.BigDecimal;

public record Product(String code, String name, String description, String imageUrl, BigDecimal price) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package dev.azdanov.orderservice.clients.catalog;

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.retry.annotation.Retry;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

@Component
public class ProductServiceClient {

private static final Logger log = LoggerFactory.getLogger(ProductServiceClient.class);

private static final String CATALOG_SERVICE = "catalog-service";

private final RestClient restClient;

ProductServiceClient(RestClient restClient) {
this.restClient = restClient;
}

@CircuitBreaker(name = CATALOG_SERVICE)
@Retry(name = CATALOG_SERVICE, fallbackMethod = "findProductByCodeFallback")
public Optional<Product> findProductByCode(String code) {
log.info("Fetching product for code: {}", code);
return Optional.ofNullable(fetchProductByCode(code));
}

private Product fetchProductByCode(String code) {
return restClient.get().uri("/api/v1/products/{code}", code).retrieve().body(Product.class);
}

Optional<Product> findProductByCodeFallback(String code, Throwable t) {
log.warn("Fallback triggered for getProductByCode. Code: {}, Error: {}", code, t.getMessage());
return Optional.empty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package dev.azdanov.orderservice.domain;

public class InvalidOrderException extends RuntimeException {

public InvalidOrderException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,23 @@ public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);

private final OrderRepository orderRepository;
private final OrderValidator orderValidator;

public OrderService(OrderRepository orderRepository) {
public OrderService(OrderRepository orderRepository, OrderValidator orderValidator) {
this.orderRepository = orderRepository;
this.orderValidator = orderValidator;
}

@Transactional
public CreateOrderResponse createOrder(String userName, CreateOrderRequest request) {
orderValidator.validate(request);

OrderEntity newOrder = OrderMapper.convertToEntity(request);
newOrder.setUserName(userName);
OrderEntity savedOrder = this.orderRepository.save(newOrder);
OrderEntity savedOrder = orderRepository.save(newOrder);

log.info("Created Order with orderNumber={}", savedOrder.getOrderNumber());

return new CreateOrderResponse(savedOrder.getOrderNumber());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package dev.azdanov.orderservice.domain;

import dev.azdanov.orderservice.clients.catalog.Product;
import dev.azdanov.orderservice.clients.catalog.ProductServiceClient;
import dev.azdanov.orderservice.domain.models.CreateOrderRequest;
import dev.azdanov.orderservice.domain.models.OrderItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
class OrderValidator {

private static final Logger log = LoggerFactory.getLogger(OrderValidator.class);

private final ProductServiceClient client;

OrderValidator(ProductServiceClient client) {
this.client = client;
}

void validate(CreateOrderRequest request) {
request.items().forEach(this::validateOrderItem);
}

private void validateOrderItem(OrderItem item) {
Product product = getProductByCode(item.code());
validateProductPrice(item, product);
}

private Product getProductByCode(String code) {
return client.findProductByCode(code)
.orElseThrow(() -> new InvalidOrderException("Invalid product code: " + code));
}

private void validateProductPrice(OrderItem item, Product product) {
if (item.price().compareTo(product.price()) != 0) {
log.error(
"Product price not matching. Actual price: {}, received price: {}", product.price(), item.price());
throw new InvalidOrderException("Product price not matching");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.azdanov.orderservice.web.exceptions;

import dev.azdanov.orderservice.domain.InvalidOrderException;
import dev.azdanov.orderservice.domain.OrderNotFoundException;
import java.net.URI;
import java.time.Instant;
Expand All @@ -22,31 +23,36 @@
class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

private static final String SERVICE_NAME = "order-service";
public static final String CATEGORY_GENERIC = "Generic";

private static final URI INTERNAL_SERVER_ERROR_TYPE = URI.create("https://http.dev/500");
private static final URI NOT_FOUND_TYPE = URI.create("https://http.dev/404");
private static final URI BAD_REQUEST_TYPE = URI.create("https://http.dev/400");

@ExceptionHandler(Exception.class)
ProblemDetail handleUnhandledException(Exception e) {
ProblemDetail problemDetail =
ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
problemDetail.setTitle("Internal Server Error");
problemDetail.setType(INTERNAL_SERVER_ERROR_TYPE);
problemDetail.setProperty("service", SERVICE_NAME);
problemDetail.setProperty("error_category", "Generic");
problemDetail.setProperty("timestamp", Instant.now());
return problemDetail;
return buildProblemDetail(
HttpStatus.INTERNAL_SERVER_ERROR,
"Internal Server Error",
INTERNAL_SERVER_ERROR_TYPE,
e.getMessage(),
CATEGORY_GENERIC);
}

@ExceptionHandler(OrderNotFoundException.class)
ProblemDetail handleOrderNotFoundException(OrderNotFoundException e) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
problemDetail.setTitle("Order Not Found");
problemDetail.setType(NOT_FOUND_TYPE);
problemDetail.setProperty("service", SERVICE_NAME);
problemDetail.setProperty("error_category", "Generic");
problemDetail.setProperty("timestamp", Instant.now());
return problemDetail;
return buildProblemDetail(
HttpStatus.NOT_FOUND, "Order Not Found", NOT_FOUND_TYPE, e.getMessage(), CATEGORY_GENERIC);
}

@ExceptionHandler(InvalidOrderException.class)
ProblemDetail handleInvalidOrderException(InvalidOrderException e) {
return buildProblemDetail(
HttpStatus.BAD_REQUEST,
"Invalid Order Creation Request",
BAD_REQUEST_TYPE,
e.getMessage(),
CATEGORY_GENERIC);
}

@Override
Expand All @@ -59,15 +65,21 @@ ProblemDetail handleOrderNotFoundException(OrderNotFoundException e) {
.map(ObjectError::getDefaultMessage)
.toList();

ProblemDetail problemDetail =
ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Invalid request payload");
problemDetail.setTitle("Bad Request");
problemDetail.setType(BAD_REQUEST_TYPE);
ProblemDetail problemDetail = buildProblemDetail(
HttpStatus.BAD_REQUEST, "Bad Request", BAD_REQUEST_TYPE, "Invalid request payload", CATEGORY_GENERIC);
problemDetail.setProperty("errors", errors);
problemDetail.setProperty("service", SERVICE_NAME);
problemDetail.setProperty("error_category", "Generic");
problemDetail.setProperty("timestamp", Instant.now());

return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(problemDetail);
}

private ProblemDetail buildProblemDetail(
HttpStatus status, String title, URI type, String detail, String errorCategory) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(status, detail);
problemDetail.setTitle(title);
problemDetail.setType(type);
problemDetail.setProperty("service", SERVICE_NAME);
problemDetail.setProperty("error_category", errorCategory);
problemDetail.setProperty("timestamp", Instant.now());
return problemDetail;
}
}
10 changes: 10 additions & 0 deletions order-service/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,13 @@ spring.rabbitmq.host=${RABBITMQ_HOST:localhost}
spring.rabbitmq.port=${RABBITMQ_PORT:5672}
spring.rabbitmq.username=${RABBITMQ_USERNAME:guest}
spring.rabbitmq.password=${RABBITMQ_PASSWORD:guest}

resilience4j.retry.backends.catalog-service.max-attempts=2
resilience4j.retry.backends.catalog-service.wait-duration=1s

resilience4j.circuitbreaker.backends.catalog-service.sliding-window-type=COUNT_BASED
resilience4j.circuitbreaker.backends.catalog-service.sliding-window-size=6
resilience4j.circuitbreaker.backends.catalog-service.minimum-number-of-calls=4
resilience4j.circuitbreaker.backends.catalog-service.wait-duration-in-open-state=20s
resilience4j.circuitbreaker.backends.catalog-service.permitted-number-of-calls-in-half-open-state=2
resilience4j.circuitbreaker.backends.catalog-service.failure-rate-threshold=50
Loading

0 comments on commit ff6b5f9

Please sign in to comment.