diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c2a45ba --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM openjdk:25-ea-4-jdk-oraclelinux9 + +WORKDIR /app + +COPY target/order-0.0.1-SNAPSHOT.jar /app/order-0.0.1-SNAPSHOT.jar + +ENTRYPOINT [ "java", "-jar", "/app/order-0.0.1-SNAPSHOT.jar" ] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c126715 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,44 @@ +services: + backend: + image: openjdk:25-ea-4-jdk-oraclelinux9 + container_name: order + ports: + - "8080:8080" + depends_on: + - order_db + environment: + - SPRING_DATASOURCE_URL=jdbc:postgresql://order_db:5432/orderDB + volumes: + - ./target:/app + - ./logs:/logs + command: [ "java", "-jar", "/app/order-0.0.1-SNAPSHOT.jar" ] + + order_db: + image: postgres:14.17 + container_name: order_db + environment: + POSTGRES_PASSWORD: 1234 + POSTGRES_USER: postgres + POSTGRES_DB: orderDB + ports: + - "5432:5432" + + loki: + image: grafana/loki:3.5.0 + container_name: loki + ports: + - "3100:3100" + command: -config.file=/etc/loki/local-config.yaml + + grafana: + image: grafana/grafana:10.4.1 + container_name: grafana + ports: + - "3000:3000" + depends_on: + - loki + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin + volumes: + - grafana_data:/var/lib/grafana diff --git a/pom.xml b/pom.xml index f342395..a7ebd93 100644 --- a/pom.xml +++ b/pom.xml @@ -39,6 +39,20 @@ springdoc-openapi-starter-webmvc-ui 2.8.5 + + org.springframework.boot + spring-boot-starter-security + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.8.5 + + + net.logstash.logback + logstash-logback-encoder + 7.4 + org.springframework.boot spring-boot-starter-data-jpa diff --git a/src/main/java/com/podzilla/order/controller/OrderController.java b/src/main/java/com/podzilla/order/controller/OrderController.java index 6170b14..dff1524 100644 --- a/src/main/java/com/podzilla/order/controller/OrderController.java +++ b/src/main/java/com/podzilla/order/controller/OrderController.java @@ -1,6 +1,7 @@ package com.podzilla.order.controller; import com.podzilla.order.model.Order; +import com.podzilla.order.model.OrderStatus; import com.podzilla.order.service.OrderService; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.beans.factory.annotation.Autowired; @@ -13,12 +14,16 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.DeleteMapping; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; -import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Optional; + @RestController @RequestMapping("/orders") @Tag(name = "Order API", description = "Order management operations") @@ -28,6 +33,9 @@ public class OrderController { private static final Logger LOGGER = LoggerFactory.getLogger(OrderController.class); + private static final Logger LOGGER = + LoggerFactory.getLogger(OrderController.class); + @Autowired public OrderController(final OrderService orderService) { this.orderService = orderService; @@ -88,4 +96,63 @@ public ResponseEntity deleteOrder(@PathVariable final long id) { orderService.deleteOrder(id); return ResponseEntity.noContent().build(); } + + @GetMapping("/user/{userId}") + @Operation( + summary = "Get order by user ID", + description = "Fetches an order based on the provided user ID" + ) + @ApiResponse( + responseCode = "200", description = "Order found" + ) + public ResponseEntity> getOrderByUserId( + @PathVariable final long userId) { + Optional order = orderService.getOrderByUserId(userId); + LOGGER.info("Order found for user ID: {}", userId); + return ResponseEntity.ok(order); + } + + @PutMapping("/cancel/{id}") + @Operation( + summary = "Cancel order", + description = "Cancels an order based on the provided order ID" + ) + @ApiResponse( + responseCode = "200", description = "Order cancelled" + ) + public ResponseEntity cancelOrder(@PathVariable final long id) { + Order order = orderService.cancelOrder(id); + LOGGER.info("Order with ID: {} cancelled", id); + return ResponseEntity.ok(order); + } + + @PutMapping("/status/{id}") + @Operation( + summary = "Update order status", + description = "Updates the status of an order based on " + + "the provided order ID" + ) + @ApiResponse( + responseCode = "200", description = "Order status updated" + ) + public ResponseEntity updateOrderStatus(@PathVariable final long id, + @RequestBody final OrderStatus status) { + Order order = orderService.updateOrderStatus(id, status); + LOGGER.info("Order status updated for ID: {}", id); + return ResponseEntity.ok(order); + } + + @PostMapping("/checkout/{id}") + @Operation( + summary = "Checkout order", + description = "Checks out an order based on the provided order ID" + ) + @ApiResponse( + responseCode = "200", description = "Order checked out" + ) + public ResponseEntity checkoutOrder(@PathVariable final long id) { + Order order = orderService.checkoutOrder(id); + LOGGER.info("Order with ID: {} checked out", id); + return ResponseEntity.ok(order); + } } diff --git a/src/main/java/com/podzilla/order/exception/ErrorResponse.java b/src/main/java/com/podzilla/order/exception/ErrorResponse.java new file mode 100644 index 0000000..c752990 --- /dev/null +++ b/src/main/java/com/podzilla/order/exception/ErrorResponse.java @@ -0,0 +1,19 @@ +package com.podzilla.order.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +import java.time.LocalDateTime; + +@Getter +public class ErrorResponse { + private final String message; + private final HttpStatus status; + private final LocalDateTime timestamp; + + public ErrorResponse(final String message, final HttpStatus status) { + this.message = message; + this.status = status; + this.timestamp = LocalDateTime.now(); + } +} diff --git a/src/main/java/com/podzilla/order/exception/GlobalExceptionHandler.java b/src/main/java/com/podzilla/order/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..0f19f36 --- /dev/null +++ b/src/main/java/com/podzilla/order/exception/GlobalExceptionHandler.java @@ -0,0 +1,62 @@ +package com.podzilla.order.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@RestControllerAdvice +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + + + @ExceptionHandler(AccessDeniedException.class) + public ResponseEntity handleAccessDeniedException( + final AccessDeniedException exception) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(new ErrorResponse(exception.getMessage(), + HttpStatus.FORBIDDEN)); + } + + @ExceptionHandler(AuthenticationException.class) + public ResponseEntity handleAuthenticationException( + final AuthenticationException exception) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(new ErrorResponse(exception.getMessage(), + HttpStatus.UNAUTHORIZED)); + } + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFoundException( + final NotFoundException exception) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorResponse(exception.getMessage(), + HttpStatus.NOT_FOUND)); + } + + @ExceptionHandler(ValidationException.class) + public ResponseEntity handleValidationException( + final ValidationException exception) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new ErrorResponse(exception.getMessage(), + HttpStatus.BAD_REQUEST)); + } + + @ExceptionHandler(InvalidActionException.class) + public ResponseEntity handleInvalidActionException( + final InvalidActionException exception) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new ErrorResponse(exception.getMessage(), + HttpStatus.BAD_REQUEST)); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException( + final Exception exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new ErrorResponse(exception.getMessage(), + HttpStatus.INTERNAL_SERVER_ERROR)); + } +} diff --git a/src/main/java/com/podzilla/order/exception/InvalidActionException.java b/src/main/java/com/podzilla/order/exception/InvalidActionException.java new file mode 100644 index 0000000..89ce1d5 --- /dev/null +++ b/src/main/java/com/podzilla/order/exception/InvalidActionException.java @@ -0,0 +1,7 @@ +package com.podzilla.order.exception; + +public class InvalidActionException extends RuntimeException { + public InvalidActionException(final String message) { + super("Invalid action: " + message); + } +} diff --git a/src/main/java/com/podzilla/order/exception/NotFoundException.java b/src/main/java/com/podzilla/order/exception/NotFoundException.java new file mode 100644 index 0000000..7a2c0da --- /dev/null +++ b/src/main/java/com/podzilla/order/exception/NotFoundException.java @@ -0,0 +1,7 @@ +package com.podzilla.order.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(final String message) { + super("Not Found: " + message); + } +} diff --git a/src/main/java/com/podzilla/order/exception/ValidationException.java b/src/main/java/com/podzilla/order/exception/ValidationException.java new file mode 100644 index 0000000..f23efb2 --- /dev/null +++ b/src/main/java/com/podzilla/order/exception/ValidationException.java @@ -0,0 +1,7 @@ +package com.podzilla.order.exception; + +public class ValidationException extends RuntimeException { + public ValidationException(final String message) { + super("Validation error: " + message); + } +} diff --git a/src/main/java/com/podzilla/order/model/OrderStatus.java b/src/main/java/com/podzilla/order/model/OrderStatus.java index 9dac508..07de71e 100644 --- a/src/main/java/com/podzilla/order/model/OrderStatus.java +++ b/src/main/java/com/podzilla/order/model/OrderStatus.java @@ -1,10 +1,10 @@ package com.podzilla.order.model; public enum OrderStatus { - PENDING, - CONFIRMED, + PLACED, + CANCELLED, SHIPPED, DELIVERED, - CANCELLED + FAILED } diff --git a/src/main/java/com/podzilla/order/repository/OrderRepository.java b/src/main/java/com/podzilla/order/repository/OrderRepository.java index d9112fc..fd8f9a2 100644 --- a/src/main/java/com/podzilla/order/repository/OrderRepository.java +++ b/src/main/java/com/podzilla/order/repository/OrderRepository.java @@ -4,6 +4,9 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface OrderRepository extends JpaRepository { + Optional findByUserId(Long id); } diff --git a/src/main/java/com/podzilla/order/service/OrderService.java b/src/main/java/com/podzilla/order/service/OrderService.java index e1e27d8..c520689 100644 --- a/src/main/java/com/podzilla/order/service/OrderService.java +++ b/src/main/java/com/podzilla/order/service/OrderService.java @@ -1,16 +1,18 @@ package com.podzilla.order.service; +import com.podzilla.order.exception.NotFoundException; import com.podzilla.order.model.Order; +import com.podzilla.order.model.OrderStatus; import com.podzilla.order.repository.OrderRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; - import java.time.LocalDateTime; import java.util.List; import java.util.Optional; + @Slf4j @Service public class OrderService { @@ -61,4 +63,53 @@ public void deleteOrder(final long id) { throw new RuntimeException("Order not found with id: " + id); } } + + public Optional getOrderByUserId(final long userId) { + log.info("Fetching order with user ID: {}", userId); + + Optional order = orderRepository.findByUserId(userId); + + checkNotFoundException(order.orElse(null), + "Order not found with user ID: " + userId); + + return orderRepository.findByUserId(userId); + } + + + + public Order cancelOrder(final long id) { + log.info("Cancelling order with ID: {}", id); + + Optional existingOrder = orderRepository.findById(id); + + checkNotFoundException(existingOrder.orElse(null), + "Order not found with id: " + id); + + Order order = existingOrder.get(); + order.setStatus(OrderStatus.CANCELLED); + order.setUpdatedAt(LocalDateTime.now()); + return orderRepository.save(order); + } + + public Order updateOrderStatus(final long id, + final OrderStatus status) { + log.info("Updating order status with ID: {}", id); + + Optional existingOrder = orderRepository.findById(id); + + checkNotFoundException(existingOrder.orElse(null), + "Order not found with id: " + id); + + Order order = existingOrder.get(); + order.setStatus(status); + order.setUpdatedAt(LocalDateTime.now()); + return orderRepository.save(order); + } + + private void checkNotFoundException(final Object value, + final String message) { + if (value == null) { + throw new NotFoundException(message); + } + } }