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 fd77e3d..a7ebd93 100644
--- a/pom.xml
+++ b/pom.xml
@@ -34,6 +34,25 @@
org.springframework.boot
spring-boot-starter-data-jdbc
+
+ org.springdoc
+ 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
new file mode 100644
index 0000000..2e21d66
--- /dev/null
+++ b/src/main/java/com/podzilla/order/controller/OrderController.java
@@ -0,0 +1,141 @@
+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;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+@RestController
+@RequestMapping("/orders")
+@Tag(name = "Order API", description = "Order management operations")
+public class OrderController {
+
+ private final OrderService orderService;
+ private static final Logger LOGGER =
+ LoggerFactory.getLogger(OrderController.class);
+
+ @Autowired
+ public OrderController(final OrderService orderService) {
+ this.orderService = orderService;
+ }
+
+ @PostMapping
+ @Operation(summary = "Create a new order",
+ description = "Creates a new order and returns the created order")
+ @ApiResponse(responseCode = "200", description = "Order created "
+ + "successfully")
+ public ResponseEntity createOrder(@RequestBody final Order order) {
+ LOGGER.info("Creating new order: {}", order);
+ Order createdOrder = orderService.createOrder(order);
+ return ResponseEntity.ok(createdOrder);
+ }
+
+ @GetMapping
+ @Operation(summary = "Get all orders",
+ description = "Returns a list of all orders")
+ @ApiResponse(responseCode = "200", description = "List of orders")
+ public ResponseEntity> getAllOrders() {
+ LOGGER.info("Fetching all orders");
+ List orders = orderService.getAllOrders();
+ return ResponseEntity.ok(orders);
+ }
+
+ @GetMapping("/{id}")
+ @Operation(summary = "Get order by ID",
+ description = "Returns an order by its ID")
+ @ApiResponse(responseCode = "200", description = "Order found")
+ public ResponseEntity getOrderById(@PathVariable final UUID id) {
+ LOGGER.info("Fetching order with ID: {}", id);
+ Order order = orderService.getOrderById(id)
+ .orElseThrow(() -> new RuntimeException("Order not found"));
+ return ResponseEntity.ok(order);
+ }
+
+ @PutMapping("/{id}")
+ @Operation(summary = "Update an order",
+ description = "Updates an existing order and returns the updated "
+ + "order")
+ @ApiResponse(responseCode = "200", description = "Order updated "
+ + "successfully")
+ public ResponseEntity updateOrder(@PathVariable final UUID id,
+ @RequestBody final Order order) {
+ LOGGER.info("Updating order with ID: {}", id);
+ Order updatedOrder = orderService.updateOrder(id, order);
+ return ResponseEntity.ok(updatedOrder);
+ }
+
+ @DeleteMapping("/{id}")
+ @Operation(summary = "Delete an order",
+ description = "Deletes an order by its ID")
+ @ApiResponse(responseCode = "204", description = "Order deleted "
+ + "successfully")
+ public ResponseEntity deleteOrder(@PathVariable final UUID id) {
+ LOGGER.info("Deleting order with ID: {}", 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 UUID 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 UUID 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 UUID id,
+ @RequestBody final OrderStatus status) {
+ Order order = orderService.updateOrderStatus(id, status);
+ LOGGER.info("Order status updated for ID: {}", 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/Address.java b/src/main/java/com/podzilla/order/model/Address.java
new file mode 100644
index 0000000..acf13c3
--- /dev/null
+++ b/src/main/java/com/podzilla/order/model/Address.java
@@ -0,0 +1,37 @@
+package com.podzilla.order.model;
+
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.UUID;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "addresses")
+public class Address {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.UUID)
+ private UUID id;
+
+ private String street;
+ private String city;
+ private String state;
+ private String country;
+ private String postalCode;
+
+ @ManyToOne
+ @JoinColumn(name = "order_id", nullable = false)
+ private Order order;
+}
diff --git a/src/main/java/com/podzilla/order/model/Order.java b/src/main/java/com/podzilla/order/model/Order.java
new file mode 100644
index 0000000..fbc14ee
--- /dev/null
+++ b/src/main/java/com/podzilla/order/model/Order.java
@@ -0,0 +1,68 @@
+package com.podzilla.order.model;
+
+
+import jakarta.persistence.Table;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.Id;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.OneToOne;
+import jakarta.persistence.Column;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.GenerationType;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+@Entity
+@Table(name = "orders")
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+public class Order {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private UUID id;
+
+ private UUID userId;
+
+ private double totalAmount;
+
+ @OneToOne(cascade = CascadeType.ALL)
+ private Address shippingAddress;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private OrderStatus status;
+
+ @Column(nullable = false)
+ private LocalDateTime createdAt;
+
+ @Column(nullable = false)
+ private LocalDateTime updatedAt;
+
+ @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval =
+ true)
+ private List orderItems = new ArrayList<>();
+
+ public Order(final UUID userId, final double totalAmount,
+ final OrderStatus status, final List orderItems) {
+ this.userId = userId;
+ this.totalAmount = totalAmount;
+ this.status = status;
+ this.createdAt = LocalDateTime.now();
+ this.updatedAt = LocalDateTime.now();
+ this.orderItems = orderItems;
+ }
+
+}
diff --git a/src/main/java/com/podzilla/order/model/OrderItem.java b/src/main/java/com/podzilla/order/model/OrderItem.java
new file mode 100644
index 0000000..0d7a03c
--- /dev/null
+++ b/src/main/java/com/podzilla/order/model/OrderItem.java
@@ -0,0 +1,41 @@
+package com.podzilla.order.model;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.persistence.Column;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.FetchType;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Table(name = "order_items")
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+public class OrderItem {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(nullable = false)
+ private long productId;
+
+ @Column(nullable = false)
+ private int quantity;
+
+ @Column(nullable = false)
+ private double price;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "order_id", nullable = false)
+ private Order order;
+}
diff --git a/src/main/java/com/podzilla/order/model/OrderStatus.java b/src/main/java/com/podzilla/order/model/OrderStatus.java
new file mode 100644
index 0000000..07de71e
--- /dev/null
+++ b/src/main/java/com/podzilla/order/model/OrderStatus.java
@@ -0,0 +1,10 @@
+package com.podzilla.order.model;
+
+public enum OrderStatus {
+ PLACED,
+ CANCELLED,
+ SHIPPED,
+ DELIVERED,
+ FAILED
+}
+
diff --git a/src/main/java/com/podzilla/order/repository/OrderRepository.java b/src/main/java/com/podzilla/order/repository/OrderRepository.java
new file mode 100644
index 0000000..106d6d0
--- /dev/null
+++ b/src/main/java/com/podzilla/order/repository/OrderRepository.java
@@ -0,0 +1,13 @@
+package com.podzilla.order.repository;
+
+import com.podzilla.order.model.Order;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+import java.util.UUID;
+
+@Repository
+public interface OrderRepository extends JpaRepository {
+ Optional findByUserId(UUID id);
+}
diff --git a/src/main/java/com/podzilla/order/service/OrderService.java b/src/main/java/com/podzilla/order/service/OrderService.java
new file mode 100644
index 0000000..fab9aba
--- /dev/null
+++ b/src/main/java/com/podzilla/order/service/OrderService.java
@@ -0,0 +1,114 @@
+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;
+import java.util.UUID;
+
+
+@Slf4j
+@Service
+public class OrderService {
+ private final OrderRepository orderRepository;
+
+ @Autowired
+ public OrderService(final OrderRepository orderRepository) {
+ this.orderRepository = orderRepository;
+ }
+
+ public Order createOrder(final Order order) {
+ log.info("Creating new order: {}", order);
+ order.setCreatedAt(LocalDateTime.now());
+ order.setUpdatedAt(LocalDateTime.now());
+ return orderRepository.save(order);
+ }
+
+ public List getAllOrders() {
+ log.info("Fetching all orders");
+ return orderRepository.findAll();
+ }
+
+ public Optional getOrderById(final UUID id) {
+ log.info("Fetching order with ID: {}", id);
+ return orderRepository.findById(id);
+ }
+
+ public Order updateOrder(final UUID id, final Order updatedOrder) {
+ log.info("Updating order with ID: {}", id);
+ Optional existingOrder = orderRepository.findById(id);
+ if (existingOrder.isPresent()) {
+ Order order = existingOrder.get();
+ BeanUtils.copyProperties(updatedOrder, order, "id");
+ order.setUpdatedAt(LocalDateTime.now());
+ log.info("Order with id: {} was found and updated", id);
+ return orderRepository.save(order);
+ }
+ log.warn("Order with id: {} was not found", id);
+ throw new RuntimeException("Order not found with id: " + id);
+ }
+
+ public void deleteOrder(final UUID id) {
+ if (orderRepository.existsById(id)) {
+ log.warn("Deleting order with ID: {}", id);
+ orderRepository.deleteById(id);
+ } else {
+ log.warn("Order with ID: {} not found for deletion", id);
+ throw new RuntimeException("Order not found with id: " + id);
+ }
+ }
+
+ public Optional getOrderByUserId(final UUID 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 UUID 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 UUID 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);
+ }
+ }
+}