diff --git a/.gitignore b/.gitignore
index 5eac309..773a43e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,7 @@
+# Ignore Maven build output
+/target/
+/logs/
+
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
@@ -30,4 +34,5 @@ build/
!**/src/test/**/build/
### VS Code ###
-.vscode/
\ No newline at end of file
+.vscode/
+
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..3f44e05
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..aa00ffa
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 0000000..712ab9d
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..0e6e319
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..fd7dc73
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,59 @@
+version: '3.8'
+
+services:
+
+ cart-service:
+ image: openjdk:25-ea-4-jdk-oraclelinux9
+ container_name: cart-service
+ ports:
+ - "8081:8080"
+ environment:
+ SPRING_DATA_MONGODB_URI: mongodb://cart-db:27017/cartDB
+ SPRING_DATA_MONGODB_DATABASE: cartDB
+ depends_on:
+ - cart-db
+ volumes:
+ - ./target:/app
+ - ./logs:/logs
+ command: ["java", "-jar", "/app/cart-0.0.1-SNAPSHOT.jar"]
+
+ cart-db:
+ image: mongo:8.0.9
+ container_name: cart-db
+ environment:
+ MONGO_INITDB_DATABASE: cartDB
+ ports:
+ - "27018:27017"
+ volumes:
+ - cart-mongo-data:/data/db
+
+ # Reuse Loki and Promtail from your existing setup
+ loki:
+ image: grafana/loki:latest
+ container_name: loki
+ ports:
+ - "3100:3100"
+ command:
+ - -config.file=/etc/loki/local-config.yaml
+
+ promtail:
+ image: grafana/promtail:latest
+ container_name: promtail
+ volumes:
+ - ./promtail.yml:/etc/promtail/promtail-config.yaml
+ - ./logs:/logs
+ command:
+ - -config.file=/etc/promtail/promtail-config.yaml
+ depends_on:
+ - loki
+
+ grafana:
+ image: grafana/grafana:latest
+ container_name: grafana
+ ports:
+ - "3000:3000"
+ depends_on:
+ - loki
+
+volumes:
+ cart-mongo-data:
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..337a47c
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,179 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.4.5
+
+
+
+ com.podzilla
+ cart
+ 0.0.1-SNAPSHOT
+ cart
+ This is the cart service for Podzilla
+
+
+ 23
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-mongodb
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ jakarta.validation
+ jakarta.validation-api
+ 3.0.2
+
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.8.5
+
+
+
+
+ net.logstash.logback
+ logstash-logback-encoder
+ 7.4
+
+
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ runtime
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.10.0
+ test
+
+
+
+ org.mockito
+ mockito-core
+ 5.5.0
+ test
+
+
+
+ org.mockito
+ mockito-junit-jupiter
+ 5.5.0
+ test
+
+
+
+ de.flapdoodle.embed
+ de.flapdoodle.embed.mongo
+ 4.9.3
+ test
+
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
+
+
+
+ com.github.tomakehurst
+ wiremock-jre8
+ 2.35.0
+ test
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-mongodb
+
+
+
+ de.flapdoodle.embed
+ de.flapdoodle.embed.mongo
+ 4.9.3
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ com.github.tomakehurst
+ wiremock-jre8
+ 2.35.0
+ test
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+
diff --git a/promtail.yml b/promtail.yml
new file mode 100644
index 0000000..1ede004
--- /dev/null
+++ b/promtail.yml
@@ -0,0 +1,18 @@
+server:
+ http_listen_port: 9080
+ grpc_listen_port: 0
+
+positions:
+ filename: /tmp/positions.yaml
+
+clients:
+ - url: http://loki:3100/loki/api/v1/push
+
+scrape_configs:
+ - job_name: cart-service
+ static_configs:
+ - targets:
+ - localhost
+ labels:
+ job: cart-service
+ __path__: ./logs/*.log
\ No newline at end of file
diff --git a/src/main/java/cart/CartApplication.java b/src/main/java/cart/CartApplication.java
new file mode 100644
index 0000000..2ef8d6d
--- /dev/null
+++ b/src/main/java/cart/CartApplication.java
@@ -0,0 +1,13 @@
+package cart;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
+
+@SpringBootApplication
+@EnableMongoRepositories(basePackages = "cart.repository")
+public class CartApplication {
+ public static void main(final String[] args) {
+ SpringApplication.run(CartApplication.class, args);
+ }
+}
diff --git a/src/main/java/cart/config/AppConfig.java b/src/main/java/cart/config/AppConfig.java
new file mode 100644
index 0000000..70500cc
--- /dev/null
+++ b/src/main/java/cart/config/AppConfig.java
@@ -0,0 +1,15 @@
+package cart.config;
+
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+public class AppConfig {
+
+ @Bean
+ public RestTemplate restTemplate() {
+ return new RestTemplate();
+ }
+}
diff --git a/src/main/java/cart/controller/CartController.java b/src/main/java/cart/controller/CartController.java
new file mode 100644
index 0000000..6260409
--- /dev/null
+++ b/src/main/java/cart/controller/CartController.java
@@ -0,0 +1,241 @@
+package cart.controller;
+
+
+
+import cart.model.Cart;
+import cart.service.CartService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PatchMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RequestBody;
+import cart.model.CartItem;
+import io.swagger.v3.oas.annotations.media.Content;
+
+@RestController
+@RequestMapping("/api/carts")
+@RequiredArgsConstructor
+@Tag(name = "Cart Controller", description = "Handles cart"
+ + " operations like add, update,"
+ + " remove items and manage cart")
+@Slf4j
+public class CartController {
+
+ private final CartService cartService;
+
+ @Operation(summary = "Create a new cart for a "
+ + "customer or return existing one")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200",
+ description = "Cart created or retrieved successfully"),
+ @ApiResponse(responseCode = "400",
+ description = "Invalid customer ID provided",
+ content = @Content),
+ @ApiResponse(responseCode = "500",
+ description = "Internal server error",
+ content = @Content)
+ })
+ @PostMapping("/create/{customerId}")
+ public ResponseEntity createCart(
+ @PathVariable("customerId") final String customerId) {
+ log.debug("Entering createCart endpoint"
+ + " with customerId:", customerId);
+ Cart cart = cartService.createCart(customerId);
+ log.debug("Cart created or retrieved:", cart);
+ return ResponseEntity.ok(cart);
+ }
+
+ @Operation(summary = "Get cart by customer ID")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200",
+ description = "Cart retrieved successfully"),
+ @ApiResponse(responseCode = "404",
+ description = "Cart not found for this customer")
+ })
+ @GetMapping("/customer/{customerId}")
+ public ResponseEntity getCartByCustomerId(
+ @PathVariable("customerId") final String customerId) {
+ log.debug("Entering getCartByCustomerId"
+ + " endpoint with customerId:",
+ customerId);
+ Cart cart = cartService.getCartByCustomerId(customerId);
+ log.debug("Cart retrieved:", cart);
+ return ResponseEntity.ok(cart);
+ }
+
+ @Operation(summary = "Delete cart by customer ID")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "204",
+ description = "Cart deleted successfully"),
+ @ApiResponse(responseCode = "404",
+ description = "Cart not found")
+ })
+ @DeleteMapping("/customer/{customerId}")
+ public ResponseEntity deleteCart(
+ @PathVariable("customerId") final String customerId) {
+ log.debug("Entering deleteCart end"
+ + "point with customerId:", customerId);
+ cartService.deleteCartByCustomerId(customerId);
+ log.debug("Cart deleted for customerId:",
+ customerId);
+
+ return ResponseEntity.noContent().build();
+ }
+
+ @Operation(summary = "Add an item to the cart"
+ + " or update its quantity if already exists")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200",
+ description = "Item added or updated successfully"),
+ @ApiResponse(responseCode = "400",
+ description = "Invalid item data provided"),
+ @ApiResponse(responseCode = "404",
+ description = "Cart not found for this customer")
+ })
+ @PostMapping("/{customerId}/items")
+ public ResponseEntity addItemToCart(
+ @PathVariable("customerId") final String customerId,
+ @RequestBody final CartItem cartItem) {
+ log.debug("Entering addItemToCart"
+ + " endpoint with customerId: {},"
+ + " cartItem: {}", customerId, cartItem);
+ Cart updatedCart = cartService.addItemToCart(customerId, cartItem);
+ log.debug("Cart updated with new item: {}", updatedCart);
+ return ResponseEntity.ok(updatedCart);
+ }
+
+ @Operation(summary = "Update quantity "
+ + "of an existing item in the cart")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200",
+ description = "Quantity updated successfully"),
+ @ApiResponse(responseCode = "400",
+ description = "Invalid quantity value"),
+ @ApiResponse(responseCode = "404",
+ description = "Cart or item not found")
+ })
+ @PatchMapping("/{customerId}/items/{productId}")
+ public ResponseEntity updateItemQuantity(
+ @PathVariable("customerId") final String customerId,
+ @PathVariable("productId") final String productId,
+ @RequestParam final int quantity) {
+ log.debug("Entering updateItemQuantity"
+ + " endpoint with customerId:,"
+ + " productId: {}, quantity: {}",
+ customerId, productId, quantity);
+ Cart updatedCart = cartService.updateItemQuantity(
+ customerId, productId, quantity);
+ log.debug("Cart updated with new quantity:",
+ updatedCart);
+ return ResponseEntity.ok(updatedCart);
+ }
+
+ @Operation(summary = "Remove an item from the cart")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200",
+ description = "Item removed successfully"),
+ @ApiResponse(responseCode = "404",
+ description = "Cart or item not found")
+ })
+ @DeleteMapping("/{customerId}/items/{productId}")
+ public ResponseEntity removeItemFromCart(
+ @PathVariable("customerId") final String customerId,
+ @PathVariable("productId") final String productId) {
+ log.debug("Entering removeItemFromCart"
+ + " endpoint with customerId:,"
+ + " productId:", customerId, productId);
+ Cart updatedCart = cartService
+ .removeItemFromCart(customerId, productId);
+ log.debug("Cart updated after item removal:",
+ updatedCart);
+ return ResponseEntity.ok(updatedCart);
+ }
+
+ @Operation(summary = "Clear all items from the cart")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "204",
+ description = "Cart cleared successfully"),
+ @ApiResponse(responseCode = "404",
+ description = "Cart not found")
+ })
+ @DeleteMapping("/{customerId}/clear")
+ public ResponseEntity clearCart(
+ @PathVariable("customerId") final String customerId) {
+ log.debug("Entering clearCart"
+ + " endpoint with customerId:", customerId);
+ cartService.clearCart(customerId);
+ log.debug("Cart cleared for customerId:", customerId);
+ return ResponseEntity.noContent().build();
+ }
+
+ @Operation(summary = "Archive the cart (soft-delete)")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200",
+ description = "Cart successfully archived"),
+ @ApiResponse(responseCode = "404",
+ description = "Cart not found")
+ })
+ @PatchMapping("/{customerId}/archive")
+ public ResponseEntity archiveCart(
+ @PathVariable("customerId") final String customerId) {
+ log.debug("Entering archiveCart"
+ + " endpoint with customerId:", customerId);
+ Cart archivedCart = cartService.archiveCart(customerId);
+ log.debug("Cart archived:", archivedCart);
+ return ResponseEntity.ok(archivedCart);
+ }
+
+ @Operation(summary = "Unarchive a previously archived cart")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200",
+ description = "Cart successfully unarchived"),
+ @ApiResponse(responseCode = "404",
+ description = "Archived cart not found")
+ })
+ @PatchMapping("/{customerId}/unarchive")
+ public ResponseEntity unarchiveCart(
+ @PathVariable("customerId") final String customerId) {
+ log.debug("Entering unarchiveCart"
+ + " endpoint with customerId:", customerId);
+ Cart activeCart = cartService.unarchiveCart(customerId);
+ log.debug("Cart unarchived:", activeCart);
+ return ResponseEntity.ok(activeCart);
+ }
+
+ @Operation(summary = "Checkout cart by sending it to the Order Service")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200",
+ description = "Cart checked out and sent to Order Service"),
+ @ApiResponse(responseCode = "404",
+ description = "Cart not found"),
+ @ApiResponse(responseCode = "500",
+ description = "Failed to communicate with Order Service")
+ })
+ @PostMapping("/{customerId}/checkout")
+ public ResponseEntity checkoutCart(
+ @PathVariable("customerId") final String customerId) {
+ log.debug("Entering checkoutCart"
+ + " endpoint with customerId:", customerId);
+ try {
+ Cart updatedCart = cartService.checkoutCart(customerId);
+ log.debug("Cart checked out: {}", updatedCart);
+ return ResponseEntity.ok(updatedCart);
+ } catch (Exception ex) {
+ log.error("Error during checkout"
+ + " for customerId: {}", customerId, ex);
+ throw new IllegalCallerException("Error "
+ + "communicating with Order Service");
+ }
+ }
+}
diff --git a/src/main/java/cart/exception/GlobalExceptionHandler.java b/src/main/java/cart/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000..4b03133
--- /dev/null
+++ b/src/main/java/cart/exception/GlobalExceptionHandler.java
@@ -0,0 +1,24 @@
+package cart.exception;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@ControllerAdvice
+public class GlobalExceptionHandler
+ extends ResponseEntityExceptionHandler {
+ @ExceptionHandler(GlobalHandlerException.class)
+ public ResponseEntity