From 1abdd28e0e7eaf2c3de90fa57f23e5cbc420bd61 Mon Sep 17 00:00:00 2001
From: Abdelrahman Elmeky <65960126+Aelmeky@users.noreply.github.com>
Date: Sat, 3 May 2025 00:36:06 +0300
Subject: [PATCH 01/24] feat : adding the crud operations in all layers
---
.idea/.gitignore | 8 ++
.idea/compiler.xml | 26 ++++
.idea/encodings.xml | 7 ++
.idea/jarRepositories.xml | 20 +++
.idea/misc.xml | 14 +++
.idea/vcs.xml | 6 +
pom.xml | 115 ++++++++++++++++++
.../java/cart/controller/CartController.java | 111 +++++++++++++++++
src/main/java/cart/model/Cart.java | 59 +++++++++
src/main/java/cart/model/CartItem.java | 39 ++++++
.../java/cart/repository/CartRepository.java | 12 ++
src/main/java/cart/service/CartService.java | 93 ++++++++++++++
12 files changed, 510 insertions(+)
create mode 100644 .idea/.gitignore
create mode 100644 .idea/compiler.xml
create mode 100644 .idea/encodings.xml
create mode 100644 .idea/jarRepositories.xml
create mode 100644 .idea/misc.xml
create mode 100644 .idea/vcs.xml
create mode 100644 pom.xml
create mode 100644 src/main/java/cart/controller/CartController.java
create mode 100644 src/main/java/cart/model/Cart.java
create mode 100644 src/main/java/cart/model/CartItem.java
create mode 100644 src/main/java/cart/repository/CartRepository.java
create mode 100644 src/main/java/cart/service/CartService.java
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/pom.xml b/pom.xml
new file mode 100644
index 0000000..67ec612
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,115 @@
+
+
+ 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.springframework.security
+ spring-security-test
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+
diff --git a/src/main/java/cart/controller/CartController.java b/src/main/java/cart/controller/CartController.java
new file mode 100644
index 0000000..9b13d51
--- /dev/null
+++ b/src/main/java/cart/controller/CartController.java
@@ -0,0 +1,111 @@
+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 org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import cart.model.CartItem;
+import io.swagger.v3.oas.annotations.media.Content;
+
+@RestController
+@RequestMapping("/api/cart")
+@RequiredArgsConstructor
+@Tag(name = "Cart Controller", description = "Handles cart operations like add, update, remove items and manage cart")
+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") String customerId) {
+ Cart cart = cartService.createCart(customerId);
+ 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") String customerId) {
+ Cart cart = cartService.getCartByCustomerId(customerId);
+ 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") String customerId) {
+ cartService.deleteCartByCustomerId(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") String customerId,
+ @RequestBody CartItem cartItem) {
+ Cart updatedCart = cartService.addItemToCart(customerId, cartItem);
+ 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") String customerId,
+ @PathVariable("productId") String productId,
+ @RequestParam int quantity) {
+ Cart updatedCart = cartService.updateItemQuantity(customerId, productId, quantity);
+ 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") String customerId,
+ @PathVariable("productId") String productId) {
+ Cart updatedCart = cartService.removeItemFromCart(customerId, productId);
+ 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") String customerId) {
+ cartService.clearCart(customerId);
+ return ResponseEntity.noContent().build();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/cart/model/Cart.java b/src/main/java/cart/model/Cart.java
new file mode 100644
index 0000000..a34754d
--- /dev/null
+++ b/src/main/java/cart/model/Cart.java
@@ -0,0 +1,59 @@
+package cart.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Document(collection = "carts")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class Cart {
+
+
+ @Id
+ private String id;
+
+ private String customerId;
+
+ private List items = new ArrayList<>();
+
+ @Override
+ public String toString() {
+ return "Cart{"
+ + "id='" + id + '\''
+ + ", customerId='" + customerId + '\''
+ + ", items=" + items
+ + '}';
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(final String id) {
+ this.id = id;
+ }
+
+ public String getCustomerId() {
+ return customerId;
+ }
+
+ public void setCustomerId(final String customerId) {
+ this.customerId = customerId;
+ }
+
+ public List getItems() {
+ return items;
+ }
+
+ public void setItems(final List items) {
+ this.items = items;
+ }
+}
+
diff --git a/src/main/java/cart/model/CartItem.java b/src/main/java/cart/model/CartItem.java
new file mode 100644
index 0000000..92e77dc
--- /dev/null
+++ b/src/main/java/cart/model/CartItem.java
@@ -0,0 +1,39 @@
+package cart.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class CartItem {
+
+ private String productId;
+
+ private int quantity;
+
+ @Override
+ public String toString() {
+ return "CartItem{"
+ + "productId='" + productId + '\''
+ + ", quantity=" + quantity
+ + '}';
+ }
+
+ public String getProductId() {
+ return productId;
+ }
+
+ public void setProductId(final String productId) {
+ this.productId = productId;
+ }
+
+ public int getQuantity() {
+ return quantity;
+ }
+
+ public void setQuantity(int quantity) {
+ this.quantity = quantity;
+ }
+}
diff --git a/src/main/java/cart/repository/CartRepository.java b/src/main/java/cart/repository/CartRepository.java
new file mode 100644
index 0000000..d637b49
--- /dev/null
+++ b/src/main/java/cart/repository/CartRepository.java
@@ -0,0 +1,12 @@
+package cart.repository;
+
+import cart.model.Cart;
+import org.springframework.data.mongodb.repository.MongoRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface CartRepository extends MongoRepository {
+ Optional findByCustomerId(final String customerId);
+}
\ No newline at end of file
diff --git a/src/main/java/cart/service/CartService.java b/src/main/java/cart/service/CartService.java
new file mode 100644
index 0000000..4995a1d
--- /dev/null
+++ b/src/main/java/cart/service/CartService.java
@@ -0,0 +1,93 @@
+package cart.service;
+
+import cart.model.Cart;
+import cart.model.CartItem;
+import cart.repository.CartRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.web.server.ResponseStatusException;
+
+import java.util.ArrayList;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.UUID;
+
+@Service
+@RequiredArgsConstructor
+public class CartService {
+
+ private final CartRepository cartRepository;
+
+
+ public Cart createCart(final String customerId) {
+ return cartRepository.findByCustomerId(customerId)
+ .orElseGet(() -> {
+ Cart newCart = new Cart(UUID.randomUUID().toString(), customerId, new ArrayList<>());
+ return cartRepository.save(newCart);
+ });
+ }
+
+ public Cart addItemToCart(final String customerId, final CartItem newItem) {
+ Cart cart = getCartByCustomerId(customerId);
+
+ Optional existingItem = cart.getItems().stream()
+ .filter(i -> i.getProductId().equals(newItem.getProductId()))
+ .findFirst();
+
+ if (existingItem.isPresent()) {
+ existingItem.get().setQuantity(existingItem.get().getQuantity() + newItem.getQuantity());
+ } else {
+ cart.getItems().add(newItem);
+ }
+
+ return cartRepository.save(cart);
+ }
+
+
+ public Cart updateItemQuantity(final String customerId, final String productId, final int quantity) {
+ Cart cart = getCartByCustomerId(customerId);
+
+ Optional existingItemOpt = cart.getItems().stream()
+ .filter(i -> i.getProductId().equals(productId))
+ .findFirst();
+
+ if (existingItemOpt.isEmpty()) {
+ throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Product not found in cart");
+ }
+
+ CartItem item = existingItemOpt.get();
+
+ if (quantity <= 0) {
+ cart.getItems().remove(item);
+ } else {
+ item.setQuantity(quantity);
+ }
+
+ return cartRepository.save(cart);
+
+ }
+
+
+ public Cart removeItemFromCart(final String customerId,final String productId) {
+ Cart cart = getCartByCustomerId(customerId);
+ cart.getItems().removeIf(i -> i.getProductId().equals(productId));
+ return cartRepository.save(cart);
+ }
+
+ public void deleteCartByCustomerId(final String customerId) {
+ cartRepository.findByCustomerId(customerId).ifPresent(cartRepository::delete);
+ }
+
+ public Cart getCartByCustomerId(final String customerId) {
+ return cartRepository.findByCustomerId(customerId)
+ .orElseThrow(() -> new NoSuchElementException("Cart not found"));
+ }
+
+ public void clearCart(final String customerId) {
+ Cart cart = getCartByCustomerId(customerId);
+ cart.getItems().clear();
+ cartRepository.save(cart);
+ }
+
+}
From 16a78e3b183f3e107c0d5c88e6e0c2ee0d661546 Mon Sep 17 00:00:00 2001
From: Abdelrahman Elmeky <65960126+Aelmeky@users.noreply.github.com>
Date: Sat, 3 May 2025 00:42:50 +0300
Subject: [PATCH 02/24] adding clear cart method
---
src/main/java/cart/service/CartService.java | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/main/java/cart/service/CartService.java b/src/main/java/cart/service/CartService.java
index 4995a1d..b9168cd 100644
--- a/src/main/java/cart/service/CartService.java
+++ b/src/main/java/cart/service/CartService.java
@@ -65,7 +65,6 @@ public Cart updateItemQuantity(final String customerId, final String productId,
}
return cartRepository.save(cart);
-
}
From 39fce86d2767c46383670b99a74bd232177ce90b Mon Sep 17 00:00:00 2001
From: Abdelrahman Elmeky <65960126+Aelmeky@users.noreply.github.com>
Date: Sat, 3 May 2025 00:58:03 +0300
Subject: [PATCH 03/24] adding docker file
---
docker-compose.yml | 61 +++++++++++++++++++++++++
promtail.yml | 18 ++++++++
src/main/java/cart/CartApplication.java | 11 +++++
3 files changed, 90 insertions(+)
create mode 100644 docker-compose.yml
create mode 100644 promtail.yml
create mode 100644 src/main/java/cart/CartApplication.java
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..d1ded4d
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,61 @@
+version: '3.8'
+
+services:
+
+ cart-service:
+ image: openjdk:25-ea-4-jdk-oraclelinux9
+ container_name: cart-service
+ ports:
+ - "8081:8080"
+ env_file:
+ - secret.env
+ environment:
+ SPRING_DATA_MONGODB_URI: mongodb://cart-db:27017/cartDB
+ SPRING_DATA_MONGODB_DATABASE: cartDB
+ depends_on:
+ - cart-db
+ volumes:
+ - ./target:/app
+ - ./logs/cart:/logs
+ command: ["java", "-jar", "/app/cart-service.jar"]
+
+ cart-db:
+ image: mongo:latest
+ 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-config.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/promtail.yml b/promtail.yml
new file mode 100644
index 0000000..c3a4e5b
--- /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..85eeedf
--- /dev/null
+++ b/src/main/java/cart/CartApplication.java
@@ -0,0 +1,11 @@
+package cart;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class CartApplication {
+ public static void main(final String [] args){
+ SpringApplication.run(CartApplication.class, args);
+ }
+}
From 075828c5c0fbc7ff10922c7fbc8760fddfa13a0c Mon Sep 17 00:00:00 2001
From: Abdelrahman Elmeky <65960126+Aelmeky@users.noreply.github.com>
Date: Sat, 3 May 2025 00:59:24 +0300
Subject: [PATCH 04/24] adding git ignore
---
.gitignore | 8 ++++++++
1 file changed, 8 insertions(+)
create mode 100644 .gitignore
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..12b534a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+# Ignore Maven build output
+/target/
+
+# Ignore IntelliJ IDEA files
+.idea/
+*.iml
+*.ipr
+*.iws
From 58841eeb0a097e4c83a6f40b644ce48c2a82593d Mon Sep 17 00:00:00 2001
From: Abdelrahman Elmeky <65960126+Aelmeky@users.noreply.github.com>
Date: Sat, 3 May 2025 15:56:10 +0300
Subject: [PATCH 05/24] adding the 3 endpoints archive and unarchive and
checkout
---
src/main/java/cart/config/AppConfig.java | 15 ++++++
.../java/cart/controller/CartController.java | 38 +++++++++++++++
src/main/java/cart/model/Cart.java | 11 +++++
src/main/java/cart/model/OrderRequest.java | 26 +++++++++++
.../java/cart/repository/CartRepository.java | 2 +
src/main/java/cart/service/CartService.java | 46 ++++++++++++++++++-
src/main/resources/application.properties | 2 +
7 files changed, 139 insertions(+), 1 deletion(-)
create mode 100644 src/main/java/cart/config/AppConfig.java
create mode 100644 src/main/java/cart/model/OrderRequest.java
create mode 100644 src/main/resources/application.properties
diff --git a/src/main/java/cart/config/AppConfig.java b/src/main/java/cart/config/AppConfig.java
new file mode 100644
index 0000000..6fa562e
--- /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();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/cart/controller/CartController.java b/src/main/java/cart/controller/CartController.java
index 9b13d51..02328a7 100644
--- a/src/main/java/cart/controller/CartController.java
+++ b/src/main/java/cart/controller/CartController.java
@@ -108,4 +108,42 @@ public ResponseEntity clearCart(@PathVariable("customerId") String custome
cartService.clearCart(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") String customerId) {
+ Cart archivedCart = cartService.archiveCart(customerId);
+ 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") String customerId) {
+ Cart activeCart = cartService.unarchiveCart(customerId);
+ 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") String customerId) {
+ try {
+ Cart updatedCart = cartService.checkoutCart(customerId);
+ return ResponseEntity.ok(updatedCart);
+ } catch (Exception ex) {
+ throw new IllegalCallerException("Error communicating with Order Service");
+ }
+ }
}
\ No newline at end of file
diff --git a/src/main/java/cart/model/Cart.java b/src/main/java/cart/model/Cart.java
index a34754d..30f4680 100644
--- a/src/main/java/cart/model/Cart.java
+++ b/src/main/java/cart/model/Cart.java
@@ -23,12 +23,15 @@ public class Cart {
private List items = new ArrayList<>();
+ private boolean archived = false;
+
@Override
public String toString() {
return "Cart{"
+ "id='" + id + '\''
+ ", customerId='" + customerId + '\''
+ ", items=" + items
+ + ", archived" + archived
+ '}';
}
@@ -55,5 +58,13 @@ public List getItems() {
public void setItems(final List items) {
this.items = items;
}
+
+ public boolean isArchived() {
+ return archived;
+ }
+
+ public void setArchived(final boolean archived) {
+ this.archived = archived;
+ }
}
diff --git a/src/main/java/cart/model/OrderRequest.java b/src/main/java/cart/model/OrderRequest.java
new file mode 100644
index 0000000..8ebb3a8
--- /dev/null
+++ b/src/main/java/cart/model/OrderRequest.java
@@ -0,0 +1,26 @@
+package cart.model;
+
+
+import java.util.List;
+
+public class OrderRequest {
+ private String customerId;
+ private List items;
+
+ // Getters and setters
+ public String getCustomerId() {
+ return customerId;
+ }
+
+ public void setCustomerId(String customerId) {
+ this.customerId = customerId;
+ }
+
+ public List getItems() {
+ return items;
+ }
+
+ public void setItems(List items) {
+ this.items = items;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/cart/repository/CartRepository.java b/src/main/java/cart/repository/CartRepository.java
index d637b49..8e33a69 100644
--- a/src/main/java/cart/repository/CartRepository.java
+++ b/src/main/java/cart/repository/CartRepository.java
@@ -9,4 +9,6 @@
@Repository
public interface CartRepository extends MongoRepository {
Optional findByCustomerId(final String customerId);
+ Optional findByCustomerIdAndArchived(String customerId, boolean archived);
+
}
\ No newline at end of file
diff --git a/src/main/java/cart/service/CartService.java b/src/main/java/cart/service/CartService.java
index b9168cd..3fbfa9f 100644
--- a/src/main/java/cart/service/CartService.java
+++ b/src/main/java/cart/service/CartService.java
@@ -2,10 +2,13 @@
import cart.model.Cart;
import cart.model.CartItem;
+import cart.model.OrderRequest;
import cart.repository.CartRepository;
import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
import org.springframework.web.server.ResponseStatusException;
import java.util.ArrayList;
@@ -19,11 +22,16 @@ public class CartService {
private final CartRepository cartRepository;
+ private final RestTemplate restTemplate;
+
+ @Value("${order.service.url}")
+ private String orderServiceUrl;
+
public Cart createCart(final String customerId) {
return cartRepository.findByCustomerId(customerId)
.orElseGet(() -> {
- Cart newCart = new Cart(UUID.randomUUID().toString(), customerId, new ArrayList<>());
+ Cart newCart = new Cart(UUID.randomUUID().toString(), customerId, new ArrayList<>(), false);
return cartRepository.save(newCart);
});
}
@@ -89,4 +97,40 @@ public void clearCart(final String customerId) {
cartRepository.save(cart);
}
+ public Cart archiveCart(String customerId) {
+ Cart cart = getActiveCart(customerId);
+ cart.setArchived(true);
+ return cartRepository.save(cart);
+ }
+
+ public Cart unarchiveCart(String customerId) {
+ Cart cart = getArchivedCart(customerId);
+ cart.setArchived(false);
+ return cartRepository.save(cart);
+ }
+
+ public Cart checkoutCart(String customerId) {
+ Cart cart = getActiveCart(customerId);
+
+ OrderRequest orderRequest = new OrderRequest();
+ orderRequest.setCustomerId(customerId);
+ orderRequest.setItems(cart.getItems());
+
+ restTemplate.postForObject(orderServiceUrl + "/orders", orderRequest, Void.class);
+
+ cart.getItems().clear();
+ return cartRepository.save(cart);
+ }
+
+
+ private Cart getActiveCart(String customerId) {
+ return cartRepository.findByCustomerIdAndArchived(customerId, false)
+ .orElseThrow(() -> new NoSuchElementException("Cart not found for customer ID: " + customerId));
+ }
+
+ private Cart getArchivedCart(String customerId) {
+ return cartRepository.findByCustomerIdAndArchived(customerId, true)
+ .orElseThrow(() -> new NoSuchElementException("No archived cart found for customer ID: " + customerId));
+ }
+
}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
new file mode 100644
index 0000000..2e21dc6
--- /dev/null
+++ b/src/main/resources/application.properties
@@ -0,0 +1,2 @@
+order.service.url=http://order-service:8082
+spring.data.mongodb.uri=mongodb://localhost:27018/cartDB
\ No newline at end of file
From 26c31a8113bb5d7a22e739d7a6545819f6c0ad5a Mon Sep 17 00:00:00 2001
From: Abdelrahman Elmeky <65960126+Aelmeky@users.noreply.github.com>
Date: Sat, 3 May 2025 23:47:42 +0300
Subject: [PATCH 06/24] fixing comments and logger config
---
docker-compose.yml | 8 +-
logs/app.log | 367 ++++++++++++++++++++++
promtail.yml | 2 +-
src/main/resources/application.properties | 5 +-
src/main/resources/logback-spring.xml | 15 +
5 files changed, 390 insertions(+), 7 deletions(-)
create mode 100644 logs/app.log
create mode 100644 src/main/resources/logback-spring.xml
diff --git a/docker-compose.yml b/docker-compose.yml
index d1ded4d..e35d882 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -7,8 +7,6 @@ services:
container_name: cart-service
ports:
- "8081:8080"
- env_file:
- - secret.env
environment:
SPRING_DATA_MONGODB_URI: mongodb://cart-db:27017/cartDB
SPRING_DATA_MONGODB_DATABASE: cartDB
@@ -16,8 +14,8 @@ services:
- cart-db
volumes:
- ./target:/app
- - ./logs/cart:/logs
- command: ["java", "-jar", "/app/cart-service.jar"]
+ - ./logs:/logs
+ command: ["java", "-jar", "/app/cart-0.0.1-SNAPSHOT.jar"]
cart-db:
image: mongo:latest
@@ -42,7 +40,7 @@ services:
image: grafana/promtail:latest
container_name: promtail
volumes:
- - ./promtail-config.yml:/etc/promtail/promtail-config.yaml
+ - ./promtail.yml:/etc/promtail/promtail-config.yaml
- ./logs:/logs
command:
- -config.file=/etc/promtail/promtail-config.yaml
diff --git a/logs/app.log b/logs/app.log
new file mode 100644
index 0000000..20570ce
--- /dev/null
+++ b/logs/app.log
@@ -0,0 +1,367 @@
+2025-05-03T20:31:22.279Z INFO 1 --- [main] cart.CartApplication : Starting CartApplication v0.0.1-SNAPSHOT using Java 25-ea with PID 1 (/app/cart-0.0.1-SNAPSHOT.jar started by root in /)
+2025-05-03T20:31:22.309Z INFO 1 --- [main] cart.CartApplication : No active profile set, falling back to 1 default profile: "default"
+2025-05-03T20:31:28.546Z INFO 1 --- [main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data MongoDB repositories in DEFAULT mode.
+2025-05-03T20:31:28.823Z INFO 1 --- [main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 221 ms. Found 1 MongoDB repository interface.
+2025-05-03T20:31:31.202Z INFO 1 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
+2025-05-03T20:31:31.305Z INFO 1 --- [main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
+2025-05-03T20:31:31.311Z INFO 1 --- [main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.40]
+2025-05-03T20:31:31.496Z INFO 1 --- [main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
+2025-05-03T20:31:31.506Z INFO 1 --- [main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 8747 ms
+2025-05-03T20:31:33.078Z INFO 1 --- [main] org.mongodb.driver.client : MongoClient with metadata {"driver": {"name": "mongo-java-driver|sync|spring-boot", "version": "5.2.1"}, "os": {"type": "Linux", "name": "Linux", "architecture": "amd64", "version": "5.15.133.1-microsoft-standard-WSL2"}, "platform": "Java/Oracle Corporation/25-ea+4-344", "env": {"container": {"runtime": "docker"}}} created with settings MongoClientSettings{readPreference=primary, writeConcern=WriteConcern{w=null, wTimeout=null ms, journal=null}, retryWrites=true, retryReads=true, readConcern=ReadConcern{level=null}, credential=null, transportSettings=null, commandListeners=[], codecRegistry=ProvidersCodecRegistry{codecProviders=[ValueCodecProvider{}, BsonValueCodecProvider{}, DBRefCodecProvider{}, DBObjectCodecProvider{}, DocumentCodecProvider{}, CollectionCodecProvider{}, IterableCodecProvider{}, MapCodecProvider{}, GeoJsonCodecProvider{}, GridFSFileCodecProvider{}, Jsr310CodecProvider{}, JsonObjectCodecProvider{}, BsonCodecProvider{}, EnumCodecProvider{}, com.mongodb.client.model.mql.ExpressionCodecProvider@5b1c32e4, com.mongodb.Jep395RecordCodecProvider@2bab618, com.mongodb.KotlinCodecProvider@48bc2fce]}, loggerSettings=LoggerSettings{maxDocumentLength=1000}, clusterSettings={hosts=[cart-db:27017], srvServiceName=mongodb, mode=SINGLE, requiredClusterType=UNKNOWN, requiredReplicaSetName='null', serverSelector='null', clusterListeners='[]', serverSelectionTimeout='30000 ms', localThreshold='15 ms'}, socketSettings=SocketSettings{connectTimeoutMS=10000, readTimeoutMS=0, receiveBufferSize=0, proxySettings=ProxySettings{host=null, port=null, username=null, password=null}}, heartbeatSocketSettings=SocketSettings{connectTimeoutMS=10000, readTimeoutMS=10000, receiveBufferSize=0, proxySettings=ProxySettings{host=null, port=null, username=null, password=null}}, connectionPoolSettings=ConnectionPoolSettings{maxSize=100, minSize=0, maxWaitTimeMS=120000, maxConnectionLifeTimeMS=0, maxConnectionIdleTimeMS=0, maintenanceInitialDelayMS=0, maintenanceFrequencyMS=60000, connectionPoolListeners=[], maxConnecting=2}, serverSettings=ServerSettings{heartbeatFrequencyMS=10000, minHeartbeatFrequencyMS=500, serverMonitoringMode=AUTO, serverListeners='[]', serverMonitorListeners='[]'}, sslSettings=SslSettings{enabled=false, invalidHostNameAllowed=false, context=null}, applicationName='null', compressorList=[], uuidRepresentation=JAVA_LEGACY, serverApi=null, autoEncryptionSettings=null, dnsClient=null, inetAddressResolver=null, contextProvider=null, timeoutMS=null}
+2025-05-03T20:31:33.264Z INFO 1 --- [cluster-ClusterId{value='68167d2481d2884e0500a1eb', description='null'}-cart-db:27017] org.mongodb.driver.cluster : Monitor thread successfully connected to server with description ServerDescription{address=cart-db:27017, type=STANDALONE, cryptd=false, state=CONNECTED, ok=true, minWireVersion=0, maxWireVersion=21, maxDocumentSize=16777216, logicalSessionTimeoutMinutes=30, roundTripTimeNanos=256246676, minRoundTripTimeNanos=0}
+2025-05-03T20:31:38.379Z INFO 1 --- [main] o.s.v.b.OptionalValidatorFactoryBean : Failed to set up a Bean Validation provider: jakarta.validation.NoProviderFoundException: Unable to create a Configuration, because no Jakarta Bean Validation provider could be found. Add a provider like Hibernate Validator (RI) to your classpath.
+2025-05-03T20:31:39.314Z DEBUG 1 --- [main] s.w.s.m.m.a.RequestMappingHandlerMapping : 16 mappings in 'requestMappingHandlerMapping'
+2025-05-03T20:31:40.217Z DEBUG 1 --- [main] o.s.w.s.handler.SimpleUrlHandlerMapping : Patterns [/webjars/**, /**, /swagger-ui*/*swagger-initializer.js, /swagger-ui*/**] in 'resourceHandlerMapping'
+2025-05-03T20:31:40.665Z DEBUG 1 --- [main] s.w.s.m.m.a.RequestMappingHandlerAdapter : ControllerAdvice beans: 0 @ModelAttribute, 0 @InitBinder, 1 RequestBodyAdvice, 1 ResponseBodyAdvice
+2025-05-03T20:31:42.243Z DEBUG 1 --- [main] .m.m.a.ExceptionHandlerExceptionResolver : ControllerAdvice beans: 1 @ExceptionHandler, 1 ResponseBodyAdvice
+2025-05-03T20:31:43.617Z INFO 1 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
+2025-05-03T20:31:43.762Z INFO 1 --- [main] cart.CartApplication : Started CartApplication in 34.402 seconds (process running for 45.042)
+2025-05-03T20:38:11.427Z INFO 1 --- [http-nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
+2025-05-03T20:38:11.437Z INFO 1 --- [http-nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
+2025-05-03T20:38:11.462Z DEBUG 1 --- [http-nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Detected StandardServletMultipartResolver
+2025-05-03T20:38:11.466Z DEBUG 1 --- [http-nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Detected AcceptHeaderLocaleResolver
+2025-05-03T20:38:11.479Z DEBUG 1 --- [http-nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Detected FixedThemeResolver
+2025-05-03T20:38:11.535Z DEBUG 1 --- [http-nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Detected org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator@305063b9
+2025-05-03T20:38:11.542Z DEBUG 1 --- [http-nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Detected org.springframework.web.servlet.support.SessionFlashMapManager@38b1666c
+2025-05-03T20:38:11.558Z DEBUG 1 --- [http-nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : enableLoggingRequestDetails='false': request parameters and headers will be masked to prevent unsafe logging of potentially sensitive data
+2025-05-03T20:38:11.568Z INFO 1 --- [http-nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Completed initialization in 119 ms
+2025-05-03T20:38:11.680Z DEBUG 1 --- [http-nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : GET "/api/cart/customer/123456", parameters={}
+2025-05-03T20:38:11.829Z DEBUG 1 --- [http-nio-8080-exec-2] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cart.controller.CartController#getCartByCustomerId(String)
+2025-05-03T20:38:13.541Z DEBUG 1 --- [http-nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Failed to complete request: java.util.NoSuchElementException: Cart not found
+2025-05-03T20:38:13.564Z ERROR 1 --- [http-nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.util.NoSuchElementException: Cart not found] with root cause
+
+java.util.NoSuchElementException: Cart not found
+ at cart.service.CartService.lambda$getCartByCustomerId$4(CartService.java:91)
+ at java.base/java.util.Optional.orElseThrow(Optional.java:403)
+ at cart.service.CartService.getCartByCustomerId(CartService.java:91)
+ at cart.controller.CartController.getCartByCustomerId(CartController.java:44)
+ at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
+ at java.base/java.lang.reflect.Method.invoke(Method.java:565)
+ at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:258)
+ at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:191)
+ at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118)
+ at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:986)
+ at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:891)
+ at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
+ at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089)
+ at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979)
+ at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)
+ at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903)
+ at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)
+ at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)
+ at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
+ at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)
+ at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
+ at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
+ at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
+ at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
+ at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
+ at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
+ at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
+ at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
+ at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
+ at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
+ at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
+ at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
+ at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
+ at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
+ at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
+ at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
+ at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
+ at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
+ at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)
+ at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:116)
+ at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
+ at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
+ at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)
+ at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:398)
+ at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
+ at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:903)
+ at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1740)
+ at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
+ at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1189)
+ at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:658)
+ at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
+ at java.base/java.lang.Thread.run(Thread.java:1447)
+
+2025-05-03T20:38:13.778Z DEBUG 1 --- [http-nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : "ERROR" dispatch for GET "/error", parameters={}
+2025-05-03T20:38:13.798Z DEBUG 1 --- [http-nio-8080-exec-2] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
+2025-05-03T20:38:14.135Z DEBUG 1 --- [http-nio-8080-exec-2] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/yaml]
+2025-05-03T20:38:14.179Z DEBUG 1 --- [http-nio-8080-exec-2] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [{timestamp=Sat May 03 20:38:13 UTC 2025, status=500, error=Internal Server Error, path=/api/cart/cus (truncated)...]
+2025-05-03T20:38:14.631Z DEBUG 1 --- [http-nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Exiting from "ERROR" dispatch, status 500
+2025-05-03T20:38:23.070Z DEBUG 1 --- [http-nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : GET "/api/cart/customer/12345", parameters={}
+2025-05-03T20:38:23.082Z DEBUG 1 --- [http-nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cart.controller.CartController#getCartByCustomerId(String)
+2025-05-03T20:38:23.152Z DEBUG 1 --- [http-nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Failed to complete request: java.util.NoSuchElementException: Cart not found
+2025-05-03T20:38:23.161Z ERROR 1 --- [http-nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.util.NoSuchElementException: Cart not found] with root cause
+
+java.util.NoSuchElementException: Cart not found
+ at cart.service.CartService.lambda$getCartByCustomerId$4(CartService.java:91)
+ at java.base/java.util.Optional.orElseThrow(Optional.java:403)
+ at cart.service.CartService.getCartByCustomerId(CartService.java:91)
+ at cart.controller.CartController.getCartByCustomerId(CartController.java:44)
+ at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
+ at java.base/java.lang.reflect.Method.invoke(Method.java:565)
+ at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:258)
+ at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:191)
+ at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118)
+ at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:986)
+ at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:891)
+ at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
+ at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089)
+ at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979)
+ at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)
+ at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903)
+ at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)
+ at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)
+ at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
+ at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)
+ at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
+ at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
+ at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
+ at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
+ at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
+ at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
+ at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
+ at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
+ at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
+ at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
+ at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
+ at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
+ at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
+ at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
+ at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
+ at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
+ at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
+ at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
+ at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)
+ at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:116)
+ at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
+ at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
+ at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)
+ at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:398)
+ at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
+ at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:903)
+ at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1740)
+ at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
+ at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1189)
+ at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:658)
+ at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
+ at java.base/java.lang.Thread.run(Thread.java:1447)
+
+2025-05-03T20:38:23.186Z DEBUG 1 --- [http-nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : "ERROR" dispatch for GET "/error", parameters={}
+2025-05-03T20:38:23.205Z DEBUG 1 --- [http-nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
+2025-05-03T20:38:23.222Z DEBUG 1 --- [http-nio-8080-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/yaml]
+2025-05-03T20:38:23.253Z DEBUG 1 --- [http-nio-8080-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [{timestamp=Sat May 03 20:38:23 UTC 2025, status=500, error=Internal Server Error, path=/api/cart/cus (truncated)...]
+2025-05-03T20:38:23.387Z DEBUG 1 --- [http-nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Exiting from "ERROR" dispatch, status 500
+2025-05-03T20:39:12.637Z DEBUG 1 --- [http-nio-8080-exec-3] o.s.web.servlet.DispatcherServlet : POST "/api/cart/create/12356", parameters={}
+2025-05-03T20:39:12.641Z DEBUG 1 --- [http-nio-8080-exec-3] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cart.controller.CartController#createCart(String)
+2025-05-03T20:39:13.272Z DEBUG 1 --- [http-nio-8080-exec-3] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/yaml]
+2025-05-03T20:39:13.297Z DEBUG 1 --- [http-nio-8080-exec-3] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [Cart{id='42ce5685-e107-4872-a6b0-78daca819b9b', customerId='12356', items=[], archivedfalse}]
+2025-05-03T20:39:13.325Z DEBUG 1 --- [http-nio-8080-exec-3] o.s.web.servlet.DispatcherServlet : Completed 200 OK
+2025-05-03T20:39:32.998Z DEBUG 1 --- [http-nio-8080-exec-6] o.s.web.servlet.DispatcherServlet : GET "/api/cart/customer/123456", parameters={}
+2025-05-03T20:39:33.007Z DEBUG 1 --- [http-nio-8080-exec-6] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cart.controller.CartController#getCartByCustomerId(String)
+2025-05-03T20:39:33.036Z DEBUG 1 --- [http-nio-8080-exec-6] o.s.web.servlet.DispatcherServlet : Failed to complete request: java.util.NoSuchElementException: Cart not found
+2025-05-03T20:39:33.042Z ERROR 1 --- [http-nio-8080-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.util.NoSuchElementException: Cart not found] with root cause
+
+java.util.NoSuchElementException: Cart not found
+ at cart.service.CartService.lambda$getCartByCustomerId$4(CartService.java:91)
+ at java.base/java.util.Optional.orElseThrow(Optional.java:403)
+ at cart.service.CartService.getCartByCustomerId(CartService.java:91)
+ at cart.controller.CartController.getCartByCustomerId(CartController.java:44)
+ at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
+ at java.base/java.lang.reflect.Method.invoke(Method.java:565)
+ at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:258)
+ at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:191)
+ at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118)
+ at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:986)
+ at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:891)
+ at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
+ at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089)
+ at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979)
+ at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)
+ at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903)
+ at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)
+ at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)
+ at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
+ at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)
+ at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
+ at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
+ at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
+ at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
+ at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
+ at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
+ at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
+ at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
+ at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
+ at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
+ at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
+ at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
+ at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
+ at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
+ at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
+ at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
+ at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
+ at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
+ at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)
+ at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:116)
+ at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
+ at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
+ at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)
+ at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:398)
+ at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
+ at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:903)
+ at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1740)
+ at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
+ at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1189)
+ at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:658)
+ at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
+ at java.base/java.lang.Thread.run(Thread.java:1447)
+
+2025-05-03T20:39:33.051Z DEBUG 1 --- [http-nio-8080-exec-6] o.s.web.servlet.DispatcherServlet : "ERROR" dispatch for GET "/error", parameters={}
+2025-05-03T20:39:33.066Z DEBUG 1 --- [http-nio-8080-exec-6] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
+2025-05-03T20:39:33.077Z DEBUG 1 --- [http-nio-8080-exec-6] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/yaml]
+2025-05-03T20:39:33.084Z DEBUG 1 --- [http-nio-8080-exec-6] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [{timestamp=Sat May 03 20:39:33 UTC 2025, status=500, error=Internal Server Error, path=/api/cart/cus (truncated)...]
+2025-05-03T20:39:33.088Z DEBUG 1 --- [http-nio-8080-exec-6] o.s.web.servlet.DispatcherServlet : Exiting from "ERROR" dispatch, status 500
+2025-05-03T20:39:47.537Z DEBUG 1 --- [http-nio-8080-exec-7] o.s.web.servlet.DispatcherServlet : POST "/api/cart/create/12356", parameters={}
+2025-05-03T20:39:47.545Z DEBUG 1 --- [http-nio-8080-exec-7] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cart.controller.CartController#createCart(String)
+2025-05-03T20:39:47.575Z DEBUG 1 --- [http-nio-8080-exec-7] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/yaml]
+2025-05-03T20:39:47.632Z DEBUG 1 --- [http-nio-8080-exec-7] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [Cart{id='42ce5685-e107-4872-a6b0-78daca819b9b', customerId='12356', items=[], archivedfalse}]
+2025-05-03T20:39:47.664Z DEBUG 1 --- [http-nio-8080-exec-7] o.s.web.servlet.DispatcherServlet : Completed 200 OK
+2025-05-03T20:39:57.758Z DEBUG 1 --- [http-nio-8080-exec-8] o.s.web.servlet.DispatcherServlet : GET "/api/cart/customer/12356", parameters={}
+2025-05-03T20:39:57.767Z DEBUG 1 --- [http-nio-8080-exec-8] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cart.controller.CartController#getCartByCustomerId(String)
+2025-05-03T20:39:57.808Z DEBUG 1 --- [http-nio-8080-exec-8] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/yaml]
+2025-05-03T20:39:57.811Z DEBUG 1 --- [http-nio-8080-exec-8] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [Cart{id='42ce5685-e107-4872-a6b0-78daca819b9b', customerId='12356', items=[], archivedfalse}]
+2025-05-03T20:39:57.824Z DEBUG 1 --- [http-nio-8080-exec-8] o.s.web.servlet.DispatcherServlet : Completed 200 OK
+2025-05-03T20:40:20.555Z DEBUG 1 --- [http-nio-8080-exec-10] o.s.web.servlet.DispatcherServlet : PATCH "/api/cart/12356/archive", parameters={}
+2025-05-03T20:40:20.561Z DEBUG 1 --- [http-nio-8080-exec-10] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cart.controller.CartController#archiveCart(String)
+2025-05-03T20:40:20.842Z DEBUG 1 --- [http-nio-8080-exec-10] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/yaml]
+2025-05-03T20:40:20.844Z DEBUG 1 --- [http-nio-8080-exec-10] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [Cart{id='42ce5685-e107-4872-a6b0-78daca819b9b', customerId='12356', items=[], archivedtrue}]
+2025-05-03T20:40:20.856Z DEBUG 1 --- [http-nio-8080-exec-10] o.s.web.servlet.DispatcherServlet : Completed 200 OK
+2025-05-03T20:40:33.609Z DEBUG 1 --- [http-nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : GET "/api/cart/customer/12356", parameters={}
+2025-05-03T20:40:33.612Z DEBUG 1 --- [http-nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cart.controller.CartController#getCartByCustomerId(String)
+2025-05-03T20:40:33.629Z DEBUG 1 --- [http-nio-8080-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/yaml]
+2025-05-03T20:40:33.641Z DEBUG 1 --- [http-nio-8080-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [Cart{id='42ce5685-e107-4872-a6b0-78daca819b9b', customerId='12356', items=[], archivedtrue}]
+2025-05-03T20:40:33.657Z DEBUG 1 --- [http-nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed 200 OK
+2025-05-03T20:40:44.620Z DEBUG 1 --- [http-nio-8080-exec-4] o.s.web.servlet.DispatcherServlet : PATCH "/api/cart/12356/unarchive", parameters={}
+2025-05-03T20:40:44.623Z DEBUG 1 --- [http-nio-8080-exec-4] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cart.controller.CartController#unarchiveCart(String)
+2025-05-03T20:40:44.659Z DEBUG 1 --- [http-nio-8080-exec-4] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/yaml]
+2025-05-03T20:40:44.669Z DEBUG 1 --- [http-nio-8080-exec-4] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [Cart{id='42ce5685-e107-4872-a6b0-78daca819b9b', customerId='12356', items=[], archivedfalse}]
+2025-05-03T20:40:44.672Z DEBUG 1 --- [http-nio-8080-exec-4] o.s.web.servlet.DispatcherServlet : Completed 200 OK
+2025-05-03T20:41:26.966Z DEBUG 1 --- [http-nio-8080-exec-7] o.s.web.servlet.DispatcherServlet : POST "/api/cart/12356/items", parameters={}
+2025-05-03T20:41:26.982Z DEBUG 1 --- [http-nio-8080-exec-7] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cart.controller.CartController#addItemToCart(String, CartItem)
+2025-05-03T20:41:28.869Z DEBUG 1 --- [http-nio-8080-exec-7] m.m.a.RequestResponseBodyMethodProcessor : Read "application/json;charset=UTF-8" to [CartItem{productId='123', quantity=2}]
+2025-05-03T20:41:29.043Z DEBUG 1 --- [http-nio-8080-exec-7] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/yaml]
+2025-05-03T20:41:29.070Z DEBUG 1 --- [http-nio-8080-exec-7] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [Cart{id='42ce5685-e107-4872-a6b0-78daca819b9b', customerId='12356', items=[CartItem{productId='123', (truncated)...]
+2025-05-03T20:41:29.127Z DEBUG 1 --- [http-nio-8080-exec-7] o.s.web.servlet.DispatcherServlet : Completed 200 OK
+2025-05-03T20:41:44.041Z DEBUG 1 --- [http-nio-8080-exec-8] o.s.web.servlet.DispatcherServlet : GET "/api/cart/customer/12356", parameters={}
+2025-05-03T20:41:44.044Z DEBUG 1 --- [http-nio-8080-exec-8] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cart.controller.CartController#getCartByCustomerId(String)
+2025-05-03T20:41:44.149Z DEBUG 1 --- [http-nio-8080-exec-8] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/yaml]
+2025-05-03T20:41:44.151Z DEBUG 1 --- [http-nio-8080-exec-8] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [Cart{id='42ce5685-e107-4872-a6b0-78daca819b9b', customerId='12356', items=[CartItem{productId='123', (truncated)...]
+2025-05-03T20:41:44.154Z DEBUG 1 --- [http-nio-8080-exec-8] o.s.web.servlet.DispatcherServlet : Completed 200 OK
+2025-05-03T20:42:11.607Z DEBUG 1 --- [http-nio-8080-exec-10] o.s.web.servlet.DispatcherServlet : PATCH "/api/cart/12356/items/123?quantity=4", parameters={masked}
+2025-05-03T20:42:11.613Z DEBUG 1 --- [http-nio-8080-exec-10] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cart.controller.CartController#updateItemQuantity(String, String, int)
+2025-05-03T20:42:11.684Z DEBUG 1 --- [http-nio-8080-exec-10] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/yaml]
+2025-05-03T20:42:11.687Z DEBUG 1 --- [http-nio-8080-exec-10] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [Cart{id='42ce5685-e107-4872-a6b0-78daca819b9b', customerId='12356', items=[CartItem{productId='123', (truncated)...]
+2025-05-03T20:42:11.705Z DEBUG 1 --- [http-nio-8080-exec-10] o.s.web.servlet.DispatcherServlet : Completed 200 OK
+2025-05-03T20:42:31.364Z DEBUG 1 --- [http-nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : DELETE "/api/cart/12356/clear", parameters={}
+2025-05-03T20:42:31.369Z DEBUG 1 --- [http-nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cart.controller.CartController#clearCart(String)
+2025-05-03T20:42:31.399Z DEBUG 1 --- [http-nio-8080-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/yaml]
+2025-05-03T20:42:31.404Z DEBUG 1 --- [http-nio-8080-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Nothing to write: null body
+2025-05-03T20:42:31.407Z DEBUG 1 --- [http-nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed 204 NO_CONTENT
+2025-05-03T20:42:48.558Z DEBUG 1 --- [http-nio-8080-exec-4] o.s.web.servlet.DispatcherServlet : GET "/api/cart/customer/12356", parameters={}
+2025-05-03T20:42:48.560Z DEBUG 1 --- [http-nio-8080-exec-4] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cart.controller.CartController#getCartByCustomerId(String)
+2025-05-03T20:42:48.575Z DEBUG 1 --- [http-nio-8080-exec-4] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/yaml]
+2025-05-03T20:42:48.578Z DEBUG 1 --- [http-nio-8080-exec-4] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [Cart{id='42ce5685-e107-4872-a6b0-78daca819b9b', customerId='12356', items=[], archivedfalse}]
+2025-05-03T20:42:48.581Z DEBUG 1 --- [http-nio-8080-exec-4] o.s.web.servlet.DispatcherServlet : Completed 200 OK
+2025-05-03T20:43:11.772Z DEBUG 1 --- [http-nio-8080-exec-7] o.s.web.servlet.DispatcherServlet : POST "/api/cart/12356/items", parameters={}
+2025-05-03T20:43:11.775Z DEBUG 1 --- [http-nio-8080-exec-7] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cart.controller.CartController#addItemToCart(String, CartItem)
+2025-05-03T20:43:11.834Z DEBUG 1 --- [http-nio-8080-exec-7] m.m.a.RequestResponseBodyMethodProcessor : Read "application/json;charset=UTF-8" to [CartItem{productId='124', quantity=3}]
+2025-05-03T20:43:11.891Z DEBUG 1 --- [http-nio-8080-exec-7] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/yaml]
+2025-05-03T20:43:11.894Z DEBUG 1 --- [http-nio-8080-exec-7] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [Cart{id='42ce5685-e107-4872-a6b0-78daca819b9b', customerId='12356', items=[CartItem{productId='124', (truncated)...]
+2025-05-03T20:43:11.905Z DEBUG 1 --- [http-nio-8080-exec-7] o.s.web.servlet.DispatcherServlet : Completed 200 OK
+2025-05-03T20:43:21.624Z DEBUG 1 --- [http-nio-8080-exec-8] o.s.web.servlet.DispatcherServlet : GET "/api/cart/customer/12356", parameters={}
+2025-05-03T20:43:21.629Z DEBUG 1 --- [http-nio-8080-exec-8] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cart.controller.CartController#getCartByCustomerId(String)
+2025-05-03T20:43:21.649Z DEBUG 1 --- [http-nio-8080-exec-8] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/yaml]
+2025-05-03T20:43:21.659Z DEBUG 1 --- [http-nio-8080-exec-8] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [Cart{id='42ce5685-e107-4872-a6b0-78daca819b9b', customerId='12356', items=[CartItem{productId='124', (truncated)...]
+2025-05-03T20:43:21.681Z DEBUG 1 --- [http-nio-8080-exec-8] o.s.web.servlet.DispatcherServlet : Completed 200 OK
+2025-05-03T20:43:44.983Z DEBUG 1 --- [http-nio-8080-exec-9] o.s.web.servlet.DispatcherServlet : PATCH "/api/cart/12356/items/124?quantity=-1", parameters={masked}
+2025-05-03T20:43:44.986Z DEBUG 1 --- [http-nio-8080-exec-9] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cart.controller.CartController#updateItemQuantity(String, String, int)
+2025-05-03T20:43:45.145Z DEBUG 1 --- [http-nio-8080-exec-9] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/yaml]
+2025-05-03T20:43:45.151Z DEBUG 1 --- [http-nio-8080-exec-9] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [Cart{id='42ce5685-e107-4872-a6b0-78daca819b9b', customerId='12356', items=[], archivedfalse}]
+2025-05-03T20:43:45.170Z DEBUG 1 --- [http-nio-8080-exec-9] o.s.web.servlet.DispatcherServlet : Completed 200 OK
+2025-05-03T20:43:52.213Z DEBUG 1 --- [http-nio-8080-exec-10] o.s.web.servlet.DispatcherServlet : GET "/api/cart/customer/12356", parameters={}
+2025-05-03T20:43:52.217Z DEBUG 1 --- [http-nio-8080-exec-10] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cart.controller.CartController#getCartByCustomerId(String)
+2025-05-03T20:43:52.245Z DEBUG 1 --- [http-nio-8080-exec-10] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/yaml]
+2025-05-03T20:43:52.249Z DEBUG 1 --- [http-nio-8080-exec-10] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [Cart{id='42ce5685-e107-4872-a6b0-78daca819b9b', customerId='12356', items=[], archivedfalse}]
+2025-05-03T20:43:52.260Z DEBUG 1 --- [http-nio-8080-exec-10] o.s.web.servlet.DispatcherServlet : Completed 200 OK
+2025-05-03T20:44:04.613Z DEBUG 1 --- [http-nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : DELETE "/api/cart/customer/12356", parameters={}
+2025-05-03T20:44:04.616Z DEBUG 1 --- [http-nio-8080-exec-2] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cart.controller.CartController#deleteCart(String)
+2025-05-03T20:44:04.737Z DEBUG 1 --- [http-nio-8080-exec-2] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/yaml]
+2025-05-03T20:44:04.739Z DEBUG 1 --- [http-nio-8080-exec-2] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Nothing to write: null body
+2025-05-03T20:44:04.742Z DEBUG 1 --- [http-nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Completed 204 NO_CONTENT
+2025-05-03T20:44:10.048Z DEBUG 1 --- [http-nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : GET "/api/cart/customer/12356", parameters={}
+2025-05-03T20:44:10.060Z DEBUG 1 --- [http-nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cart.controller.CartController#getCartByCustomerId(String)
+2025-05-03T20:44:10.095Z DEBUG 1 --- [http-nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Failed to complete request: java.util.NoSuchElementException: Cart not found
+2025-05-03T20:44:10.101Z ERROR 1 --- [http-nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.util.NoSuchElementException: Cart not found] with root cause
+
+java.util.NoSuchElementException: Cart not found
+ at cart.service.CartService.lambda$getCartByCustomerId$4(CartService.java:91)
+ at java.base/java.util.Optional.orElseThrow(Optional.java:403)
+ at cart.service.CartService.getCartByCustomerId(CartService.java:91)
+ at cart.controller.CartController.getCartByCustomerId(CartController.java:44)
+ at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
+ at java.base/java.lang.reflect.Method.invoke(Method.java:565)
+ at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:258)
+ at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:191)
+ at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118)
+ at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:986)
+ at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:891)
+ at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
+ at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089)
+ at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979)
+ at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)
+ at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903)
+ at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)
+ at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)
+ at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
+ at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)
+ at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
+ at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
+ at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
+ at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
+ at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
+ at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
+ at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
+ at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
+ at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
+ at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
+ at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
+ at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
+ at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
+ at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
+ at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
+ at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
+ at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
+ at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
+ at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)
+ at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:116)
+ at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
+ at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
+ at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)
+ at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:398)
+ at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
+ at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:903)
+ at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1740)
+ at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
+ at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1189)
+ at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:658)
+ at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
+ at java.base/java.lang.Thread.run(Thread.java:1447)
+
+2025-05-03T20:44:10.113Z DEBUG 1 --- [http-nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : "ERROR" dispatch for GET "/error", parameters={}
+2025-05-03T20:44:10.120Z DEBUG 1 --- [http-nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
+2025-05-03T20:44:10.138Z DEBUG 1 --- [http-nio-8080-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/yaml]
+2025-05-03T20:44:10.166Z DEBUG 1 --- [http-nio-8080-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [{timestamp=Sat May 03 20:44:10 UTC 2025, status=500, error=Internal Server Error, path=/api/cart/cus (truncated)...]
+2025-05-03T20:44:10.183Z DEBUG 1 --- [http-nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Exiting from "ERROR" dispatch, status 500
+2025-05-03T20:44:29.759Z INFO 1 --- [SpringApplicationShutdownHook] o.s.b.w.e.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
+2025-05-03T20:44:29.781Z INFO 1 --- [tomcat-shutdown] o.s.b.w.e.tomcat.GracefulShutdown : Graceful shutdown complete
diff --git a/promtail.yml b/promtail.yml
index c3a4e5b..1ede004 100644
--- a/promtail.yml
+++ b/promtail.yml
@@ -15,4 +15,4 @@ scrape_configs:
- localhost
labels:
job: cart-service
- __path__: /logs/*.log
\ No newline at end of file
+ __path__: ./logs/*.log
\ No newline at end of file
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 2e21dc6..f14db7a 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,2 +1,5 @@
order.service.url=http://order-service:8082
-spring.data.mongodb.uri=mongodb://localhost:27018/cartDB
\ No newline at end of file
+spring.data.mongodb.uri=mongodb://localhost:27018/cartDB
+logging.file.name=./logs/app.log
+logging.level.root=info
+logging.level.com.podzilla.cart=debug
\ No newline at end of file
diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml
new file mode 100644
index 0000000..39be967
--- /dev/null
+++ b/src/main/resources/logback-spring.xml
@@ -0,0 +1,15 @@
+
+
+
+ logs/app.log
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
From 4a54a1887028469e5a77e04084d7ea26d6d2235c Mon Sep 17 00:00:00 2001
From: Abdelrahman Elmeky <65960126+Aelmeky@users.noreply.github.com>
Date: Sun, 4 May 2025 13:40:22 +0300
Subject: [PATCH 07/24] fixing lint errors
---
.../java/cart/controller/CartController.java | 139 ++++++++++++------
src/main/java/cart/model/CartItem.java | 2 +-
src/main/java/cart/model/OrderRequest.java | 6 +-
.../java/cart/repository/CartRepository.java | 7 +-
src/main/java/cart/service/CartService.java | 51 ++++---
5 files changed, 134 insertions(+), 71 deletions(-)
diff --git a/src/main/java/cart/controller/CartController.java b/src/main/java/cart/controller/CartController.java
index 02328a7..6c45acf 100644
--- a/src/main/java/cart/controller/CartController.java
+++ b/src/main/java/cart/controller/CartController.java
@@ -10,140 +10,187 @@
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.*;
+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/cart")
@RequiredArgsConstructor
-@Tag(name = "Cart Controller", description = "Handles cart operations like add, update, remove items and manage cart")
+@Tag(name = "Cart Controller", description = "Handles cart" +
+ " operations like add, update," +
+ " remove items and manage cart")
public class CartController {
private final CartService cartService;
- @Operation(summary = "Create a new cart for a customer or return existing one")
+ @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)
+ @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") String customerId) {
+ public ResponseEntity createCart(
+ @PathVariable("customerId") final String customerId) {
Cart cart = cartService.createCart(customerId);
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")
+ @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") String customerId) {
+ public ResponseEntity getCartByCustomerId(
+ @PathVariable("customerId") final String customerId) {
Cart cart = cartService.getCartByCustomerId(customerId);
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")
+ @ApiResponse(responseCode = "204",
+ description = "Cart deleted successfully"),
+ @ApiResponse(responseCode = "404",
+ description = "Cart not found")
})
@DeleteMapping("/customer/{customerId}")
- public ResponseEntity deleteCart(@PathVariable("customerId") String customerId) {
+ public ResponseEntity deleteCart(
+ @PathVariable("customerId") final String customerId) {
cartService.deleteCartByCustomerId(customerId);
return ResponseEntity.noContent().build();
}
- @Operation(summary = "Add an item to the cart or update its quantity if already exists")
+ @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")
+ @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") String customerId,
- @RequestBody CartItem cartItem) {
+ @PathVariable("customerId") final String customerId,
+ @RequestBody final CartItem cartItem) {
Cart updatedCart = cartService.addItemToCart(customerId, cartItem);
return ResponseEntity.ok(updatedCart);
}
- @Operation(summary = "Update quantity of an existing item in the cart")
+ @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")
+ @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") String customerId,
- @PathVariable("productId") String productId,
+ @PathVariable("customerId") final String customerId,
+ @PathVariable("productId") final String productId,
@RequestParam int quantity) {
- Cart updatedCart = cartService.updateItemQuantity(customerId, productId, quantity);
+ Cart updatedCart = cartService.updateItemQuantity(
+ customerId, productId, quantity);
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")
+ @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") String customerId,
- @PathVariable("productId") String productId) {
- Cart updatedCart = cartService.removeItemFromCart(customerId, productId);
+ @PathVariable("customerId") final String customerId,
+ @PathVariable("productId") final String productId) {
+ Cart updatedCart = cartService
+ .removeItemFromCart(customerId, productId);
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")
+ @ApiResponse(responseCode = "204",
+ description = "Cart cleared successfully"),
+ @ApiResponse(responseCode = "404",
+ description = "Cart not found")
})
@DeleteMapping("/{customerId}/clear")
- public ResponseEntity clearCart(@PathVariable("customerId") String customerId) {
+ public ResponseEntity clearCart(
+ @PathVariable("customerId") final String customerId) {
cartService.clearCart(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")
+ @ApiResponse(responseCode = "200",
+ description = "Cart successfully archived"),
+ @ApiResponse(responseCode = "404",
+ description = "Cart not found")
})
@PatchMapping("/{customerId}/archive")
- public ResponseEntity archiveCart(@PathVariable("customerId") String customerId) {
+ public ResponseEntity archiveCart(
+ @PathVariable("customerId") final String customerId) {
Cart archivedCart = cartService.archiveCart(customerId);
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")
+ @ApiResponse(responseCode = "200",
+ description = "Cart successfully unarchived"),
+ @ApiResponse(responseCode = "404",
+ description = "Archived cart not found")
})
@PatchMapping("/{customerId}/unarchive")
- public ResponseEntity unarchiveCart(@PathVariable("customerId") String customerId) {
+ public ResponseEntity unarchiveCart(
+ @PathVariable("customerId") final String customerId) {
Cart activeCart = cartService.unarchiveCart(customerId);
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")
+ @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") String customerId) {
+ public ResponseEntity checkoutCart(
+ @PathVariable("customerId") final String customerId) {
try {
Cart updatedCart = cartService.checkoutCart(customerId);
return ResponseEntity.ok(updatedCart);
} catch (Exception ex) {
- throw new IllegalCallerException("Error communicating with Order Service");
+ throw new IllegalCallerException("Error " +
+ "communicating with Order Service");
}
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/cart/model/CartItem.java b/src/main/java/cart/model/CartItem.java
index 92e77dc..38dd7ce 100644
--- a/src/main/java/cart/model/CartItem.java
+++ b/src/main/java/cart/model/CartItem.java
@@ -33,7 +33,7 @@ public int getQuantity() {
return quantity;
}
- public void setQuantity(int quantity) {
+ public void setQuantity(final int quantity) {
this.quantity = quantity;
}
}
diff --git a/src/main/java/cart/model/OrderRequest.java b/src/main/java/cart/model/OrderRequest.java
index 8ebb3a8..03e899e 100644
--- a/src/main/java/cart/model/OrderRequest.java
+++ b/src/main/java/cart/model/OrderRequest.java
@@ -12,7 +12,7 @@ public String getCustomerId() {
return customerId;
}
- public void setCustomerId(String customerId) {
+ public void setCustomerId(final String customerId) {
this.customerId = customerId;
}
@@ -20,7 +20,7 @@ public List getItems() {
return items;
}
- public void setItems(List items) {
+ public void setItems(final List items) {
this.items = items;
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/cart/repository/CartRepository.java b/src/main/java/cart/repository/CartRepository.java
index 8e33a69..2b10e48 100644
--- a/src/main/java/cart/repository/CartRepository.java
+++ b/src/main/java/cart/repository/CartRepository.java
@@ -8,7 +8,8 @@
@Repository
public interface CartRepository extends MongoRepository {
- Optional findByCustomerId(final String customerId);
- Optional findByCustomerIdAndArchived(String customerId, boolean archived);
+ Optional findByCustomerId(String customerId);
+ Optional findByCustomerIdAndArchived
+ (String customerId, boolean archived);
-}
\ No newline at end of file
+}
diff --git a/src/main/java/cart/service/CartService.java b/src/main/java/cart/service/CartService.java
index 3fbfa9f..2a4de6c 100644
--- a/src/main/java/cart/service/CartService.java
+++ b/src/main/java/cart/service/CartService.java
@@ -31,20 +31,27 @@ public class CartService {
public Cart createCart(final String customerId) {
return cartRepository.findByCustomerId(customerId)
.orElseGet(() -> {
- Cart newCart = new Cart(UUID.randomUUID().toString(), customerId, new ArrayList<>(), false);
+ Cart newCart = new Cart(UUID.randomUUID()
+ .toString(), customerId, new
+ ArrayList<>(), false);
return cartRepository.save(newCart);
});
}
- public Cart addItemToCart(final String customerId, final CartItem newItem) {
+ public Cart addItemToCart(final String customerId
+ , final CartItem newItem) {
Cart cart = getCartByCustomerId(customerId);
- Optional existingItem = cart.getItems().stream()
- .filter(i -> i.getProductId().equals(newItem.getProductId()))
+ Optional existingItem =
+ cart.getItems().stream()
+ .filter(i -> i.getProductId()
+ .equals(newItem.getProductId()))
.findFirst();
if (existingItem.isPresent()) {
- existingItem.get().setQuantity(existingItem.get().getQuantity() + newItem.getQuantity());
+ existingItem.get()
+ .setQuantity(existingItem.get().
+ getQuantity() + newItem.getQuantity());
} else {
cart.getItems().add(newItem);
}
@@ -53,7 +60,8 @@ public Cart addItemToCart(final String customerId, final CartItem newItem) {
}
- public Cart updateItemQuantity(final String customerId, final String productId, final int quantity) {
+ public Cart updateItemQuantity(final String customerId,
+ final String productId, final int quantity) {
Cart cart = getCartByCustomerId(customerId);
Optional existingItemOpt = cart.getItems().stream()
@@ -61,7 +69,8 @@ public Cart updateItemQuantity(final String customerId, final String productId,
.findFirst();
if (existingItemOpt.isEmpty()) {
- throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Product not found in cart");
+ throw new ResponseStatusException
+ (HttpStatus.NOT_FOUND, "Product not found in cart");
}
CartItem item = existingItemOpt.get();
@@ -76,19 +85,22 @@ public Cart updateItemQuantity(final String customerId, final String productId,
}
- public Cart removeItemFromCart(final String customerId,final String productId) {
+ public Cart removeItemFromCart(final String customerId,
+ final String productId) {
Cart cart = getCartByCustomerId(customerId);
cart.getItems().removeIf(i -> i.getProductId().equals(productId));
return cartRepository.save(cart);
}
public void deleteCartByCustomerId(final String customerId) {
- cartRepository.findByCustomerId(customerId).ifPresent(cartRepository::delete);
+ cartRepository.findByCustomerId(customerId)
+ .ifPresent(cartRepository::delete);
}
public Cart getCartByCustomerId(final String customerId) {
return cartRepository.findByCustomerId(customerId)
- .orElseThrow(() -> new NoSuchElementException("Cart not found"));
+ .orElseThrow(() -> new
+ NoSuchElementException("Cart not found"));
}
public void clearCart(final String customerId) {
@@ -97,40 +109,43 @@ public void clearCart(final String customerId) {
cartRepository.save(cart);
}
- public Cart archiveCart(String customerId) {
+ public Cart archiveCart(final String customerId) {
Cart cart = getActiveCart(customerId);
cart.setArchived(true);
return cartRepository.save(cart);
}
- public Cart unarchiveCart(String customerId) {
+ public Cart unarchiveCart(final String customerId) {
Cart cart = getArchivedCart(customerId);
cart.setArchived(false);
return cartRepository.save(cart);
}
- public Cart checkoutCart(String customerId) {
+ public Cart checkoutCart(final String customerId) {
Cart cart = getActiveCart(customerId);
OrderRequest orderRequest = new OrderRequest();
orderRequest.setCustomerId(customerId);
orderRequest.setItems(cart.getItems());
- restTemplate.postForObject(orderServiceUrl + "/orders", orderRequest, Void.class);
+ restTemplate.postForObject
+ (orderServiceUrl + "/orders", orderRequest, Void.class);
cart.getItems().clear();
return cartRepository.save(cart);
}
- private Cart getActiveCart(String customerId) {
+ private Cart getActiveCart(final String customerId) {
return cartRepository.findByCustomerIdAndArchived(customerId, false)
- .orElseThrow(() -> new NoSuchElementException("Cart not found for customer ID: " + customerId));
+ .orElseThrow(() -> new
+ NoSuchElementException("Cart not found for customer ID: " + customerId));
}
- private Cart getArchivedCart(String customerId) {
+ private Cart getArchivedCart(final String customerId) {
return cartRepository.findByCustomerIdAndArchived(customerId, true)
- .orElseThrow(() -> new NoSuchElementException("No archived cart found for customer ID: " + customerId));
+ .orElseThrow(() ->
+ new NoSuchElementException("No archived cart found for customer ID: " + customerId));
}
}
From 95a864048c4b96b9f836f465f2d7a107dba09c2e Mon Sep 17 00:00:00 2001
From: Abdelrahman Elmeky <65960126+Aelmeky@users.noreply.github.com>
Date: Sun, 4 May 2025 14:03:38 +0300
Subject: [PATCH 08/24] fixing lint errors
---
src/main/java/cart/CartApplication.java | 2 +-
src/main/java/cart/config/AppConfig.java | 2 +-
.../java/cart/controller/CartController.java | 24 ++++++++++---------
.../java/cart/repository/CartRepository.java | 4 ++--
src/main/java/cart/service/CartService.java | 18 +++++++-------
5 files changed, 27 insertions(+), 23 deletions(-)
diff --git a/src/main/java/cart/CartApplication.java b/src/main/java/cart/CartApplication.java
index 85eeedf..3a1e3fc 100644
--- a/src/main/java/cart/CartApplication.java
+++ b/src/main/java/cart/CartApplication.java
@@ -5,7 +5,7 @@
@SpringBootApplication
public class CartApplication {
- public static void main(final String [] args){
+ 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
index 6fa562e..70500cc 100644
--- a/src/main/java/cart/config/AppConfig.java
+++ b/src/main/java/cart/config/AppConfig.java
@@ -12,4 +12,4 @@ public class AppConfig {
public RestTemplate restTemplate() {
return new RestTemplate();
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/cart/controller/CartController.java b/src/main/java/cart/controller/CartController.java
index 6c45acf..df1c19c 100644
--- a/src/main/java/cart/controller/CartController.java
+++ b/src/main/java/cart/controller/CartController.java
@@ -32,15 +32,17 @@ public class CartController {
private final CartService cartService;
- @Operation(summary = "Create a new cart for a " +
- "customer or return existing one")
+ @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),
+ description = "Invalid customer ID provided",
+ content = @Content),
@ApiResponse(responseCode = "500",
- description = "Internal server error", content = @Content)
+ description = "Internal server error",
+ content = @Content)
})
@PostMapping("/create/{customerId}")
public ResponseEntity createCart(
@@ -77,8 +79,8 @@ public ResponseEntity deleteCart(
return ResponseEntity.noContent().build();
}
- @Operation(summary = "Add an item to the cart" +
- " or update its quantity if already exists")
+ @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"),
@@ -95,8 +97,8 @@ public ResponseEntity addItemToCart(
return ResponseEntity.ok(updatedCart);
}
- @Operation(summary = "Update quantity " +
- "of an existing item in the cart")
+ @Operation(summary = "Update quantity "
+ + "of an existing item in the cart")
@ApiResponses(value = {
@ApiResponse(responseCode = "200",
description = "Quantity updated successfully"),
@@ -109,7 +111,7 @@ public ResponseEntity addItemToCart(
public ResponseEntity updateItemQuantity(
@PathVariable("customerId") final String customerId,
@PathVariable("productId") final String productId,
- @RequestParam int quantity) {
+ @RequestParam final int quantity) {
Cart updatedCart = cartService.updateItemQuantity(
customerId, productId, quantity);
return ResponseEntity.ok(updatedCart);
@@ -189,8 +191,8 @@ public ResponseEntity checkoutCart(
Cart updatedCart = cartService.checkoutCart(customerId);
return ResponseEntity.ok(updatedCart);
} catch (Exception ex) {
- throw new IllegalCallerException("Error " +
- "communicating with Order Service");
+ throw new IllegalCallerException("Error "
+ + "communicating with Order Service");
}
}
}
diff --git a/src/main/java/cart/repository/CartRepository.java b/src/main/java/cart/repository/CartRepository.java
index 2b10e48..65d18fd 100644
--- a/src/main/java/cart/repository/CartRepository.java
+++ b/src/main/java/cart/repository/CartRepository.java
@@ -9,7 +9,7 @@
@Repository
public interface CartRepository extends MongoRepository {
Optional findByCustomerId(String customerId);
- Optional findByCustomerIdAndArchived
- (String customerId, boolean archived);
+ Optional findByCustomerIdAndArchived(
+ String customerId, boolean archived);
}
diff --git a/src/main/java/cart/service/CartService.java b/src/main/java/cart/service/CartService.java
index 2a4de6c..8eda01d 100644
--- a/src/main/java/cart/service/CartService.java
+++ b/src/main/java/cart/service/CartService.java
@@ -38,8 +38,8 @@ public Cart createCart(final String customerId) {
});
}
- public Cart addItemToCart(final String customerId
- , final CartItem newItem) {
+ public Cart addItemToCart(final String customerId,
+ final CartItem newItem) {
Cart cart = getCartByCustomerId(customerId);
Optional existingItem =
@@ -69,8 +69,8 @@ public Cart updateItemQuantity(final String customerId,
.findFirst();
if (existingItemOpt.isEmpty()) {
- throw new ResponseStatusException
- (HttpStatus.NOT_FOUND, "Product not found in cart");
+ throw new ResponseStatusException(
+ HttpStatus.NOT_FOUND, "Product not found in cart");
}
CartItem item = existingItemOpt.get();
@@ -128,8 +128,8 @@ public Cart checkoutCart(final String customerId) {
orderRequest.setCustomerId(customerId);
orderRequest.setItems(cart.getItems());
- restTemplate.postForObject
- (orderServiceUrl + "/orders", orderRequest, Void.class);
+ restTemplate.postForObject(
+ orderServiceUrl + "/orders", orderRequest, Void.class);
cart.getItems().clear();
return cartRepository.save(cart);
@@ -139,13 +139,15 @@ public Cart checkoutCart(final String customerId) {
private Cart getActiveCart(final String customerId) {
return cartRepository.findByCustomerIdAndArchived(customerId, false)
.orElseThrow(() -> new
- NoSuchElementException("Cart not found for customer ID: " + customerId));
+ NoSuchElementException("Cart"
+ + " not found for customer ID: " + customerId));
}
private Cart getArchivedCart(final String customerId) {
return cartRepository.findByCustomerIdAndArchived(customerId, true)
.orElseThrow(() ->
- new NoSuchElementException("No archived cart found for customer ID: " + customerId));
+ new NoSuchElementException("No archived"
+ + " cart found for customer ID: " + customerId));
}
}
From 838c2098eedde1d2f8ec37ca7131d50f4082cf8d Mon Sep 17 00:00:00 2001
From: Abdelrahman Elmeky <65960126+Aelmeky@users.noreply.github.com>
Date: Sun, 4 May 2025 14:44:10 +0300
Subject: [PATCH 09/24] fixing lint errors
---
src/main/java/cart/controller/CartController.java | 6 +++---
src/main/java/cart/service/CartService.java | 3 ++-
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/src/main/java/cart/controller/CartController.java b/src/main/java/cart/controller/CartController.java
index df1c19c..138f68e 100644
--- a/src/main/java/cart/controller/CartController.java
+++ b/src/main/java/cart/controller/CartController.java
@@ -25,9 +25,9 @@
@RestController
@RequestMapping("/api/cart")
@RequiredArgsConstructor
-@Tag(name = "Cart Controller", description = "Handles cart" +
- " operations like add, update," +
- " remove items and manage cart")
+@Tag(name = "Cart Controller", description = "Handles cart"
+ + " operations like add, update,"
+ + " remove items and manage cart")
public class CartController {
private final CartService cartService;
diff --git a/src/main/java/cart/service/CartService.java b/src/main/java/cart/service/CartService.java
index 8eda01d..1f7e48a 100644
--- a/src/main/java/cart/service/CartService.java
+++ b/src/main/java/cart/service/CartService.java
@@ -147,7 +147,8 @@ private Cart getArchivedCart(final String customerId) {
return cartRepository.findByCustomerIdAndArchived(customerId, true)
.orElseThrow(() ->
new NoSuchElementException("No archived"
- + " cart found for customer ID: " + customerId));
+ + " cart found for customer ID: "
+ + customerId));
}
}
From e7ff6319e96dce38427d700e7acc14203c6cd013 Mon Sep 17 00:00:00 2001
From: Abdelrahman Elmeky <65960126+Aelmeky@users.noreply.github.com>
Date: Wed, 7 May 2025 13:00:09 +0300
Subject: [PATCH 10/24] refactore : adding logger to controller and resloving
comments
---
.../java/cart/controller/CartController.java | 45 +++++++++++++++++-
src/main/java/cart/model/Cart.java | 46 ++-----------------
src/main/java/cart/model/CartItem.java | 25 +---------
src/main/java/cart/model/OrderRequest.java | 27 +++++------
src/main/java/cart/service/CartService.java | 8 +++-
5 files changed, 68 insertions(+), 83 deletions(-)
diff --git a/src/main/java/cart/controller/CartController.java b/src/main/java/cart/controller/CartController.java
index 138f68e..6260409 100644
--- a/src/main/java/cart/controller/CartController.java
+++ b/src/main/java/cart/controller/CartController.java
@@ -9,6 +9,7 @@
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;
@@ -23,11 +24,12 @@
import io.swagger.v3.oas.annotations.media.Content;
@RestController
-@RequestMapping("/api/cart")
+@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;
@@ -47,7 +49,10 @@ public class CartController {
@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);
}
@@ -61,7 +66,11 @@ public ResponseEntity createCart(
@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);
}
@@ -75,7 +84,12 @@ public ResponseEntity getCartByCustomerId(
@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();
}
@@ -93,7 +107,11 @@ public ResponseEntity deleteCart(
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);
}
@@ -112,8 +130,14 @@ 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);
}
@@ -128,8 +152,13 @@ public ResponseEntity updateItemQuantity(
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);
}
@@ -143,7 +172,10 @@ public ResponseEntity removeItemFromCart(
@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();
}
@@ -157,7 +189,10 @@ public ResponseEntity clearCart(
@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);
}
@@ -171,7 +206,10 @@ public ResponseEntity archiveCart(
@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);
}
@@ -187,10 +225,15 @@ public ResponseEntity unarchiveCart(
@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/model/Cart.java b/src/main/java/cart/model/Cart.java
index 30f4680..6d93d92 100644
--- a/src/main/java/cart/model/Cart.java
+++ b/src/main/java/cart/model/Cart.java
@@ -1,10 +1,12 @@
package cart.model;
+import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
+import org.springframework.data.mongodb.core.mapping.Field;
import java.util.ArrayList;
import java.util.List;
@@ -15,56 +17,16 @@
@AllArgsConstructor
public class Cart {
-
@Id
+ @Field("_id")
private String id;
+ @NotBlank
private String customerId;
private List items = new ArrayList<>();
private boolean archived = false;
- @Override
- public String toString() {
- return "Cart{"
- + "id='" + id + '\''
- + ", customerId='" + customerId + '\''
- + ", items=" + items
- + ", archived" + archived
- + '}';
- }
-
- public String getId() {
- return id;
- }
-
- public void setId(final String id) {
- this.id = id;
- }
-
- public String getCustomerId() {
- return customerId;
- }
-
- public void setCustomerId(final String customerId) {
- this.customerId = customerId;
- }
-
- public List getItems() {
- return items;
- }
-
- public void setItems(final List items) {
- this.items = items;
- }
-
- public boolean isArchived() {
- return archived;
- }
-
- public void setArchived(final boolean archived) {
- this.archived = archived;
- }
}
diff --git a/src/main/java/cart/model/CartItem.java b/src/main/java/cart/model/CartItem.java
index 38dd7ce..1a147c3 100644
--- a/src/main/java/cart/model/CartItem.java
+++ b/src/main/java/cart/model/CartItem.java
@@ -1,5 +1,6 @@
package cart.model;
+import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@@ -9,31 +10,9 @@
@AllArgsConstructor
public class CartItem {
+ @NotBlank
private String productId;
private int quantity;
- @Override
- public String toString() {
- return "CartItem{"
- + "productId='" + productId + '\''
- + ", quantity=" + quantity
- + '}';
- }
-
- public String getProductId() {
- return productId;
- }
-
- public void setProductId(final String productId) {
- this.productId = productId;
- }
-
- public int getQuantity() {
- return quantity;
- }
-
- public void setQuantity(final int quantity) {
- this.quantity = quantity;
- }
}
diff --git a/src/main/java/cart/model/OrderRequest.java b/src/main/java/cart/model/OrderRequest.java
index 03e899e..80b8bb2 100644
--- a/src/main/java/cart/model/OrderRequest.java
+++ b/src/main/java/cart/model/OrderRequest.java
@@ -1,26 +1,21 @@
package cart.model;
+import jakarta.validation.constraints.NotBlank;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
import java.util.List;
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
public class OrderRequest {
- private String customerId;
- private List items;
- // Getters and setters
- public String getCustomerId() {
- return customerId;
- }
-
- public void setCustomerId(final String customerId) {
- this.customerId = customerId;
- }
+ @NotBlank
+ private String customerId;
- public List getItems() {
- return items;
- }
+ private List items;
- public void setItems(final List items) {
- this.items = items;
- }
}
diff --git a/src/main/java/cart/service/CartService.java b/src/main/java/cart/service/CartService.java
index 1f7e48a..eadfcef 100644
--- a/src/main/java/cart/service/CartService.java
+++ b/src/main/java/cart/service/CartService.java
@@ -5,6 +5,7 @@
import cart.model.OrderRequest;
import cart.repository.CartRepository;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
@@ -18,6 +19,7 @@
@Service
@RequiredArgsConstructor
+@Slf4j
public class CartService {
private final CartRepository cartRepository;
@@ -29,13 +31,17 @@ public class CartService {
public Cart createCart(final String customerId) {
- return cartRepository.findByCustomerId(customerId)
+ log.debug("Entering createCart"
+ + " with customerId:", customerId);
+ Cart cart = cartRepository.findByCustomerId(customerId)
.orElseGet(() -> {
Cart newCart = new Cart(UUID.randomUUID()
.toString(), customerId, new
ArrayList<>(), false);
return cartRepository.save(newCart);
});
+ log.debug("Cart created/retrieved:", cart);
+ return cart;
}
public Cart addItemToCart(final String customerId,
From bac8a8fb7ab2eef6ed03a473cffa7ea9874f943e Mon Sep 17 00:00:00 2001
From: Abdelrahman Elmeky <65960126+Aelmeky@users.noreply.github.com>
Date: Wed, 7 May 2025 18:32:40 +0200
Subject: [PATCH 11/24] feat : adding logger to the cart service
---
src/main/java/cart/service/CartService.java | 116 ++++++++++++++++----
1 file changed, 92 insertions(+), 24 deletions(-)
diff --git a/src/main/java/cart/service/CartService.java b/src/main/java/cart/service/CartService.java
index eadfcef..ab0fb07 100644
--- a/src/main/java/cart/service/CartService.java
+++ b/src/main/java/cart/service/CartService.java
@@ -38,14 +38,19 @@ public Cart createCart(final String customerId) {
Cart newCart = new Cart(UUID.randomUUID()
.toString(), customerId, new
ArrayList<>(), false);
+ log.debug("Cart created:", newCart);
return cartRepository.save(newCart);
});
- log.debug("Cart created/retrieved:", cart);
+ log.debug("Cart retrieved:", cart);
return cart;
}
public Cart addItemToCart(final String customerId,
final CartItem newItem) {
+ log.debug("Entering addItemToCart "
+ + "with customerId:, newItem:",
+ customerId, newItem);
+
Cart cart = getCartByCustomerId(customerId);
Optional existingItem =
@@ -55,19 +60,29 @@ public Cart addItemToCart(final String customerId,
.findFirst();
if (existingItem.isPresent()) {
+ log.debug("Item already exists, updating"
+ + " quantity for productId:", newItem.getProductId());
+
existingItem.get()
.setQuantity(existingItem.get().
getQuantity() + newItem.getQuantity());
} else {
+ log.debug("Adding new item to cart for productId: ",
+ newItem.getProductId());
cart.getItems().add(newItem);
}
- return cartRepository.save(cart);
+ Cart updatedCart = cartRepository.save(cart);
+ log.debug("Cart updated:", updatedCart);
+ return updatedCart;
}
public Cart updateItemQuantity(final String customerId,
final String productId, final int quantity) {
+ log.debug("Entering updateItemQuantity with"
+ + " customerId:, productId:, quantity: ",
+ customerId, productId, quantity);
Cart cart = getCartByCustomerId(customerId);
Optional existingItemOpt = cart.getItems().stream()
@@ -75,6 +90,8 @@ public Cart updateItemQuantity(final String customerId,
.findFirst();
if (existingItemOpt.isEmpty()) {
+ log.error("Product not found in"
+ + " cart for productId:", productId);
throw new ResponseStatusException(
HttpStatus.NOT_FOUND, "Product not found in cart");
}
@@ -82,79 +99,130 @@ public Cart updateItemQuantity(final String customerId,
CartItem item = existingItemOpt.get();
if (quantity <= 0) {
+ log.debug("Removing item from cart"
+ + " as quantity <= 0 for productId:", productId);
cart.getItems().remove(item);
} else {
+ log.debug("Updating quantity to:"
+ + " for productId:", quantity, productId);
item.setQuantity(quantity);
}
- return cartRepository.save(cart);
+ Cart updatedCart = cartRepository.save(cart);
+ log.debug("Cart updated: {}", updatedCart);
+ return updatedCart;
}
public Cart removeItemFromCart(final String customerId,
final String productId) {
+ log.debug("Entering removeItemFromCart"
+ + " with customerId:, productId:", customerId, productId);
Cart cart = getCartByCustomerId(customerId);
cart.getItems().removeIf(i -> i.getProductId().equals(productId));
- return cartRepository.save(cart);
+ Cart updatedCart = cartRepository.save(cart);
+ log.debug("Item removed, updated cart: {}", updatedCart);
+ return updatedCart;
}
public void deleteCartByCustomerId(final String customerId) {
+ log.debug("Entering deleteCartByCustomerId"
+ + " with customerId:", customerId);
cartRepository.findByCustomerId(customerId)
- .ifPresent(cartRepository::delete);
+ .ifPresent(cart -> {
+ log.debug("Deleting cart for customerId:", customerId);
+ cartRepository.delete(cart);
+ });
+ log.debug("Cart deletion completed for"
+ + " customerId:", customerId);
}
public Cart getCartByCustomerId(final String customerId) {
- return cartRepository.findByCustomerId(customerId)
- .orElseThrow(() -> new
- NoSuchElementException("Cart not found"));
+ log.debug("Entering getCartByCustomerId"
+ + " with customerId:", customerId);
+ Cart cart = cartRepository.findByCustomerId(customerId)
+ .orElseThrow(() -> {
+ log.error("Cart not found for customerId:", customerId);
+ return new NoSuchElementException("Cart not found");
+ });
+ log.debug("Cart retrieved:", cart);
+ return cart;
+
}
public void clearCart(final String customerId) {
+ log.debug("Entering clearCart with customerId:", customerId);
Cart cart = getCartByCustomerId(customerId);
cart.getItems().clear();
cartRepository.save(cart);
+ log.debug("Cart cleared for customerId:", customerId);
}
public Cart archiveCart(final String customerId) {
+ log.debug("Entering archiveCart with customerId:", customerId);
Cart cart = getActiveCart(customerId);
cart.setArchived(true);
- return cartRepository.save(cart);
+ Cart archivedCart = cartRepository.save(cart);
+ log.debug("Cart archived: {}", archivedCart);
+ return archivedCart;
}
public Cart unarchiveCart(final String customerId) {
+ log.debug("Entering unarchiveCart with customerId:", customerId);
Cart cart = getArchivedCart(customerId);
cart.setArchived(false);
- return cartRepository.save(cart);
+ Cart activeCart = cartRepository.save(cart);
+ log.debug("Cart unarchived:", activeCart);
+ return activeCart;
}
public Cart checkoutCart(final String customerId) {
+ log.debug("Entering checkoutCart with customerId:", customerId);
Cart cart = getActiveCart(customerId);
OrderRequest orderRequest = new OrderRequest();
orderRequest.setCustomerId(customerId);
orderRequest.setItems(cart.getItems());
- restTemplate.postForObject(
- orderServiceUrl + "/orders", orderRequest, Void.class);
-
- cart.getItems().clear();
- return cartRepository.save(cart);
+ try {
+ log.debug("Sending order request to"
+ + " Order Service for customerId:", customerId);
+ restTemplate.postForObject(orderServiceUrl + "/orders", orderRequest, Void.class);
+ cart.getItems().clear();
+ Cart updatedCart = cartRepository.save(cart);
+ log.debug("Cart checked out and cleared:", updatedCart);
+ return updatedCart;
+ } catch (Exception e) {
+ log.error("Failed to checkout cart for customerId:", customerId, e);
+ throw new RuntimeException("Error"
+ + " communicating with Order Service", e);
+ }
}
private Cart getActiveCart(final String customerId) {
- return cartRepository.findByCustomerIdAndArchived(customerId, false)
- .orElseThrow(() -> new
- NoSuchElementException("Cart"
- + " not found for customer ID: " + customerId));
+ log.debug("Entering getActiveCart with customerId:", customerId);
+ Cart cart = cartRepository.findByCustomerIdAndArchived(customerId, false)
+ .orElseThrow(() -> {
+ log.error("Active cart not found"
+ + " for customerId:", customerId);
+ return new NoSuchElementException("Cart not"
+ + " found for customer ID: " + customerId);
+ });
+ log.debug("Active cart retrieved:", cart);
}
private Cart getArchivedCart(final String customerId) {
- return cartRepository.findByCustomerIdAndArchived(customerId, true)
- .orElseThrow(() ->
- new NoSuchElementException("No archived"
- + " cart found for customer ID: "
- + customerId));
+ log.debug("Entering getArchivedCart with customerId:", customerId);
+ Cart cart = cartRepository.findByCustomerIdAndArchived(customerId, true)
+ .orElseThrow(() -> {
+ log.error("Archived cart not found"
+ + " for customerId:", customerId);
+ return new NoSuchElementException("No archived "
+ + "cart found for customer ID: " + customerId);
+ });
+ log.debug("Archived cart retrieved:", cart);
+ return cart;
}
}
From 2f3e9a3f074087be5499bef3b5b4bb6720da3d19 Mon Sep 17 00:00:00 2001
From: Abdelrahman Elmeky <65960126+Aelmeky@users.noreply.github.com>
Date: Wed, 7 May 2025 18:33:02 +0200
Subject: [PATCH 12/24] fix : fixing sytnax error
---
src/main/java/cart/service/CartService.java | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/main/java/cart/service/CartService.java b/src/main/java/cart/service/CartService.java
index ab0fb07..ecd6640 100644
--- a/src/main/java/cart/service/CartService.java
+++ b/src/main/java/cart/service/CartService.java
@@ -210,6 +210,7 @@ private Cart getActiveCart(final String customerId) {
+ " found for customer ID: " + customerId);
});
log.debug("Active cart retrieved:", cart);
+ return cart;
}
private Cart getArchivedCart(final String customerId) {
From 2bd01abe932a13932ec751474e7a4b3c77b107ef Mon Sep 17 00:00:00 2001
From: Abdelrahman Elmeky <65960126+Aelmeky@users.noreply.github.com>
Date: Wed, 7 May 2025 18:45:48 +0200
Subject: [PATCH 13/24] adding logs to the gitignire and fixing lint errors
---
.gitignore | 2 +-
src/main/java/cart/service/CartService.java | 6 ++++--
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/.gitignore b/.gitignore
index a5d2318..773a43e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,6 @@
# Ignore Maven build output
/target/
-
+/logs/
HELP.md
target/
diff --git a/src/main/java/cart/service/CartService.java b/src/main/java/cart/service/CartService.java
index ecd6640..5f52353 100644
--- a/src/main/java/cart/service/CartService.java
+++ b/src/main/java/cart/service/CartService.java
@@ -187,7 +187,8 @@ public Cart checkoutCart(final String customerId) {
try {
log.debug("Sending order request to"
+ " Order Service for customerId:", customerId);
- restTemplate.postForObject(orderServiceUrl + "/orders", orderRequest, Void.class);
+ restTemplate.postForObject(orderServiceUrl
+ + "/orders", orderRequest, Void.class);
cart.getItems().clear();
Cart updatedCart = cartRepository.save(cart);
log.debug("Cart checked out and cleared:", updatedCart);
@@ -202,7 +203,8 @@ public Cart checkoutCart(final String customerId) {
private Cart getActiveCart(final String customerId) {
log.debug("Entering getActiveCart with customerId:", customerId);
- Cart cart = cartRepository.findByCustomerIdAndArchived(customerId, false)
+ Cart cart = cartRepository.findByCustomerIdAndArchived(customerId,
+ false)
.orElseThrow(() -> {
log.error("Active cart not found"
+ " for customerId:", customerId);
From 79640f1918915358385c3e6368e75bdf7e88f008 Mon Sep 17 00:00:00 2001
From: Abdelrahman Elmeky <65960126+Aelmeky@users.noreply.github.com>
Date: Thu, 8 May 2025 15:30:58 +0300
Subject: [PATCH 14/24] tests : adding unit tests for the cart service
---
pom.xml | 29 ++
src/test/java/service/CartServiceTest.java | 325 +++++++++++++++++++++
src/test/resources/application.properties | 4 +
3 files changed, 358 insertions(+)
create mode 100644 src/test/java/service/CartServiceTest.java
create mode 100644 src/test/resources/application.properties
diff --git a/pom.xml b/pom.xml
index 67ec612..32d7846 100644
--- a/pom.xml
+++ b/pom.xml
@@ -76,6 +76,35 @@
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
diff --git a/src/test/java/service/CartServiceTest.java b/src/test/java/service/CartServiceTest.java
new file mode 100644
index 0000000..983f455
--- /dev/null
+++ b/src/test/java/service/CartServiceTest.java
@@ -0,0 +1,325 @@
+package service;
+
+import cart.model.Cart;
+import cart.model.CartItem;
+import cart.model.OrderRequest;
+import cart.repository.CartRepository;
+import cart.service.CartService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.server.ResponseStatusException;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class CartServiceTest {
+
+ @Mock
+ private CartRepository cartRepository;
+
+ @Mock
+ private RestTemplate restTemplate;
+
+ @InjectMocks
+ private CartService cartService;
+
+ private Cart cart;
+ private CartItem cartItem;
+ private final String customerId = "cust123";
+ private final String productId = "prod456";
+ private final String cartId = UUID.randomUUID().toString();
+
+ @BeforeEach
+ void setUp() throws NoSuchFieldException, IllegalAccessException {
+ cart = new Cart(cartId, customerId, new ArrayList<>(), false);
+ cartItem = new CartItem();
+ cartItem.setProductId(productId);
+ cartItem.setQuantity(1);
+
+ // Set orderServiceUrl using reflection
+ Field orderServiceUrlField = CartService.class.getDeclaredField("orderServiceUrl");
+ orderServiceUrlField.setAccessible(true);
+ orderServiceUrlField.set(cartService, "http://localhost:8080");
+ }
+
+ @Test
+ void createCart_existingCart_returnsCart() {
+ when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
+
+ Cart result = cartService.createCart(customerId);
+
+ assertEquals(cart, result);
+ verify(cartRepository).findByCustomerId(customerId);
+ verify(cartRepository, never()).save(any());
+ }
+
+ @Test
+ void createCart_noExistingCart_createsAndSavesCart() {
+ when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.empty());
+ when(cartRepository.save(any(Cart.class))).thenAnswer(invocation -> invocation.getArgument(0));
+
+ Cart result = cartService.createCart(customerId);
+
+ assertEquals(customerId, result.getCustomerId());
+ assertFalse(result.isArchived());
+ assertTrue(result.getItems().isEmpty());
+ verify(cartRepository).findByCustomerId(customerId);
+ verify(cartRepository).save(any(Cart.class));
+ }
+
+ @Test
+ void addItemToCart_newItem_addsItem() {
+ when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
+ when(cartRepository.save(any(Cart.class))).thenReturn(cart);
+
+ Cart result = cartService.addItemToCart(customerId, cartItem);
+
+ assertEquals(1, result.getItems().size());
+ assertEquals(cartItem, result.getItems().get(0));
+ verify(cartRepository).findByCustomerId(customerId);
+ verify(cartRepository).save(cart);
+ }
+
+ @Test
+ void addItemToCart_existingItem_updatesQuantity() {
+ cart.getItems().add(cartItem);
+ CartItem newItem = new CartItem();
+ newItem.setProductId(productId);
+ newItem.setQuantity(2);
+ when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
+ when(cartRepository.save(any(Cart.class))).thenReturn(cart);
+
+ Cart result = cartService.addItemToCart(customerId, newItem);
+
+ assertEquals(1, result.getItems().size());
+ assertEquals(3, result.getItems().get(0).getQuantity());
+ verify(cartRepository).findByCustomerId(customerId);
+ verify(cartRepository).save(cart);
+ }
+
+ @Test
+ void addItemToCart_cartNotFound_throwsNoSuchElementException() {
+ when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.empty());
+
+ assertThrows(NoSuchElementException.class, () -> cartService.addItemToCart(customerId, cartItem));
+ verify(cartRepository).findByCustomerId(customerId);
+ verify(cartRepository, never()).save(any());
+ }
+
+ @Test
+ void updateItemQuantity_existingItem_updatesQuantity() {
+ cart.getItems().add(cartItem);
+ when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
+ when(cartRepository.save(any(Cart.class))).thenReturn(cart);
+
+ Cart result = cartService.updateItemQuantity(customerId, productId, 5);
+
+ assertEquals(5, result.getItems().get(0).getQuantity());
+ verify(cartRepository).findByCustomerId(customerId);
+ verify(cartRepository).save(cart);
+ }
+
+ @Test
+ void updateItemQuantity_quantityZero_removesItem() {
+ cart.getItems().add(cartItem);
+ when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
+ when(cartRepository.save(any(Cart.class))).thenReturn(cart);
+
+ Cart result = cartService.updateItemQuantity(customerId, productId, 0);
+
+ assertTrue(result.getItems().isEmpty());
+ verify(cartRepository).findByCustomerId(customerId);
+ verify(cartRepository).save(cart);
+ }
+
+ @Test
+ void updateItemQuantity_itemNotFound_throwsResponseStatusException() {
+ when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
+
+ ResponseStatusException exception = assertThrows(ResponseStatusException.class,
+ () -> cartService.updateItemQuantity(customerId, productId, 5));
+ assertEquals(HttpStatus.NOT_FOUND, exception.getStatusCode());
+ verify(cartRepository).findByCustomerId(customerId);
+ verify(cartRepository, never()).save(any());
+ }
+
+ @Test
+ void removeItemFromCart_itemExists_removesItem() {
+ cart.getItems().add(cartItem);
+ when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
+ when(cartRepository.save(any(Cart.class))).thenReturn(cart);
+
+ Cart result = cartService.removeItemFromCart(customerId, productId);
+
+ assertTrue(result.getItems().isEmpty());
+ verify(cartRepository).findByCustomerId(customerId);
+ verify(cartRepository).save(cart);
+ }
+
+ @Test
+ void removeItemFromCart_cartNotFound_throwsNoSuchElementException() {
+ when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.empty());
+
+ assertThrows(NoSuchElementException.class, () -> cartService.removeItemFromCart(customerId, productId));
+ verify(cartRepository).findByCustomerId(customerId);
+ verify(cartRepository, never()).save(any());
+ }
+
+ @Test
+ void deleteCartByCustomerId_cartExists_deletesCart() {
+ when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
+
+ cartService.deleteCartByCustomerId(customerId);
+
+ verify(cartRepository).findByCustomerId(customerId);
+ verify(cartRepository).delete(cart);
+ }
+
+ @Test
+ void deleteCartByCustomerId_cartNotFound_doesNothing() {
+ when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.empty());
+
+ cartService.deleteCartByCustomerId(customerId);
+
+ verify(cartRepository).findByCustomerId(customerId);
+ verify(cartRepository, never()).delete(any());
+ }
+
+ @Test
+ void getCartByCustomerId_cartExists_returnsCart() {
+ when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
+
+ Cart result = cartService.getCartByCustomerId(customerId);
+
+ assertEquals(cart, result);
+ verify(cartRepository).findByCustomerId(customerId);
+ }
+
+ @Test
+ void getCartByCustomerId_cartNotFound_throwsNoSuchElementException() {
+ when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.empty());
+
+ assertThrows(NoSuchElementException.class, () -> cartService.getCartByCustomerId(customerId));
+ verify(cartRepository).findByCustomerId(customerId);
+ }
+
+ @Test
+ void clearCart_cartExists_clearsItems() {
+ cart.getItems().add(cartItem);
+ when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
+ when(cartRepository.save(any(Cart.class))).thenReturn(cart);
+
+ cartService.clearCart(customerId);
+
+ assertTrue(cart.getItems().isEmpty());
+ verify(cartRepository).findByCustomerId(customerId);
+ verify(cartRepository).save(cart);
+ }
+
+ @Test
+ void clearCart_cartNotFound_throwsNoSuchElementException() {
+ when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.empty());
+
+ assertThrows(NoSuchElementException.class, () -> cartService.clearCart(customerId));
+ verify(cartRepository).findByCustomerId(customerId);
+ verify(cartRepository, never()).save(any());
+ }
+
+ @Test
+ void archiveCart_activeCart_archivesCart() {
+ when(cartRepository.findByCustomerIdAndArchived(customerId, false)).thenReturn(Optional.of(cart));
+ when(cartRepository.save(any(Cart.class))).thenReturn(cart);
+
+ Cart result = cartService.archiveCart(customerId);
+
+ assertTrue(result.isArchived());
+ verify(cartRepository).findByCustomerIdAndArchived(customerId, false);
+ verify(cartRepository).save(cart);
+ }
+
+ @Test
+ void archiveCart_noActiveCart_throwsNoSuchElementException() {
+ when(cartRepository.findByCustomerIdAndArchived(customerId, false)).thenReturn(Optional.empty());
+
+ assertThrows(NoSuchElementException.class, () -> cartService.archiveCart(customerId));
+ verify(cartRepository).findByCustomerIdAndArchived(customerId, false);
+ verify(cartRepository, never()).save(any());
+ }
+
+ @Test
+ void unarchiveCart_archivedCart_unarchivesCart() {
+ cart.setArchived(true);
+ when(cartRepository.findByCustomerIdAndArchived(customerId, true)).thenReturn(Optional.of(cart));
+ when(cartRepository.save(any(Cart.class))).thenReturn(cart);
+
+ Cart result = cartService.unarchiveCart(customerId);
+
+ assertFalse(result.isArchived());
+ verify(cartRepository).findByCustomerIdAndArchived(customerId, true);
+ verify(cartRepository).save(cart);
+ }
+
+ @Test
+ void unarchiveCart_noArchivedCart_throwsNoSuchElementException() {
+ when(cartRepository.findByCustomerIdAndArchived(customerId, true)).thenReturn(Optional.empty());
+
+ assertThrows(NoSuchElementException.class, () -> cartService.unarchiveCart(customerId));
+ verify(cartRepository).findByCustomerIdAndArchived(customerId, true);
+ verify(cartRepository, never()).save(any());
+ }
+
+ @Test
+ void checkoutCart_validCart_sendsToOrderServiceAndClearsCart() {
+ cart.getItems().add(cartItem);
+ when(cartRepository.findByCustomerIdAndArchived(customerId, false)).thenReturn(Optional.of(cart));
+ when(cartRepository.save(any(Cart.class))).thenReturn(cart);
+ when(restTemplate.postForObject(eq("http://localhost:8080/orders"), any(OrderRequest.class), eq(Void.class)))
+ .thenReturn(null);
+
+ Cart result = cartService.checkoutCart(customerId);
+
+ assertTrue(result.getItems().isEmpty());
+ verify(cartRepository).findByCustomerIdAndArchived(customerId, false);
+ verify(restTemplate).postForObject(eq("http://localhost:8080/orders"), any(OrderRequest.class), eq(Void.class));
+ verify(cartRepository).save(cart);
+ }
+
+ @Test
+ void checkoutCart_noActiveCart_throwsNoSuchElementException() {
+ when(cartRepository.findByCustomerIdAndArchived(customerId, false)).thenReturn(Optional.empty());
+
+ assertThrows(NoSuchElementException.class, () -> cartService.checkoutCart(customerId));
+ verify(cartRepository).findByCustomerIdAndArchived(customerId, false);
+ verify(restTemplate, never()).postForObject(any(), any(), any());
+ verify(cartRepository, never()).save(any());
+ }
+
+ @Test
+ void checkoutCart_orderServiceFails_throwsRuntimeException() {
+ cart.getItems().add(cartItem);
+ when(cartRepository.findByCustomerIdAndArchived(customerId, false)).thenReturn(Optional.of(cart));
+ when(restTemplate.postForObject(eq("http://localhost:8080/orders"), any(OrderRequest.class), eq(Void.class)))
+ .thenThrow(new RuntimeException("Order Service error"));
+
+ RuntimeException exception = assertThrows(RuntimeException.class, () -> cartService.checkoutCart(customerId));
+ assertEquals("Error communicating with Order Service", exception.getMessage());
+ verify(cartRepository).findByCustomerIdAndArchived(customerId, false);
+ verify(restTemplate).postForObject(eq("http://localhost:8080/orders"), any(OrderRequest.class), eq(Void.class));
+ verify(cartRepository, never()).save(any());
+ }
+}
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
new file mode 100644
index 0000000..a38ed3d
--- /dev/null
+++ b/src/test/resources/application.properties
@@ -0,0 +1,4 @@
+spring.data.mongodb.uri=mongodb://localhost:27017/testdb
+spring.data.mongodb.database=testdb
+spring.main.banner-mode= off
+spring.main.log-startup-info=false
\ No newline at end of file
From f5822f6e9bd843076b9b78b9781380077a78bc3d Mon Sep 17 00:00:00 2001
From: Abdelrahman Elmeky <65960126+Aelmeky@users.noreply.github.com>
Date: Thu, 8 May 2025 15:45:31 +0300
Subject: [PATCH 15/24] adding global hanlder
---
docker-compose.yml | 2 +-
.../exception/GlobalExceptionHandler.java | 24 ++++++++++++++++++
.../exception/GlobalHandlerException.java | 25 +++++++++++++++++++
src/main/java/cart/service/CartService.java | 7 +++---
4 files changed, 54 insertions(+), 4 deletions(-)
create mode 100644 src/main/java/cart/exception/GlobalExceptionHandler.java
create mode 100644 src/main/java/cart/exception/GlobalHandlerException.java
diff --git a/docker-compose.yml b/docker-compose.yml
index e35d882..fd7dc73 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -18,7 +18,7 @@ services:
command: ["java", "-jar", "/app/cart-0.0.1-SNAPSHOT.jar"]
cart-db:
- image: mongo:latest
+ image: mongo:8.0.9
container_name: cart-db
environment:
MONGO_INITDB_DATABASE: cartDB
diff --git a/src/main/java/cart/exception/GlobalExceptionHandler.java b/src/main/java/cart/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000..cacdeb6
--- /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
+
+
+
+ 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
+
+
diff --git a/src/main/java/cart/CartApplication.java b/src/main/java/cart/CartApplication.java
index 3a1e3fc..2ef8d6d 100644
--- a/src/main/java/cart/CartApplication.java
+++ b/src/main/java/cart/CartApplication.java
@@ -2,8 +2,10 @@
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/service/AddItemCommand.java b/src/main/java/cart/service/AddItemCommand.java
new file mode 100644
index 0000000..a0755c2
--- /dev/null
+++ b/src/main/java/cart/service/AddItemCommand.java
@@ -0,0 +1,66 @@
+package cart.service;
+
+import cart.model.Cart;
+import cart.model.CartItem;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Optional;
+
+@RequiredArgsConstructor
+@Slf4j
+public class AddItemCommand implements CartCommand {
+
+ private final CartService cartService;
+ private final String customerId;
+ private final CartItem newItem;
+
+ @Override
+ public Cart execute() {
+ log.debug("Executing AddItemCommand for customerId: {}, item: {}", customerId, newItem);
+ Cart cart = cartService.getCartByCustomerId(customerId);
+
+ Optional existingItem = cart.getItems().stream()
+ .filter(i -> i.getProductId().equals(newItem.getProductId()))
+ .findFirst();
+
+ if (existingItem.isPresent()) {
+ log.debug("Item exists, updating quantity for productId: {}", newItem.getProductId());
+ existingItem.get().setQuantity(existingItem.get().getQuantity() + newItem.getQuantity());
+ } else {
+ log.debug("Adding new item to cart for productId: {}", newItem.getProductId());
+ cart.getItems().add(newItem);
+ }
+
+ Cart updatedCart = cartService.saveCart(cart);
+ log.debug("AddItemCommand executed, updated cart: {}", updatedCart);
+ return updatedCart;
+ }
+
+ @Override
+ public Cart undo() {
+ log.debug("Undoing AddItemCommand for customerId: {}, item: {}", customerId, newItem);
+ Cart cart = cartService.getCartByCustomerId(customerId);
+
+ Optional existingItem = cart.getItems().stream()
+ .filter(i -> i.getProductId().equals(newItem.getProductId()))
+ .findFirst();
+
+ if (existingItem.isPresent()) {
+ int newQuantity = existingItem.get().getQuantity() - newItem.getQuantity();
+ if (newQuantity <= 0) {
+ log.debug("Removing item during undo for productId: {}", newItem.getProductId());
+ cart.getItems().removeIf(i -> i.getProductId().equals(newItem.getProductId()));
+ } else {
+ log.debug("Reducing quantity during undo for productId: {}", newItem.getProductId());
+ existingItem.get().setQuantity(newQuantity);
+ }
+ } else {
+ log.warn("Item not found during undo for productId: {}", newItem.getProductId());
+ }
+
+ Cart updatedCart = cartService.saveCart(cart);
+ log.debug("AddItemCommand undone, updated cart: {}", updatedCart);
+ return updatedCart;
+ }
+}
diff --git a/src/main/java/cart/service/CartCommand.java b/src/main/java/cart/service/CartCommand.java
new file mode 100644
index 0000000..052da6f
--- /dev/null
+++ b/src/main/java/cart/service/CartCommand.java
@@ -0,0 +1,8 @@
+package cart.service;
+
+import cart.model.Cart;
+
+public interface CartCommand {
+ Cart execute();
+ Cart undo();
+}
\ No newline at end of file
diff --git a/src/main/java/cart/service/CartService.java b/src/main/java/cart/service/CartService.java
index edfce98..2745f4e 100644
--- a/src/main/java/cart/service/CartService.java
+++ b/src/main/java/cart/service/CartService.java
@@ -50,31 +50,8 @@ public Cart addItemToCart(final String customerId,
log.debug("Entering addItemToCart "
+ "with customerId:, newItem:",
customerId, newItem);
-
- Cart cart = getCartByCustomerId(customerId);
-
- Optional existingItem =
- cart.getItems().stream()
- .filter(i -> i.getProductId()
- .equals(newItem.getProductId()))
- .findFirst();
-
- if (existingItem.isPresent()) {
- log.debug("Item already exists, updating"
- + " quantity for productId:", newItem.getProductId());
-
- existingItem.get()
- .setQuantity(existingItem.get().
- getQuantity() + newItem.getQuantity());
- } else {
- log.debug("Adding new item to cart for productId: ",
- newItem.getProductId());
- cart.getItems().add(newItem);
- }
-
- Cart updatedCart = cartRepository.save(cart);
- log.debug("Cart updated:", updatedCart);
- return updatedCart;
+ CartCommand command = new AddItemCommand(this, customerId, newItem);
+ return command.execute();
}
@@ -83,34 +60,8 @@ public Cart updateItemQuantity(final String customerId,
log.debug("Entering updateItemQuantity with"
+ " customerId:, productId:, quantity: ",
customerId, productId, quantity);
- Cart cart = getCartByCustomerId(customerId);
-
- Optional existingItemOpt = cart.getItems().stream()
- .filter(i -> i.getProductId().equals(productId))
- .findFirst();
-
- if (existingItemOpt.isEmpty()) {
- log.error("Product not found in"
- + " cart for productId:", productId);
- throw new GlobalHandlerException(
- HttpStatus.NOT_FOUND, "Product not found in cart");
- }
-
- CartItem item = existingItemOpt.get();
-
- if (quantity <= 0) {
- log.debug("Removing item from cart"
- + " as quantity <= 0 for productId:", productId);
- cart.getItems().remove(item);
- } else {
- log.debug("Updating quantity to:"
- + " for productId:", quantity, productId);
- item.setQuantity(quantity);
- }
-
- Cart updatedCart = cartRepository.save(cart);
- log.debug("Cart updated: {}", updatedCart);
- return updatedCart;
+ CartCommand command = new UpdateQuantityCommand(this, customerId, productId, quantity);
+ return command.execute();
}
@@ -118,11 +69,9 @@ public Cart removeItemFromCart(final String customerId,
final String productId) {
log.debug("Entering removeItemFromCart"
+ " with customerId:, productId:", customerId, productId);
- Cart cart = getCartByCustomerId(customerId);
- cart.getItems().removeIf(i -> i.getProductId().equals(productId));
- Cart updatedCart = cartRepository.save(cart);
- log.debug("Item removed, updated cart: {}", updatedCart);
- return updatedCart;
+
+ CartCommand command = new RemoveItemCommand(this, customerId, productId);
+ return command.execute();
}
public void deleteCartByCustomerId(final String customerId) {
@@ -229,4 +178,8 @@ private Cart getArchivedCart(final String customerId) {
return cart;
}
+ public Cart saveCart(Cart cart) {
+ return cartRepository.save(cart);
+ }
+
}
diff --git a/src/main/java/cart/service/RemoveItemCommand.java b/src/main/java/cart/service/RemoveItemCommand.java
new file mode 100644
index 0000000..2745cdc
--- /dev/null
+++ b/src/main/java/cart/service/RemoveItemCommand.java
@@ -0,0 +1,57 @@
+package cart.service;
+
+import cart.model.Cart;
+import cart.model.CartItem;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Optional;
+
+@RequiredArgsConstructor
+@Slf4j
+public class RemoveItemCommand implements CartCommand {
+
+ private final CartService cartService;
+ private final String customerId;
+ private final String productId;
+ private CartItem removedItem;
+
+ @Override
+ public Cart execute() {
+ log.debug("Executing RemoveItemCommand for customerId: {}, productId: {}", customerId, productId);
+ Cart cart = cartService.getCartByCustomerId(customerId);
+
+ Optional itemToRemove = cart.getItems().stream()
+ .filter(i -> i.getProductId().equals(productId))
+ .findFirst();
+
+ if (itemToRemove.isPresent()) {
+ removedItem = new CartItem(itemToRemove.get().getProductId(), itemToRemove.get().getQuantity());
+ cart.getItems().removeIf(i -> i.getProductId().equals(productId));
+ log.debug("Item removed for productId: {}", productId);
+ } else {
+ log.warn("Item not found for removal, productId: {}", productId);
+ }
+
+ Cart updatedCart = cartService.saveCart(cart);
+ log.debug("RemoveItemCommand executed, updated cart: {}", updatedCart);
+ return updatedCart;
+ }
+
+ @Override
+ public Cart undo() {
+ log.debug("Undoing RemoveItemCommand for customerId: {}, productId: {}", customerId, productId);
+ Cart cart = cartService.getCartByCustomerId(customerId);
+
+ if (removedItem != null) {
+ log.debug("Restoring item during undo for productId: {}", productId);
+ cart.getItems().add(removedItem);
+ } else {
+ log.warn("No item to restore during undo for productId: {}", productId);
+ }
+
+ Cart updatedCart = cartService.saveCart(cart);
+ log.debug("RemoveItemCommand undone, updated cart: {}", updatedCart);
+ return updatedCart;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/cart/service/UpdateQuantityCommand.java b/src/main/java/cart/service/UpdateQuantityCommand.java
new file mode 100644
index 0000000..02dbc66
--- /dev/null
+++ b/src/main/java/cart/service/UpdateQuantityCommand.java
@@ -0,0 +1,81 @@
+package cart.service;
+
+import cart.exception.GlobalHandlerException;
+import cart.model.Cart;
+import cart.model.CartItem;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+
+import java.util.Optional;
+
+@RequiredArgsConstructor
+@Slf4j
+public class UpdateQuantityCommand implements CartCommand {
+
+ private final CartService cartService;
+ private final String customerId;
+ private final String productId;
+ private final int newQuantity;
+ private Integer previousQuantity; // For undo
+
+ @Override
+ public Cart execute() {
+ log.debug("Executing UpdateQuantityCommand for customerId: {}, productId: {}, quantity: {}", customerId, productId, newQuantity);
+ Cart cart = cartService.getCartByCustomerId(customerId);
+
+ Optional existingItemOpt = cart.getItems().stream()
+ .filter(i -> i.getProductId().equals(productId))
+ .findFirst();
+
+ if (existingItemOpt.isEmpty()) {
+ log.error("Product not found in cart for productId: {}", productId);
+ throw new GlobalHandlerException(HttpStatus.NOT_FOUND, "Product not found in cart");
+ }
+
+ CartItem item = existingItemOpt.get();
+ previousQuantity = item.getQuantity();
+
+ if (newQuantity <= 0) {
+ log.debug("Removing item as quantity <= 0 for productId: {}", productId);
+ cart.getItems().remove(item);
+ } else {
+ log.debug("Updating quantity to: {} for productId: {}", newQuantity, productId);
+ item.setQuantity(newQuantity);
+ }
+
+ Cart updatedCart = cartService.saveCart(cart);
+ log.debug("UpdateQuantityCommand executed, updated cart: {}", updatedCart);
+ return updatedCart;
+ }
+
+ @Override
+ public Cart undo() {
+ log.debug("Undoing UpdateQuantityCommand for customerId: {}, productId: {}", customerId, productId);
+ Cart cart = cartService.getCartByCustomerId(customerId);
+
+ if (previousQuantity == null) {
+ log.warn("No previous quantity to restore for productId: {}", productId);
+ return cart;
+ }
+
+ Optional existingItemOpt = cart.getItems().stream()
+ .filter(i -> i.getProductId().equals(productId))
+ .findFirst();
+
+ if (previousQuantity <= 0) {
+ log.debug("Restoring removed item during undo for productId: {}", productId);
+ cart.getItems().add(new CartItem(productId, previousQuantity));
+ } else if (existingItemOpt.isPresent()) {
+ log.debug("Restoring previous quantity during undo for productId: {}", productId);
+ existingItemOpt.get().setQuantity(previousQuantity);
+ } else {
+ log.debug("Adding item back during undo for productId: {}", productId);
+ cart.getItems().add(new CartItem(productId, previousQuantity));
+ }
+
+ Cart updatedCart = cartService.saveCart(cart);
+ log.debug("UpdateQuantityCommand undone, updated cart: {}", updatedCart);
+ return updatedCart;
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/api/CartControllerTests.java b/src/test/java/api/CartControllerTests.java
new file mode 100644
index 0000000..919e57d
--- /dev/null
+++ b/src/test/java/api/CartControllerTests.java
@@ -0,0 +1,325 @@
+//package api;
+//
+//import cart.model.Cart;
+//import cart.model.CartItem;
+//import cart.repository.CartRepository;
+//import com.fasterxml.jackson.databind.ObjectMapper;
+//import lombok.RequiredArgsConstructor;
+//import org.junit.jupiter.api.AfterEach;
+//import org.junit.jupiter.api.BeforeEach;
+//import org.junit.jupiter.api.Test;
+//import org.springframework.beans.factory.annotation.Value;
+//import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+//import org.springframework.boot.test.context.SpringBootTest;
+//import org.springframework.http.HttpMethod;
+//import org.springframework.http.MediaType;
+//import org.springframework.test.context.ActiveProfiles;
+//import org.springframework.test.web.client.ExpectedCount;
+//import org.springframework.test.web.client.MockRestServiceServer;
+//import org.springframework.test.web.servlet.MockMvc;
+//import org.springframework.web.client.RestTemplate;
+//
+//import java.util.ArrayList;
+//import java.util.List;
+//import java.util.UUID;
+//
+//import static org.hamcrest.Matchers.*;
+//import static org.junit.jupiter.api.Assertions.*;
+//import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
+//import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
+//import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
+//import static org.springframework.test.web.client.response.MockRestResponseCreators.withServerError;
+//import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+//import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+//
+//@SpringBootTest
+//@AutoConfigureMockMvc
+//@RequiredArgsConstructor
+//@ActiveProfiles("test")
+//public class CartControllerTests {
+//
+// private MockMvc mockMvc;
+//
+// private CartRepository cartRepository;
+//
+// private ObjectMapper objectMapper;
+//
+// private RestTemplate restTemplate;
+//
+// private MockRestServiceServer mockServer;
+//
+// @Value("${order.service.url}")
+// private String orderServiceUrl;
+//
+// private String customerId;
+// private String productId1;
+// private String productId2;
+//
+// @BeforeEach
+// void setUp() {
+// // Initialize MockRestServiceServer
+// mockServer = MockRestServiceServer.createServer(restTemplate);
+//
+// cartRepository.deleteAll(); // Clean slate before each test
+//
+// customerId = "cust-" + UUID.randomUUID().toString();
+// productId1 = "prod-" + UUID.randomUUID().toString();
+// productId2 = "prod-" + UUID.randomUUID().toString();
+// }
+//
+// @AfterEach
+// void tearDown() {
+// cartRepository.deleteAll(); // Clean up after each test
+// mockServer.verify(); // Verify all expected RestTemplate calls were made
+// }
+//
+// private Cart createAndSaveTestCart(String custId, boolean archived, CartItem... items) {
+// Cart cart = new Cart(UUID.randomUUID().toString(), custId, new ArrayList<>(List.of(items)), archived);
+// return cartRepository.save(cart);
+// }
+//
+// @Test
+// void createCart_shouldCreateNewCart_whenCartDoesNotExist() throws Exception {
+// mockMvc.perform(post("/api/carts/create/{customerId}", customerId))
+// .andExpect(status().isOk())
+// .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+// .andExpect(jsonPath("$.customerId", is(customerId)))
+// .andExpect(jsonPath("$.items", empty()))
+// .andExpect(jsonPath("$.archived", is(false)));
+//
+// assertTrue(cartRepository.findByCustomerId(customerId).isPresent());
+// }
+//
+// @Test
+// void createCart_shouldReturnExistingCart_whenCartExists() throws Exception {
+// Cart existingCart = createAndSaveTestCart(customerId, false, new CartItem(productId1, 1));
+//
+// mockMvc.perform(post("/api/carts/create/{customerId}", customerId))
+// .andExpect(status().isOk())
+// .andExpect(jsonPath("$.id", is(existingCart.getId())))
+// .andExpect(jsonPath("$.customerId", is(customerId)))
+// .andExpect(jsonPath("$.items[0].productId", is(productId1)));
+//
+// assertEquals(1, cartRepository.count());
+// }
+//
+// @Test
+// void getCartByCustomerId_shouldReturnCart_whenExists() throws Exception {
+// Cart cart = createAndSaveTestCart(customerId, false, new CartItem(productId1, 2));
+//
+// mockMvc.perform(get("/api/carts/customer/{customerId}", customerId))
+// .andExpect(status().isOk())
+// .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+// .andExpect(jsonPath("$.id", is(cart.getId())))
+// .andExpect(jsonPath("$.items[0].productId", is(productId1)))
+// .andExpect(jsonPath("$.items[0].quantity", is(2)));
+// }
+//
+// @Test
+// void getCartByCustomerId_shouldReturnNotFound_whenNotExists() throws Exception {
+// mockMvc.perform(get("/api/carts/customer/{customerId}", "non-existent-customer"))
+// .andExpect(status().isNotFound());
+// }
+//
+// @Test
+// void deleteCart_shouldDeleteCartAndReturnNoContent() throws Exception {
+// createAndSaveTestCart(customerId, false);
+//
+// mockMvc.perform(delete("/api/carts/customer/{customerId}", customerId))
+// .andExpect(status().isNoContent());
+//
+// assertFalse(cartRepository.findByCustomerId(customerId).isPresent());
+// }
+//
+// @Test
+// void deleteCart_shouldDoNothingAndReturnNoContent_whenCartNotFound() throws Exception {
+// mockMvc.perform(delete("/api/carts/customer/{customerId}", customerId))
+// .andExpect(status().isNoContent()); // Service method handles not found gracefully for delete
+// }
+//
+//
+// @Test
+// void addItemToCart_shouldAddNewItemToExistingCart() throws Exception {
+// createAndSaveTestCart(customerId, false);
+// CartItem newItem = new CartItem(productId1, 3);
+//
+// mockMvc.perform(post("/api/carts/{customerId}/items", customerId)
+// .contentType(MediaType.APPLICATION_JSON)
+// .content(objectMapper.writeValueAsString(newItem)))
+// .andExpect(status().isOk())
+// .andExpect(jsonPath("$.items", hasSize(1)))
+// .andExpect(jsonPath("$.items[0].productId", is(productId1)))
+// .andExpect(jsonPath("$.items[0].quantity", is(3)));
+//
+// Cart updatedCart = cartRepository.findByCustomerId(customerId).orElseThrow();
+// assertEquals(1, updatedCart.getItems().size());
+// assertEquals(productId1, updatedCart.getItems().get(0).getProductId());
+// }
+//
+// @Test
+// void addItemToCart_shouldUpdateQuantityIfItemExists() throws Exception {
+// createAndSaveTestCart(customerId, false, new CartItem(productId1, 2));
+// CartItem itemToAdd = new CartItem(productId1, 3); // Adding more of the same item
+//
+// mockMvc.perform(post("/api/carts/{customerId}/items", customerId)
+// .contentType(MediaType.APPLICATION_JSON)
+// .content(objectMapper.writeValueAsString(itemToAdd)))
+// .andExpect(status().isOk())
+// .andExpect(jsonPath("$.items", hasSize(1)))
+// .andExpect(jsonPath("$.items[0].productId", is(productId1)))
+// .andExpect(jsonPath("$.items[0].quantity", is(5))); // 2 + 3
+//
+// Cart updatedCart = cartRepository.findByCustomerId(customerId).orElseThrow();
+// assertEquals(5, updatedCart.getItems().get(0).getQuantity());
+// }
+//
+// @Test
+// void updateItemQuantity_shouldUpdateQuantityOfExistingItem() throws Exception {
+// createAndSaveTestCart(customerId, false, new CartItem(productId1, 2));
+// int newQuantity = 5;
+//
+// mockMvc.perform(patch("/api/carts/{customerId}/items/{productId}", customerId, productId1)
+// .param("quantity", String.valueOf(newQuantity)))
+// .andExpect(status().isOk())
+// .andExpect(jsonPath("$.items[0].quantity", is(newQuantity)));
+//
+// Cart updatedCart = cartRepository.findByCustomerId(customerId).orElseThrow();
+// assertEquals(newQuantity, updatedCart.getItems().get(0).getQuantity());
+// }
+//
+// @Test
+// void updateItemQuantity_shouldRemoveItemIfQuantityIsZero() throws Exception {
+// createAndSaveTestCart(customerId, false, new CartItem(productId1, 2));
+//
+// mockMvc.perform(patch("/api/carts/{customerId}/items/{productId}", customerId, productId1)
+// .param("quantity", "0"))
+// .andExpect(status().isOk())
+// .andExpect(jsonPath("$.items", empty()));
+//
+// Cart updatedCart = cartRepository.findByCustomerId(customerId).orElseThrow();
+// assertTrue(updatedCart.getItems().isEmpty());
+// }
+//
+// @Test
+// void updateItemQuantity_shouldReturnNotFound_whenItemNotInCart() throws Exception {
+// createAndSaveTestCart(customerId, false); // Cart exists but is empty
+//
+// mockMvc.perform(patch("/api/carts/{customerId}/items/{productId}", customerId, "non-existent-product")
+// .param("quantity", "5"))
+// .andExpect(status().isNotFound()); // Based on CartService logic throwing GlobalHandlerException
+// }
+//
+//
+// @Test
+// void removeItemFromCart_shouldRemoveItem() throws Exception {
+// createAndSaveTestCart(customerId, false, new CartItem(productId1, 1), new CartItem(productId2, 1));
+//
+// mockMvc.perform(delete("/api/carts/{customerId}/items/{productId}", customerId, productId1))
+// .andExpect(status().isOk())
+// .andExpect(jsonPath("$.items", hasSize(1)))
+// .andExpect(jsonPath("$.items[0].productId", is(productId2)));
+//
+// Cart updatedCart = cartRepository.findByCustomerId(customerId).orElseThrow();
+// assertEquals(1, updatedCart.getItems().size());
+// assertEquals(productId2, updatedCart.getItems().get(0).getProductId());
+// }
+//
+// @Test
+// void clearCart_shouldRemoveAllItemsFromCart() throws Exception {
+// createAndSaveTestCart(customerId, false, new CartItem(productId1, 1), new CartItem(productId2, 1));
+//
+// mockMvc.perform(delete("/api/carts/{customerId}/clear", customerId))
+// .andExpect(status().isNoContent());
+//
+// Cart clearedCart = cartRepository.findByCustomerId(customerId).orElseThrow();
+// assertTrue(clearedCart.getItems().isEmpty());
+// }
+//
+// @Test
+// void archiveCart_shouldSetArchivedToTrue() throws Exception {
+// createAndSaveTestCart(customerId, false, new CartItem(productId1, 1));
+//
+// mockMvc.perform(patch("/api/carts/{customerId}/archive", customerId))
+// .andExpect(status().isOk())
+// .andExpect(jsonPath("$.archived", is(true)));
+//
+// Cart archivedCart = cartRepository.findByCustomerId(customerId).orElseThrow();
+// assertTrue(archivedCart.isArchived());
+// }
+//
+// @Test
+// void archiveCart_shouldReturnNotFound_whenActiveCartNotExists() throws Exception {
+// // Ensure no active cart exists or only an archived one
+// createAndSaveTestCart(customerId, true); // Save an already archived cart
+//
+// mockMvc.perform(patch("/api/carts/{customerId}/archive", customerId))
+// .andExpect(status().isNotFound()); // Because getActiveCart will throw NoSuchElementException
+// }
+//
+// @Test
+// void unarchiveCart_shouldSetArchivedToFalse() throws Exception {
+// createAndSaveTestCart(customerId, true, new CartItem(productId1, 1)); // Start with an archived cart
+//
+// mockMvc.perform(patch("/api/carts/{customerId}/unarchive", customerId))
+// .andExpect(status().isOk())
+// .andExpect(jsonPath("$.archived", is(false)));
+//
+// Cart unarchivedCart = cartRepository.findByCustomerId(customerId).orElseThrow();
+// assertFalse(unarchivedCart.isArchived());
+// }
+//
+// @Test
+// void unarchiveCart_shouldReturnNotFound_whenArchivedCartNotExists() throws Exception {
+// // Ensure no archived cart exists or only an active one
+// createAndSaveTestCart(customerId, false); // Save an active cart
+//
+// mockMvc.perform(patch("/api/carts/{customerId}/unarchive", customerId))
+// .andExpect(status().isNotFound()); // Because getArchivedCart will throw NoSuchElementException
+// }
+//
+// @Test
+// void checkoutCart_shouldClearCartAndCallOrderService_whenSuccessful() throws Exception {
+// Cart cartToCheckout = createAndSaveTestCart(customerId, false, new CartItem(productId1, 2), new CartItem(productId2, 1));
+//
+// // Expect a POST request to the order service
+// mockServer.expect(ExpectedCount.once(),
+// requestTo(orderServiceUrl + "/orders"))
+// .andExpect(method(HttpMethod.POST))
+// // You can add more specific assertions for the request body if needed:
+// // .andExpect(content().json(objectMapper.writeValueAsString(expectedOrderRequest)))
+// .andRespond(withSuccess()); // Simulate a successful response from Order Service
+//
+// mockMvc.perform(post("/api/carts/{customerId}/checkout", customerId))
+// .andExpect(status().isOk())
+// .andExpect(jsonPath("$.customerId", is(customerId)))
+// .andExpect(jsonPath("$.items", empty())); // Cart items should be cleared
+//
+// Cart finalCartState = cartRepository.findByCustomerId(customerId).orElseThrow();
+// assertTrue(finalCartState.getItems().isEmpty());
+// assertFalse(finalCartState.isArchived()); // Should remain active but empty
+// }
+//
+// @Test
+// void checkoutCart_shouldReturnInternalServerError_whenOrderServiceFails() throws Exception {
+// createAndSaveTestCart(customerId, false, new CartItem(productId1, 1));
+//
+// mockServer.expect(ExpectedCount.once(),
+// requestTo(orderServiceUrl + "/orders"))
+// .andExpect(method(HttpMethod.POST))
+// .andRespond(withServerError()); // Simulate an error from Order Service
+//
+// mockMvc.perform(post("/api/carts/{customerId}/checkout", customerId))
+// .andExpect(status().isInternalServerError()) // Based on your controller's exception handling
+// .andExpect(content().string(containsString("Error communicating with Order Service"))); // Check error message
+//
+// // Cart should NOT be cleared if order service call fails
+// Cart cartStateAfterFailure = cartRepository.findByCustomerId(customerId).orElseThrow();
+// assertFalse(cartStateAfterFailure.getItems().isEmpty());
+// }
+//
+// @Test
+// void checkoutCart_shouldReturnNotFound_whenCartNotExists() throws Exception {
+// mockMvc.perform(post("/api/carts/{customerId}/checkout", "non-existent-customer"))
+// .andExpect(status().isNotFound());
+// }
+//}
\ No newline at end of file
diff --git a/src/test/java/service/CartServiceTest.java b/src/test/java/service/CartServiceTest.java
index 1e1318f..d7670d3 100644
--- a/src/test/java/service/CartServiceTest.java
+++ b/src/test/java/service/CartServiceTest.java
@@ -1,5 +1,4 @@
package service;
-
import cart.exception.GlobalHandlerException;
import cart.model.Cart;
import cart.model.CartItem;
@@ -13,10 +12,9 @@
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
+import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.client.RestTemplate;
-import org.springframework.web.server.ResponseStatusException;
-import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.NoSuchElementException;
import java.util.Optional;
@@ -44,19 +42,16 @@ class CartServiceTest {
private final String customerId = "cust123";
private final String productId = "prod456";
private final String cartId = UUID.randomUUID().toString();
+ private final String orderServiceUrl = "http://localhost:8080";
@BeforeEach
- void setUp() throws NoSuchFieldException, IllegalAccessException {
+ void setUp() {
// Initialize test data
cart = new Cart(cartId, customerId, new ArrayList<>(), false);
- cartItem = new CartItem();
- cartItem.setProductId(productId);
- cartItem.setQuantity(1);
-
- // Set orderServiceUrl using reflection
- Field orderServiceUrlField = CartService.class.getDeclaredField("orderServiceUrl");
- orderServiceUrlField.setAccessible(true);
- orderServiceUrlField.set(cartService, "http://localhost:8080");
+ cartItem = new CartItem(productId, 1);
+
+ // Set orderServiceUrl
+ ReflectionTestUtils.setField(cartService, "orderServiceUrl", orderServiceUrl);
}
@Test
@@ -92,17 +87,16 @@ void addItemToCart_newItem_addsItem() {
Cart result = cartService.addItemToCart(customerId, cartItem);
assertEquals(1, result.getItems().size());
- assertEquals(cartItem, result.getItems().get(0));
+ assertEquals(cartItem.getProductId(), result.getItems().get(0).getProductId());
+ assertEquals(cartItem.getQuantity(), result.getItems().get(0).getQuantity());
verify(cartRepository).findByCustomerId(customerId);
verify(cartRepository).save(cart);
}
@Test
void addItemToCart_existingItem_updatesQuantity() {
- cart.getItems().add(cartItem);
- CartItem newItem = new CartItem();
- newItem.setProductId(productId);
- newItem.setQuantity(2);
+ cart.getItems().add(new CartItem(productId, 1));
+ CartItem newItem = new CartItem(productId, 2);
when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
when(cartRepository.save(any(Cart.class))).thenReturn(cart);
@@ -128,7 +122,7 @@ void addItemToCart_cartNotFound_throwsGlobalHandlerException() {
@Test
void updateItemQuantity_existingItem_updatesQuantity() {
- cart.getItems().add(cartItem);
+ cart.getItems().add(new CartItem(productId, 1));
when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
when(cartRepository.save(any(Cart.class))).thenReturn(cart);
@@ -141,7 +135,7 @@ void updateItemQuantity_existingItem_updatesQuantity() {
@Test
void updateItemQuantity_quantityZero_removesItem() {
- cart.getItems().add(cartItem);
+ cart.getItems().add(new CartItem(productId, 1));
when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
when(cartRepository.save(any(Cart.class))).thenReturn(cart);
@@ -166,7 +160,7 @@ void updateItemQuantity_itemNotFound_throwsGlobalHandlerException() {
@Test
void removeItemFromCart_itemExists_removesItem() {
- cart.getItems().add(cartItem);
+ cart.getItems().add(new CartItem(productId, 1));
when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
when(cartRepository.save(any(Cart.class))).thenReturn(cart);
@@ -232,7 +226,7 @@ void getCartByCustomerId_cartNotFound_throwsGlobalHandlerException() {
@Test
void clearCart_cartExists_clearsItems() {
- cart.getItems().add(cartItem);
+ cart.getItems().add(new CartItem(productId, 1));
when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
when(cartRepository.save(any(Cart.class))).thenReturn(cart);
@@ -300,17 +294,17 @@ void unarchiveCart_noArchivedCart_throwsNoSuchElementException() {
@Test
void checkoutCart_validCart_sendsToOrderServiceAndClearsCart() {
- cart.getItems().add(cartItem);
+ cart.getItems().add(new CartItem(productId, 1));
when(cartRepository.findByCustomerIdAndArchived(customerId, false)).thenReturn(Optional.of(cart));
when(cartRepository.save(any(Cart.class))).thenReturn(cart);
- when(restTemplate.postForObject(eq("http://localhost:8080/orders"), any(OrderRequest.class), eq(Void.class)))
+ when(restTemplate.postForObject(eq(orderServiceUrl + "/orders"), any(OrderRequest.class), eq(Void.class)))
.thenReturn(null);
Cart result = cartService.checkoutCart(customerId);
assertTrue(result.getItems().isEmpty());
verify(cartRepository).findByCustomerIdAndArchived(customerId, false);
- verify(restTemplate).postForObject(eq("http://localhost:8080/orders"), any(OrderRequest.class), eq(Void.class));
+ verify(restTemplate).postForObject(eq(orderServiceUrl + "/orders"), any(OrderRequest.class), eq(Void.class));
verify(cartRepository).save(cart);
}
@@ -326,15 +320,15 @@ void checkoutCart_noActiveCart_throwsNoSuchElementException() {
@Test
void checkoutCart_orderServiceFails_throwsRuntimeException() {
- cart.getItems().add(cartItem);
+ cart.getItems().add(new CartItem(productId, 1));
when(cartRepository.findByCustomerIdAndArchived(customerId, false)).thenReturn(Optional.of(cart));
- when(restTemplate.postForObject(eq("http://localhost:8080/orders"), any(OrderRequest.class), eq(Void.class)))
+ when(restTemplate.postForObject(eq(orderServiceUrl + "/orders"), any(OrderRequest.class), eq(Void.class)))
.thenThrow(new RuntimeException("Order Service error"));
RuntimeException exception = assertThrows(RuntimeException.class, () -> cartService.checkoutCart(customerId));
assertEquals("Error communicating with Order Service", exception.getMessage());
verify(cartRepository).findByCustomerIdAndArchived(customerId, false);
- verify(restTemplate).postForObject(eq("http://localhost:8080/orders"), any(OrderRequest.class), eq(Void.class));
+ verify(restTemplate).postForObject(eq(orderServiceUrl + "/orders"), any(OrderRequest.class), eq(Void.class));
verify(cartRepository, never()).save(any());
}
}
\ No newline at end of file
From bebc8270e8b472fc912dad3e2dd3e1a3ce6db79c Mon Sep 17 00:00:00 2001
From: Abdelrahman Elmeky <65960126+Aelmeky@users.noreply.github.com>
Date: Sun, 11 May 2025 22:24:02 +0300
Subject: [PATCH 20/24] reafctor : fixing lint error
---
src/main/java/cart/service/CartCommand.java | 2 +-
src/main/java/cart/service/CartService.java | 3 +-
.../java/cart/service/RemoveItemCommand.java | 2 +-
.../cart/service/UpdateQuantityCommand.java | 35 ++++++++++++-------
4 files changed, 26 insertions(+), 16 deletions(-)
diff --git a/src/main/java/cart/service/CartCommand.java b/src/main/java/cart/service/CartCommand.java
index 052da6f..da64aad 100644
--- a/src/main/java/cart/service/CartCommand.java
+++ b/src/main/java/cart/service/CartCommand.java
@@ -5,4 +5,4 @@
public interface CartCommand {
Cart execute();
Cart undo();
-}
\ No newline at end of file
+}
diff --git a/src/main/java/cart/service/CartService.java b/src/main/java/cart/service/CartService.java
index 2745f4e..cd53992 100644
--- a/src/main/java/cart/service/CartService.java
+++ b/src/main/java/cart/service/CartService.java
@@ -14,7 +14,6 @@
import java.util.ArrayList;
import java.util.NoSuchElementException;
-import java.util.Optional;
import java.util.UUID;
@Service
@@ -178,7 +177,7 @@ private Cart getArchivedCart(final String customerId) {
return cart;
}
- public Cart saveCart(Cart cart) {
+ public Cart saveCart(final Cart cart) {
return cartRepository.save(cart);
}
diff --git a/src/main/java/cart/service/RemoveItemCommand.java b/src/main/java/cart/service/RemoveItemCommand.java
index 2745cdc..1486f4a 100644
--- a/src/main/java/cart/service/RemoveItemCommand.java
+++ b/src/main/java/cart/service/RemoveItemCommand.java
@@ -54,4 +54,4 @@ public Cart undo() {
log.debug("RemoveItemCommand undone, updated cart: {}", updatedCart);
return updatedCart;
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/cart/service/UpdateQuantityCommand.java b/src/main/java/cart/service/UpdateQuantityCommand.java
index 02dbc66..0b3f4b7 100644
--- a/src/main/java/cart/service/UpdateQuantityCommand.java
+++ b/src/main/java/cart/service/UpdateQuantityCommand.java
@@ -17,11 +17,13 @@ public class UpdateQuantityCommand implements CartCommand {
private final String customerId;
private final String productId;
private final int newQuantity;
- private Integer previousQuantity; // For undo
+ private Integer previousQuantity;
@Override
public Cart execute() {
- log.debug("Executing UpdateQuantityCommand for customerId: {}, productId: {}, quantity: {}", customerId, productId, newQuantity);
+ log.debug("Executing UpdateQuantityCommand "
+ + "for customerId: {}, productId: {}, quantity: "
+ + "{}", customerId, productId, newQuantity);
Cart cart = cartService.getCartByCustomerId(customerId);
Optional existingItemOpt = cart.getItems().stream()
@@ -29,18 +31,22 @@ public Cart execute() {
.findFirst();
if (existingItemOpt.isEmpty()) {
- log.error("Product not found in cart for productId: {}", productId);
- throw new GlobalHandlerException(HttpStatus.NOT_FOUND, "Product not found in cart");
+ log.error("Product not found in cart for "
+ + "productId: {}", productId);
+ throw new GlobalHandlerException(
+ HttpStatus.NOT_FOUND, "Product not found in cart");
}
CartItem item = existingItemOpt.get();
previousQuantity = item.getQuantity();
if (newQuantity <= 0) {
- log.debug("Removing item as quantity <= 0 for productId: {}", productId);
+ log.debug("Removing item as quantity <= 0 for"
+ + " productId: {}", productId);
cart.getItems().remove(item);
} else {
- log.debug("Updating quantity to: {} for productId: {}", newQuantity, productId);
+ log.debug("Updating quantity to: {} for "
+ + "productId: {}", newQuantity, productId);
item.setQuantity(newQuantity);
}
@@ -51,7 +57,8 @@ public Cart execute() {
@Override
public Cart undo() {
- log.debug("Undoing UpdateQuantityCommand for customerId: {}, productId: {}", customerId, productId);
+ log.debug("Undoing UpdateQuantityCommand for"
+ + " customerId: {}, productId: {}", customerId, productId);
Cart cart = cartService.getCartByCustomerId(customerId);
if (previousQuantity == null) {
@@ -64,18 +71,22 @@ public Cart undo() {
.findFirst();
if (previousQuantity <= 0) {
- log.debug("Restoring removed item during undo for productId: {}", productId);
+ log.debug("Restoring removed item during "
+ + "undo for productId: {}", productId);
cart.getItems().add(new CartItem(productId, previousQuantity));
} else if (existingItemOpt.isPresent()) {
- log.debug("Restoring previous quantity during undo for productId: {}", productId);
+ log.debug("Restoring previous quantity "
+ + "during undo for productId: {}", productId);
existingItemOpt.get().setQuantity(previousQuantity);
} else {
- log.debug("Adding item back during undo for productId: {}", productId);
+ log.debug("Adding item back during"
+ + " undo for productId: {}", productId);
cart.getItems().add(new CartItem(productId, previousQuantity));
}
Cart updatedCart = cartService.saveCart(cart);
- log.debug("UpdateQuantityCommand undone, updated cart: {}", updatedCart);
+ log.debug("UpdateQuantityCommand undone,"
+ + " updated cart: {}", updatedCart);
return updatedCart;
}
-}
\ No newline at end of file
+}
From efb3af5bc757d60853bcfa920786331f14b068e0 Mon Sep 17 00:00:00 2001
From: Abdelrahman Elmeky <65960126+Aelmeky@users.noreply.github.com>
Date: Tue, 13 May 2025 12:17:46 +0300
Subject: [PATCH 21/24] feat: adding promocode and checkout
---
pom.xml | 5 +
src/main/java/cart/config/RabbitMQConfig.java | 50 ++
.../java/cart/controller/CartController.java | 41 ++
.../cart/controller/PromoCodeController.java | 65 +++
src/main/java/cart/model/Cart.java | 9 +
src/main/java/cart/model/CartItem.java | 13 +
src/main/java/cart/model/OrderRequest.java | 11 +-
src/main/java/cart/model/PromoCode.java | 49 ++
.../cart/repository/PromoCodeRepository.java | 13 +
src/main/java/cart/service/CartService.java | 214 ++++++--
.../java/cart/service/PromoCodeService.java | 52 ++
.../java/cart/service/RemoveItemCommand.java | 6 +-
.../cart/service/UpdateQuantityCommand.java | 4 +-
src/main/resources/application.properties | 12 +-
src/test/java/service/CartServiceTest.java | 478 +++++++++++-------
15 files changed, 801 insertions(+), 221 deletions(-)
create mode 100644 src/main/java/cart/config/RabbitMQConfig.java
create mode 100644 src/main/java/cart/controller/PromoCodeController.java
create mode 100644 src/main/java/cart/model/PromoCode.java
create mode 100644 src/main/java/cart/repository/PromoCodeRepository.java
create mode 100644 src/main/java/cart/service/PromoCodeService.java
diff --git a/pom.xml b/pom.xml
index 337a47c..baaece5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -55,6 +55,11 @@
2.8.5
+
+ org.springframework.boot
+ spring-boot-starter-amqp
+
+
net.logstash.logback
diff --git a/src/main/java/cart/config/RabbitMQConfig.java b/src/main/java/cart/config/RabbitMQConfig.java
new file mode 100644
index 0000000..79b49e1
--- /dev/null
+++ b/src/main/java/cart/config/RabbitMQConfig.java
@@ -0,0 +1,50 @@
+package cart.config;
+
+import org.springframework.amqp.core.Binding;
+import org.springframework.amqp.core.BindingBuilder;
+import org.springframework.amqp.core.Queue;
+import org.springframework.amqp.core.TopicExchange;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class RabbitMQConfig {
+
+ @Value("${rabbitmq.exchange.name}")
+ private String exchangeName;
+
+ @Value("${rabbitmq.routing.key.checkout}")
+ private String checkoutRoutingKey;
+
+ // Define the exchange (e.g., a Topic Exchange)
+ @Bean
+ TopicExchange cartEventsExchange() {
+ return new TopicExchange(exchangeName);
+ }
+
+ // Note: The Cart Service *produces* messages. The *consumer* (Order Service)
+ // would typically define the Queue and the Binding. However, defining the
+ // exchange here is good practice for the producer.
+ // If the Cart service also needed to *consume* events (e.g., order confirmations),
+ // you would define Queues and Bindings here as well.
+
+ /* Example Consumer setup (would be in Order Service):
+ @Value("${rabbitmq.queue.name.order}") // e.g., q.order.checkout
+ private String orderQueueName;
+
+ @Bean
+ Queue orderQueue() {
+ return new Queue(orderQueueName, true); // durable=true
+ }
+
+ @Bean
+ Binding orderBinding(Queue orderQueue, TopicExchange cartEventsExchange) {
+ return BindingBuilder.bind(orderQueue).to(cartEventsExchange).with(checkoutRoutingKey);
+ }
+ */
+
+ // You might also need a MessageConverter bean (e.g., Jackson2JsonMessageConverter)
+ // if you haven't configured one globally, to ensure your CheckoutEvent object
+ // is serialized correctly (usually auto-configured by Spring Boot).
+}
\ No newline at end of file
diff --git a/src/main/java/cart/controller/CartController.java b/src/main/java/cart/controller/CartController.java
index 6260409..16a3a0c 100644
--- a/src/main/java/cart/controller/CartController.java
+++ b/src/main/java/cart/controller/CartController.java
@@ -238,4 +238,45 @@ public ResponseEntity checkoutCart(
+ "communicating with Order Service");
}
}
+
+ @Operation(summary = "Apply a promo code to the cart")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200",
+ description = "Promo code applied successfully"),
+ @ApiResponse(responseCode = "400",
+ description = "Invalid or expired promo code"),
+ @ApiResponse(responseCode = "404",
+ description = "Active cart not found")
+ })
+ @PostMapping("/{customerId}/promo/{promoCode}")
+ public ResponseEntity applyPromoCode(
+ @PathVariable("customerId") final String customerId,
+ @PathVariable("promoCode") final String promoCode) {
+ log.debug("Entering applyPromoCode endpoint with "
+ + "customerId: {}, promoCode: {}",
+ customerId, promoCode);
+ Cart updatedCart = cartService.applyPromoCode(
+ customerId, promoCode);
+ log.debug("Promo code applied, updated cart: "
+ + "{}", updatedCart);
+ return ResponseEntity.ok(updatedCart);
+ }
+
+ @Operation(summary = "Remove the applied promo code from the cart")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200",
+ description = "Promo code removed successfully"),
+ @ApiResponse(responseCode = "404",
+ description = "Active cart not found")
+ })
+ @DeleteMapping("/{customerId}/promo")
+ public ResponseEntity removePromoCode(
+ @PathVariable("customerId") final String customerId) {
+ log.debug("Entering removePromoCode "
+ + "endpoint with customerId: {}", customerId);
+ Cart updatedCart = cartService.removePromoCode(customerId);
+ log.debug("Promo code removed (if any),"
+ + " updated cart: {}", updatedCart);
+ return ResponseEntity.ok(updatedCart);
+ }
}
diff --git a/src/main/java/cart/controller/PromoCodeController.java b/src/main/java/cart/controller/PromoCodeController.java
new file mode 100644
index 0000000..ca51ad8
--- /dev/null
+++ b/src/main/java/cart/controller/PromoCodeController.java
@@ -0,0 +1,65 @@
+package cart.controller;
+
+import cart.model.PromoCode;
+import cart.service.PromoCodeService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/admin/promocodes")
+@RequiredArgsConstructor
+@Tag(name = "PromoCode Admin", description = "Manage promotional codes (Requires Admin Role)")
+@Slf4j
+public class PromoCodeController {
+
+ private final PromoCodeService promoCodeService;
+
+ @Operation(summary = "Create a Promo Code")
+ @PostMapping
+ public ResponseEntity createOrUpdatePromoCode(
+ @Valid @RequestBody final PromoCode promoCode) {
+ log.info("Admin request to create promo code: {}", promoCode.getCode());
+ PromoCode savedPromoCode = promoCodeService.createOrUpdatePromoCode(promoCode);
+ return ResponseEntity.ok(savedPromoCode);
+ }
+
+ @Operation(summary = "Get all Promo Codes")
+ @GetMapping
+ public ResponseEntity> getAllPromoCodes() {
+ log.info("Admin request to get all promo codes");
+ return ResponseEntity.ok(promoCodeService.findAll());
+ }
+
+ @Operation(summary = "Get a specific Promo Code by code")
+ @GetMapping("/{code}")
+ public ResponseEntity getPromoCodeByCode(
+ @PathVariable final String code) {
+ log.info("Admin request to get promo code: {}", code);
+ return promoCodeService.findByCode(code)
+ .map(ResponseEntity::ok)
+ .orElse(ResponseEntity.notFound().build());
+ }
+
+ @Operation(summary = "Delete a Promo Code by code")
+ @DeleteMapping("/{code}")
+ public ResponseEntity deletePromoCode(
+ @PathVariable final String code) {
+ log.info("Admin request to delete promo code: {}", code);
+ try {
+ promoCodeService.deletePromoCode(code);
+ return ResponseEntity.noContent().build();
+ } catch (Exception e) {
+ log.error("Error deleting promo code", code, e.getMessage());
+ return ResponseEntity.status(
+ HttpStatus.INTERNAL_SERVER_ERROR).build();
+ }
+ }
+}
diff --git a/src/main/java/cart/model/Cart.java b/src/main/java/cart/model/Cart.java
index 6d93d92..b073a4f 100644
--- a/src/main/java/cart/model/Cart.java
+++ b/src/main/java/cart/model/Cart.java
@@ -5,9 +5,11 @@
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.Transient;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
+import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@@ -28,5 +30,12 @@ public class Cart {
private boolean archived = false;
+ private String appliedPromoCode;
+
+ private BigDecimal subTotal = BigDecimal.ZERO;
+ private BigDecimal discountAmount = BigDecimal.ZERO;
+ private BigDecimal totalPrice = BigDecimal.ZERO;
+
+
}
diff --git a/src/main/java/cart/model/CartItem.java b/src/main/java/cart/model/CartItem.java
index 1a147c3..ddac146 100644
--- a/src/main/java/cart/model/CartItem.java
+++ b/src/main/java/cart/model/CartItem.java
@@ -1,10 +1,14 @@
package cart.model;
import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.PositiveOrZero;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
+import java.math.BigDecimal;
+
@Data
@NoArgsConstructor
@AllArgsConstructor
@@ -13,6 +17,15 @@ public class CartItem {
@NotBlank
private String productId;
+ @NotNull
+ @PositiveOrZero
private int quantity;
+ @NotNull
+ @PositiveOrZero
+ private BigDecimal unitPrice;
+
+ public BigDecimal getItemTotal() {
+ return unitPrice.multiply(BigDecimal.valueOf(quantity));
+ }
}
diff --git a/src/main/java/cart/model/OrderRequest.java b/src/main/java/cart/model/OrderRequest.java
index 80b8bb2..2823dcf 100644
--- a/src/main/java/cart/model/OrderRequest.java
+++ b/src/main/java/cart/model/OrderRequest.java
@@ -1,21 +1,24 @@
package cart.model;
-import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
+import java.math.BigDecimal;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrderRequest {
-
- @NotBlank
+ private String eventId;
private String customerId;
-
+ private String cartId;
private List items;
+ private BigDecimal subTotal;
+ private BigDecimal discountAmount;
+ private BigDecimal totalPrice;
+ private String appliedPromoCode;
}
diff --git a/src/main/java/cart/model/PromoCode.java b/src/main/java/cart/model/PromoCode.java
new file mode 100644
index 0000000..c69956d
--- /dev/null
+++ b/src/main/java/cart/model/PromoCode.java
@@ -0,0 +1,49 @@
+package cart.model;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.index.Indexed;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+import java.math.BigDecimal;
+import java.time.Instant;
+
+@Document(collection = "promo_codes")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class PromoCode {
+
+ public enum DiscountType {
+ PERCENTAGE,
+ FIXED_AMOUNT
+ }
+
+ @Id
+ private String id;
+
+ @NotBlank
+ @Indexed(unique = true)
+ private String code;
+
+ private String description;
+
+ @NotNull
+ private DiscountType discountType;
+
+ @NotNull
+ @Positive
+ private BigDecimal discountValue;
+
+ private boolean active = true;
+
+ private Instant expiryDate;
+
+ private BigDecimal minimumPurchaseAmount;
+
+}
diff --git a/src/main/java/cart/repository/PromoCodeRepository.java b/src/main/java/cart/repository/PromoCodeRepository.java
new file mode 100644
index 0000000..371285a
--- /dev/null
+++ b/src/main/java/cart/repository/PromoCodeRepository.java
@@ -0,0 +1,13 @@
+package cart.repository;
+
+
+import cart.model.PromoCode;
+import org.springframework.data.mongodb.repository.MongoRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface PromoCodeRepository extends MongoRepository {
+ Optional findByCode(String code);
+}
diff --git a/src/main/java/cart/service/CartService.java b/src/main/java/cart/service/CartService.java
index cd53992..fb9c8e7 100644
--- a/src/main/java/cart/service/CartService.java
+++ b/src/main/java/cart/service/CartService.java
@@ -4,30 +4,43 @@
import cart.model.Cart;
import cart.model.CartItem;
import cart.model.OrderRequest;
+import cart.model.PromoCode;
import cart.repository.CartRepository;
-import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
-import org.springframework.web.client.RestTemplate;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+
+
+import java.math.BigDecimal;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.NoSuchElementException;
+import java.util.Optional;
import java.util.UUID;
@Service
-@RequiredArgsConstructor
@Slf4j
public class CartService {
private final CartRepository cartRepository;
- private final RestTemplate restTemplate;
+ private final RabbitTemplate rabbitTemplate;
+ private final PromoCodeService promoCodeService;
- @Value("${order.service.url}")
- private String orderServiceUrl;
+ @Value("${rabbitmq.exchange.name}")
+ private String exchangeName;
+ @Value("${rabbitmq.routing.key.checkout}")
+ private String checkoutRoutingKey;
+ // Constructor Injection
+ public CartService(CartRepository cartRepository, RabbitTemplate rabbitTemplate, PromoCodeService promoCodeService) {
+ this.cartRepository = cartRepository;
+ this.rabbitTemplate = rabbitTemplate;
+ this.promoCodeService = promoCodeService;
+ }
public Cart createCart(final String customerId) {
log.debug("Entering createCart"
@@ -36,7 +49,7 @@ public Cart createCart(final String customerId) {
.orElseGet(() -> {
Cart newCart = new Cart(UUID.randomUUID()
.toString(), customerId, new
- ArrayList<>(), false);
+ ArrayList<>(), false, null, BigDecimal.ZERO, BigDecimal.ZERO,BigDecimal.ZERO);
log.debug("Cart created:", newCart);
return cartRepository.save(newCart);
});
@@ -125,30 +138,6 @@ public Cart unarchiveCart(final String customerId) {
return activeCart;
}
- public Cart checkoutCart(final String customerId) {
- log.debug("Entering checkoutCart with customerId:", customerId);
- Cart cart = getActiveCart(customerId);
-
- OrderRequest orderRequest = new OrderRequest();
- orderRequest.setCustomerId(customerId);
- orderRequest.setItems(cart.getItems());
-
- try {
- log.debug("Sending order request to"
- + " Order Service for customerId:", customerId);
- restTemplate.postForObject(orderServiceUrl
- + "/orders", orderRequest, Void.class);
- cart.getItems().clear();
- Cart updatedCart = cartRepository.save(cart);
- log.debug("Cart checked out and cleared:", updatedCart);
- return updatedCart;
- } catch (Exception e) {
- log.error("Failed to checkout cart for customerId:", customerId, e);
- throw new RuntimeException("Error"
- + " communicating with Order Service", e);
- }
- }
-
private Cart getActiveCart(final String customerId) {
log.debug("Entering getActiveCart with customerId:", customerId);
@@ -177,8 +166,171 @@ private Cart getArchivedCart(final String customerId) {
return cart;
}
+// public Cart saveCart(final Cart cart) {
+// return cartRepository.save(cart);
+// }
+
+
+ public Cart applyPromoCode(final String customerId, final String promoCodeInput) {
+ log.debug("Entering applyPromoCode for customerId: {},"
+ + " promoCode: {}", customerId, promoCodeInput);
+ Cart cart = getActiveCart(customerId);
+ String promoCodeUpper = promoCodeInput.toUpperCase();
+
+ PromoCode promoCode = promoCodeService.getActivePromoCode(promoCodeUpper)
+ .orElseThrow(() -> new GlobalHandlerException(
+ HttpStatus.BAD_REQUEST, "Invalid, inactive, or expired promo code: "
+ + promoCodeInput));
+
+ log.info("Applying valid promo code '{}' to "
+ + "cartId: {}", promoCodeUpper, cart.getId());
+ cart.setAppliedPromoCode(promoCodeUpper);
+
+ return saveCart(cart);
+ }
+
+
+ public Cart removePromoCode(final String customerId) {
+ log.debug("Entering removePromoCode for customerId:"
+ + " {}", customerId);
+ Cart cart = getActiveCart(customerId);
+
+ if (cart.getAppliedPromoCode() != null) {
+ log.info("Removing applied promo code '{}' from cartId: {}",
+ cart.getAppliedPromoCode(), cart.getId());
+ cart.setAppliedPromoCode(null);
+ return saveCart(cart);
+ } else {
+ log.debug("No promo code to remove from cartId:"
+ + " {}", cart.getId());
+ return cart;
+ }
+ }
+
+
public Cart saveCart(final Cart cart) {
+ log.debug("Preparing to save cartId: {}", cart.getId());
+ recalculateCartTotals(cart);
+ log.debug("Saving cart with updated totals: {}", cart);
return cartRepository.save(cart);
}
+
+ private void recalculateCartTotals(Cart cart) {
+ log.debug("Recalculating totals for cartId: {}", cart.getId());
+
+
+ BigDecimal subTotal = calculateSubTotal(cart);
+ String formattedSubTotal = String.format("%.2f", subTotal);
+ cart.setSubTotal(new BigDecimal(formattedSubTotal));
+
+
+ BigDecimal discountAmount = BigDecimal.ZERO;
+ if (cart.getAppliedPromoCode() != null) {
+ Optional promoOpt =
+ promoCodeService.getActivePromoCode(
+ cart.getAppliedPromoCode());
+
+ if (promoOpt.isPresent()) {
+ PromoCode promo = promoOpt.get();
+ boolean validForCart = true;
+
+ if (promo.getExpiryDate() != null && promo
+ .getExpiryDate().isBefore(Instant.now())) {
+ log.warn("Applied promo code {} is expired."
+ + " Removing.", cart.getAppliedPromoCode());
+ validForCart = false;
+ }
+
+ if (validForCart) {
+ if (promo.getDiscountType() ==
+ PromoCode.DiscountType.PERCENTAGE) {
+ BigDecimal percentageValue = promo.getDiscountValue()
+ .divide(new BigDecimal("100"));
+ String formattedPercentage = String
+ .format("%.2f", percentageValue);
+ BigDecimal percentage = new BigDecimal(formattedPercentage); discountAmount = subTotal.multiply(percentage);
+ } else if (promo.getDiscountType() ==
+ PromoCode.DiscountType.FIXED_AMOUNT) {
+ discountAmount = promo.getDiscountValue();
+ }
+ }
+
+ } else {
+ log.warn("Applied promo code {} is no longer valid."
+ + " Removing.", cart.getAppliedPromoCode());
+ cart.setAppliedPromoCode(null);
+ }
+ }
+
+ discountAmount = discountAmount.max(BigDecimal.ZERO);
+ discountAmount = discountAmount.min(subTotal);
+ String formattedDiscountAmount = String.format("%.2f", discountAmount);
+ cart.setDiscountAmount(new BigDecimal(formattedDiscountAmount));
+
+ BigDecimal totalPrice = subTotal.subtract(discountAmount);
+ String formattedTotalPrice = String.format("%.2f", totalPrice);
+ cart.setTotalPrice(new BigDecimal(formattedTotalPrice));
+
+ log.debug("Recalculated totals for cartId: {}:"
+ + " SubTotal={}, Discount={}, Total={}",
+ cart.getId(), cart.getSubTotal(),
+ cart.getDiscountAmount(), cart.getTotalPrice());
+ }
+
+ private BigDecimal calculateSubTotal(Cart cart) {
+ if (cart.getItems() == null) {
+ return BigDecimal.ZERO;
+ }
+ return cart.getItems().stream()
+ .filter(item -> item.getUnitPrice() != null
+ && item.getQuantity() > 0)
+ .map(CartItem::getItemTotal)
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+ }
+
+
+ public Cart checkoutCart(final String customerId) {
+ log.debug("Entering checkoutCart [RabbitMQ] for customerId: {}", customerId);
+ Cart cart = getActiveCart(customerId);
+
+ recalculateCartTotals(cart);
+
+ if (cart.getItems().isEmpty()) {
+ log.warn("Attempted checkout for customerId: "
+ + "{} with an empty cart.", customerId);
+ throw new GlobalHandlerException(HttpStatus.BAD_REQUEST,
+ "Cannot checkout an empty cart.");
+ }
+
+ // Create the event payload using the potentially recalculated fields from the cart
+ OrderRequest checkoutEvent = new OrderRequest(
+ UUID.randomUUID().toString(),
+ customerId,
+ cart.getId(),
+ new ArrayList<>(cart.getItems()),
+ cart.getSubTotal(),
+ cart.getDiscountAmount(),
+ cart.getTotalPrice(),
+ cart.getAppliedPromoCode()
+ );
+
+ try {
+ log.debug("Publishing checkout event for cartId: {} with totals: Sub={}, Discount={}, Total={}",
+ cart.getId(), cart.getSubTotal(), cart.getDiscountAmount(), cart.getTotalPrice());
+ rabbitTemplate.convertAndSend(exchangeName, checkoutRoutingKey, checkoutEvent);
+
+ log.info("Checkout event published successfully for cartId: {}. Clearing cart.", cart.getId());
+ cart.getItems().clear();
+ cart.setAppliedPromoCode(null);
+ Cart updatedCart = saveCart(cart);
+
+ log.debug("Cart cleared and saved after checkout: {}", updatedCart);
+ return updatedCart;
+
+ } catch (Exception e) {
+ log.error("Failed to publish checkout event for cartId: {}. Error: {}", cart.getId(), e.getMessage(), e);
+ throw new RuntimeException("Checkout process failed: Could not publish event.", e);
+ }
+ }
}
diff --git a/src/main/java/cart/service/PromoCodeService.java b/src/main/java/cart/service/PromoCodeService.java
new file mode 100644
index 0000000..c3767af
--- /dev/null
+++ b/src/main/java/cart/service/PromoCodeService.java
@@ -0,0 +1,52 @@
+package cart.service;
+
+import cart.exception.GlobalHandlerException;
+import cart.model.PromoCode;
+import cart.repository.PromoCodeRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class PromoCodeService {
+
+ private final PromoCodeRepository promoCodeRepository;
+
+ public PromoCode createOrUpdatePromoCode(final PromoCode promoCode) {
+ log.info("Creating/Updating promo code: {}", promoCode.getCode());
+ promoCode.setCode(promoCode.getCode().toUpperCase());
+ Optional existing = promoCodeRepository.findByCode(promoCode.getCode());
+ existing.ifPresent(value -> promoCode.setId(value.getId()));
+
+ return promoCodeRepository.save(promoCode);
+ }
+
+ public Optional findByCode(final String code) {
+ return promoCodeRepository.findByCode(code.toUpperCase());
+ }
+
+ public List findAll() {
+ return promoCodeRepository.findAll();
+ }
+
+ public void deletePromoCode(final String code) {
+ PromoCode promo = promoCodeRepository.findByCode(
+ code.toUpperCase())
+ .orElseThrow(() -> new
+ GlobalHandlerException(HttpStatus.NOT_FOUND,
+ "Promo code not found: " + code));
+ promoCodeRepository.delete(promo);
+ log.info("Deleted promo code: {}", code);
+ }
+
+ public Optional getActivePromoCode(final String code) {
+ return promoCodeRepository.findByCode(code.toUpperCase())
+ .filter(PromoCode::isActive);
+ }
+}
diff --git a/src/main/java/cart/service/RemoveItemCommand.java b/src/main/java/cart/service/RemoveItemCommand.java
index 1486f4a..1a7bc00 100644
--- a/src/main/java/cart/service/RemoveItemCommand.java
+++ b/src/main/java/cart/service/RemoveItemCommand.java
@@ -18,7 +18,8 @@ public class RemoveItemCommand implements CartCommand {
@Override
public Cart execute() {
- log.debug("Executing RemoveItemCommand for customerId: {}, productId: {}", customerId, productId);
+ log.debug("Executing RemoveItemCommand for customerId: "
+ + "{}, productId: {}", customerId, productId);
Cart cart = cartService.getCartByCustomerId(customerId);
Optional itemToRemove = cart.getItems().stream()
@@ -26,7 +27,8 @@ public Cart execute() {
.findFirst();
if (itemToRemove.isPresent()) {
- removedItem = new CartItem(itemToRemove.get().getProductId(), itemToRemove.get().getQuantity());
+ removedItem = new CartItem(itemToRemove.get().getProductId(),
+ itemToRemove.get().getQuantity(), itemToRemove.get().getUnitPrice());
cart.getItems().removeIf(i -> i.getProductId().equals(productId));
log.debug("Item removed for productId: {}", productId);
} else {
diff --git a/src/main/java/cart/service/UpdateQuantityCommand.java b/src/main/java/cart/service/UpdateQuantityCommand.java
index 0b3f4b7..5ea737d 100644
--- a/src/main/java/cart/service/UpdateQuantityCommand.java
+++ b/src/main/java/cart/service/UpdateQuantityCommand.java
@@ -73,7 +73,7 @@ public Cart undo() {
if (previousQuantity <= 0) {
log.debug("Restoring removed item during "
+ "undo for productId: {}", productId);
- cart.getItems().add(new CartItem(productId, previousQuantity));
+ cart.getItems().add(new CartItem(productId, previousQuantity, existingItemOpt.get().getUnitPrice()));
} else if (existingItemOpt.isPresent()) {
log.debug("Restoring previous quantity "
+ "during undo for productId: {}", productId);
@@ -81,7 +81,7 @@ public Cart undo() {
} else {
log.debug("Adding item back during"
+ " undo for productId: {}", productId);
- cart.getItems().add(new CartItem(productId, previousQuantity));
+ cart.getItems().add(new CartItem(productId, previousQuantity, existingItemOpt.get().getUnitPrice()));
}
Cart updatedCart = cartService.saveCart(cart);
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index f14db7a..e66c6cb 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -2,4 +2,14 @@ order.service.url=http://order-service:8082
spring.data.mongodb.uri=mongodb://localhost:27018/cartDB
logging.file.name=./logs/app.log
logging.level.root=info
-logging.level.com.podzilla.cart=debug
\ No newline at end of file
+logging.level.com.podzilla.cart=debug
+# RabbitMQ Configuration
+spring.rabbitmq.host=localhost
+spring.rabbitmq.port=5672
+spring.rabbitmq.username=guest # Use appropriate credentials
+spring.rabbitmq.password=guest # Use appropriate credentials
+# spring.rabbitmq.virtual-host=/ # Optional
+
+# Custom properties for exchange/routing keys
+rabbitmq.exchange.name=cart.events
+rabbitmq.routing.key.checkout=order.checkout.initiate
\ No newline at end of file
diff --git a/src/test/java/service/CartServiceTest.java b/src/test/java/service/CartServiceTest.java
index d7670d3..97cd60c 100644
--- a/src/test/java/service/CartServiceTest.java
+++ b/src/test/java/service/CartServiceTest.java
@@ -1,20 +1,29 @@
package service;
+
import cart.exception.GlobalHandlerException;
import cart.model.Cart;
import cart.model.CartItem;
import cart.model.OrderRequest;
+import cart.model.PromoCode;
import cart.repository.CartRepository;
+
import cart.service.CartService;
+import cart.service.PromoCodeService;
+
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
+import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.test.util.ReflectionTestUtils;
-import org.springframework.web.client.RestTemplate;
+import java.math.BigDecimal;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.NoSuchElementException;
import java.util.Optional;
@@ -22,6 +31,7 @@
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
@@ -32,303 +42,409 @@ class CartServiceTest {
private CartRepository cartRepository;
@Mock
- private RestTemplate restTemplate;
+ private RabbitTemplate rabbitTemplate;
+ @Mock
+ private PromoCodeService promoCodeService;
+
+ @Spy
@InjectMocks
private CartService cartService;
private Cart cart;
- private CartItem cartItem;
+ private CartItem item1Input; // Item as input to service methods
+ private CartItem item2Input;
+
private final String customerId = "cust123";
- private final String productId = "prod456";
+ private final String productId1 = "prod1";
+ private final String productId2 = "prod2";
+ private final BigDecimal price1 = new BigDecimal("10.50");
+ private final BigDecimal price2 = new BigDecimal("5.00");
private final String cartId = UUID.randomUUID().toString();
- private final String orderServiceUrl = "http://localhost:8080";
+
+ private final String exchangeName = "test.cart.events";
+ private final String checkoutRoutingKey = "test.order.checkout.initiate";
+
+ private Cart createTestCartInstance() {
+ return new Cart(cartId, customerId, new ArrayList<>(), false, null,
+ BigDecimal.ZERO.setScale(2), BigDecimal.ZERO.setScale(2), BigDecimal.ZERO.setScale(2));
+ }
+
+ private PromoCode createTestPromoCode(String code, PromoCode.DiscountType type, BigDecimal value, BigDecimal minPurchase, Instant expiryDate, boolean isActive) {
+ PromoCode promo = new PromoCode();
+ promo.setCode(code.toUpperCase());
+ promo.setActive(isActive);
+ promo.setDiscountType(type);
+ promo.setDiscountValue(value);
+ promo.setMinimumPurchaseAmount(minPurchase);
+ promo.setExpiryDate(expiryDate);
+ return promo;
+ }
@BeforeEach
void setUp() {
- // Initialize test data
- cart = new Cart(cartId, customerId, new ArrayList<>(), false);
- cartItem = new CartItem(productId, 1);
+ cart = createTestCartInstance();
+ item1Input = new CartItem(productId1, 1, price1);
+ item2Input = new CartItem(productId2, 2, price2);
+
+ ReflectionTestUtils.setField(cartService, "exchangeName", exchangeName);
+ ReflectionTestUtils.setField(cartService, "checkoutRoutingKey", checkoutRoutingKey);
+
+
+ lenient().when(cartRepository.save(any(Cart.class))).thenAnswer(invocation -> {
+ Cart cartToSave = invocation.getArgument(0);
+
+ return cartToSave;
+ });
+
+
+
+ lenient().when(cartService.getCartByCustomerId(customerId)).thenReturn(cart);
+ lenient().when(cartRepository.findByCustomerIdAndArchived(customerId, false)).thenReturn(Optional.of(cart));
+ lenient().when(cartRepository.findByCustomerIdAndArchived(customerId, true))
+ .thenReturn(Optional.of(new Cart(cartId, customerId, new ArrayList<>(), true, null, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO)));
- // Set orderServiceUrl
- ReflectionTestUtils.setField(cartService, "orderServiceUrl", orderServiceUrl);
}
@Test
- void createCart_existingCart_returnsCart() {
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
+ void createCart_existingCart_returnsCartAndDoesNotSave() {
+ Cart existingCart = createTestCartInstance();
+ when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(existingCart));
Cart result = cartService.createCart(customerId);
- assertEquals(cart, result);
+ assertEquals(existingCart, result);
verify(cartRepository).findByCustomerId(customerId);
verify(cartRepository, never()).save(any());
}
@Test
- void createCart_noExistingCart_createsAndSavesCart() {
+ void createCart_noExistingCart_createsAndSavesNewCartWithZeroTotals() {
when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.empty());
- when(cartRepository.save(any(Cart.class))).thenAnswer(invocation -> invocation.getArgument(0));
+ when(cartRepository.save(any(Cart.class))).thenAnswer(invocation -> {
+ Cart newCart = invocation.getArgument(0);
+ newCart.setId(cartId);
+ return newCart;
+ });
+
Cart result = cartService.createCart(customerId);
assertEquals(customerId, result.getCustomerId());
+ assertNotNull(result.getId());
assertFalse(result.isArchived());
assertTrue(result.getItems().isEmpty());
+ assertEquals(BigDecimal.ZERO.setScale(2), result.getSubTotal());
+ assertEquals(BigDecimal.ZERO.setScale(2), result.getDiscountAmount());
+ assertEquals(BigDecimal.ZERO.setScale(2), result.getTotalPrice());
+ assertNull(result.getAppliedPromoCode());
verify(cartRepository).findByCustomerId(customerId);
verify(cartRepository).save(any(Cart.class));
}
- @Test
- void addItemToCart_newItem_addsItem() {
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
- when(cartRepository.save(any(Cart.class))).thenReturn(cart);
- Cart result = cartService.addItemToCart(customerId, cartItem);
+ @Test
+ void addItemToCart_newItem_addsItemAndRecalculatesTotals() {
+ // Act
+ Cart result = cartService.addItemToCart(customerId, item1Input); // 1 x 10.50
+ // Assert
assertEquals(1, result.getItems().size());
- assertEquals(cartItem.getProductId(), result.getItems().get(0).getProductId());
- assertEquals(cartItem.getQuantity(), result.getItems().get(0).getQuantity());
- verify(cartRepository).findByCustomerId(customerId);
- verify(cartRepository).save(cart);
+ assertTrue(result.getItems().stream().anyMatch(i -> i.getProductId().equals(productId1) && i.getQuantity() == 1));
+ assertEquals(new BigDecimal("10.50").setScale(2), result.getSubTotal());
+ assertEquals(BigDecimal.ZERO.setScale(2), result.getDiscountAmount());
+ assertEquals(new BigDecimal("10.50").setScale(2), result.getTotalPrice());
+ verify(cartRepository, atLeastOnce()).save(any(Cart.class)); // saveCart is called by command
}
@Test
- void addItemToCart_existingItem_updatesQuantity() {
- cart.getItems().add(new CartItem(productId, 1));
- CartItem newItem = new CartItem(productId, 2);
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
- when(cartRepository.save(any(Cart.class))).thenReturn(cart);
+ void addItemToCart_existingItem_updatesQuantityAndRecalculatesTotals() {
+ // Initial state: cart has item1
+ cart.getItems().add(new CartItem(productId1, 1, price1));
+ cartService.saveCart(cart); // Save to set initial totals
- Cart result = cartService.addItemToCart(customerId, newItem);
+ // Act: Add more of item1
+ CartItem additionalItem1 = new CartItem(productId1, 2, price1);
+ Cart result = cartService.addItemToCart(customerId, additionalItem1); // Adds 2, total qty = 3
+ // Assert
assertEquals(1, result.getItems().size());
- assertEquals(3, result.getItems().get(0).getQuantity());
- verify(cartRepository).findByCustomerId(customerId);
- verify(cartRepository).save(cart);
+ CartItem updatedItem = result.getItems().get(0);
+ assertEquals(productId1, updatedItem.getProductId());
+ assertEquals(3, updatedItem.getQuantity());
+ assertEquals(new BigDecimal("31.50").setScale(2), result.getSubTotal()); // 10.50 * 3
+ assertEquals(BigDecimal.ZERO.setScale(2), result.getDiscountAmount());
+ assertEquals(new BigDecimal("31.50").setScale(2), result.getTotalPrice());
+ verify(cartRepository, atLeastOnce()).save(any(Cart.class));
}
- @Test
- void addItemToCart_cartNotFound_throwsGlobalHandlerException() {
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.empty());
-
- GlobalHandlerException exception = assertThrows(GlobalHandlerException.class,
- () -> cartService.addItemToCart(customerId, cartItem));
- assertEquals(HttpStatus.NOT_FOUND, exception.getStatus());
- assertEquals("Cart not found", exception.getMessage());
- verify(cartRepository).findByCustomerId(customerId);
- verify(cartRepository, never()).save(any());
- }
@Test
- void updateItemQuantity_existingItem_updatesQuantity() {
- cart.getItems().add(new CartItem(productId, 1));
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
- when(cartRepository.save(any(Cart.class))).thenReturn(cart);
+ void updateItemQuantity_existingItem_updatesAndRecalculates() {
+ cart.getItems().add(new CartItem(productId1, 2, price1)); // 2 * 10.50 = 21.00
+ cartService.saveCart(cart); // Set initial totals
- Cart result = cartService.updateItemQuantity(customerId, productId, 5);
+ // Act
+ Cart result = cartService.updateItemQuantity(customerId, productId1, 5); // Update to 5 * 10.50 = 52.50
+ // Assert
+ assertEquals(1, result.getItems().size());
assertEquals(5, result.getItems().get(0).getQuantity());
- verify(cartRepository).findByCustomerId(customerId);
- verify(cartRepository).save(cart);
+ assertEquals(new BigDecimal("52.50").setScale(2), result.getSubTotal());
+ assertEquals(new BigDecimal("52.50").setScale(2), result.getTotalPrice());
+ verify(cartRepository, atLeastOnce()).save(any(Cart.class));
}
@Test
- void updateItemQuantity_quantityZero_removesItem() {
- cart.getItems().add(new CartItem(productId, 1));
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
- when(cartRepository.save(any(Cart.class))).thenReturn(cart);
+ void updateItemQuantity_quantityToZero_removesItemAndRecalculates() {
+ cart.getItems().add(new CartItem(productId1, 2, price1));
+ cart.getItems().add(new CartItem(productId2, 1, price2)); // p1: 21.00, p2: 5.00. Sub: 26.00
+ cartService.saveCart(cart);
- Cart result = cartService.updateItemQuantity(customerId, productId, 0);
+ // Act: Remove productId1
+ Cart result = cartService.updateItemQuantity(customerId, productId1, 0);
- assertTrue(result.getItems().isEmpty());
- verify(cartRepository).findByCustomerId(customerId);
- verify(cartRepository).save(cart);
+ // Assert
+ assertEquals(1, result.getItems().size());
+ assertEquals(productId2, result.getItems().get(0).getProductId());
+ assertEquals(new BigDecimal("5.00").setScale(2), result.getSubTotal());
+ assertEquals(new BigDecimal("5.00").setScale(2), result.getTotalPrice());
+ verify(cartRepository, atLeastOnce()).save(any(Cart.class));
}
@Test
- void updateItemQuantity_itemNotFound_throwsGlobalHandlerException() {
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
+ void removeItemFromCart_itemExists_removesAndRecalculates() {
+ cart.getItems().add(new CartItem(productId1, 2, price1));
+ cart.getItems().add(new CartItem(productId2, 1, price2));
+ cartService.saveCart(cart);
- GlobalHandlerException exception = assertThrows(GlobalHandlerException.class,
- () -> cartService.updateItemQuantity(customerId, productId, 5));
- assertEquals(HttpStatus.NOT_FOUND, exception.getStatus());
- assertEquals("Product not found in cart", exception.getMessage());
- verify(cartRepository).findByCustomerId(customerId);
- verify(cartRepository, never()).save(any());
+ // Act
+ Cart result = cartService.removeItemFromCart(customerId, productId1);
+
+ // Assert
+ assertEquals(1, result.getItems().size());
+ assertEquals(productId2, result.getItems().get(0).getProductId());
+ assertEquals(new BigDecimal("5.00").setScale(2), result.getSubTotal());
+ assertEquals(new BigDecimal("5.00").setScale(2), result.getTotalPrice());
+ verify(cartRepository, atLeastOnce()).save(any(Cart.class));
}
@Test
- void removeItemFromCart_itemExists_removesItem() {
- cart.getItems().add(new CartItem(productId, 1));
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
- when(cartRepository.save(any(Cart.class))).thenReturn(cart);
+ void clearCart_itemsExist_clearsItemsAndResetsTotals() {
+ cart.getItems().add(new CartItem(productId1, 1, price1));
+ cart.setAppliedPromoCode("TESTCODE");
+ cartService.saveCart(cart); // Initial save to set totals
- Cart result = cartService.removeItemFromCart(customerId, productId);
+ // Act
+ cartService.clearCart(customerId); // This modifies 'cart' in place due to getCartByCustomerId
- assertTrue(result.getItems().isEmpty());
- verify(cartRepository).findByCustomerId(customerId);
- verify(cartRepository).save(cart);
+ // Assert
+ // The cart object itself should be modified. saveCart is called inside clearCart.
+ assertTrue(cart.getItems().isEmpty());
+ assertNull(cart.getAppliedPromoCode());
+ assertEquals(BigDecimal.ZERO.setScale(2), cart.getSubTotal());
+ assertEquals(BigDecimal.ZERO.setScale(2), cart.getDiscountAmount());
+ assertEquals(BigDecimal.ZERO.setScale(2), cart.getTotalPrice());
+ verify(cartRepository, times(2)).save(cart); // Initial save + save in clearCart
}
+ // --- Promo Code Tests ---
@Test
- void removeItemFromCart_cartNotFound_throwsGlobalHandlerException() {
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.empty());
-
- GlobalHandlerException exception = assertThrows(GlobalHandlerException.class,
- () -> cartService.removeItemFromCart(customerId, productId));
- assertEquals(HttpStatus.NOT_FOUND, exception.getStatus());
- assertEquals("Cart not found", exception.getMessage());
- verify(cartRepository).findByCustomerId(customerId);
- verify(cartRepository, never()).save(any());
+ void applyPromoCode_validPercentageCode_calculatesDiscount() {
+ cart.getItems().add(new CartItem(productId1, 2, new BigDecimal("10.00"))); // SubTotal = 20.00
+ cartService.saveCart(cart); // Initial calculation
+
+ String promoCodeStr = "SAVE10";
+ PromoCode promo = createTestPromoCode(promoCodeStr, PromoCode.DiscountType.PERCENTAGE, new BigDecimal("10"), null, null, true);
+ when(promoCodeService.getActivePromoCode(promoCodeStr)).thenReturn(Optional.of(promo));
+
+ // Act
+ Cart result = cartService.applyPromoCode(customerId, promoCodeStr);
+
+ // Assert
+ assertEquals(promoCodeStr, result.getAppliedPromoCode());
+ assertEquals(new BigDecimal("20.00").setScale(2), result.getSubTotal());
+ assertEquals(new BigDecimal("2.00").setScale(2), result.getDiscountAmount()); // 10% of 20.00
+ assertEquals(new BigDecimal("18.00").setScale(2), result.getTotalPrice());
+ verify(cartRepository, atLeastOnce()).save(any(Cart.class));
}
@Test
- void deleteCartByCustomerId_cartExists_deletesCart() {
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
-
- cartService.deleteCartByCustomerId(customerId);
+ void applyPromoCode_validFixedCode_calculatesDiscount() {
+ cart.getItems().add(new CartItem(productId1, 3, new BigDecimal("10.00"))); // SubTotal = 30.00
+ cartService.saveCart(cart);
+
+ String promoCodeStr = "5OFF";
+ PromoCode promo = createTestPromoCode(promoCodeStr, PromoCode.DiscountType.FIXED_AMOUNT, new BigDecimal("5.00"), null, null, true);
+ when(promoCodeService.getActivePromoCode(promoCodeStr)).thenReturn(Optional.of(promo));
+
+ // Act
+ Cart result = cartService.applyPromoCode(customerId, promoCodeStr);
+
+ // Assert
+ assertEquals(promoCodeStr, result.getAppliedPromoCode());
+ assertEquals(new BigDecimal("30.00").setScale(2), result.getSubTotal());
+ assertEquals(new BigDecimal("5.00").setScale(2), result.getDiscountAmount());
+ assertEquals(new BigDecimal("25.00").setScale(2), result.getTotalPrice());
+ }
- verify(cartRepository).findByCustomerId(customerId);
- verify(cartRepository).delete(cart);
+ @Test
+ void applyPromoCode_invalidCode_throwsException() {
+ String invalidCode = "FAKECODE";
+ when(promoCodeService.getActivePromoCode(invalidCode.toUpperCase())).thenReturn(Optional.empty());
+
+ GlobalHandlerException ex = assertThrows(GlobalHandlerException.class,
+ () -> cartService.applyPromoCode(customerId, invalidCode));
+ assertEquals(HttpStatus.BAD_REQUEST, ex.getStatus());
+ assertTrue(ex.getMessage().contains("Invalid, inactive, or expired promo code"));
+ assertNull(cart.getAppliedPromoCode()); // Should not be set
}
@Test
- void deleteCartByCustomerId_cartNotFound_doesNothing() {
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.empty());
+ void applyPromoCode_expiredCode_noDiscountCodeRemoved() {
+ cart.getItems().add(new CartItem(productId1, 1, new BigDecimal("100.00")));
+ cartService.saveCart(cart);
+
+ String promoCodeStr = "EXPIRED";
+ PromoCode promo = createTestPromoCode(promoCodeStr, PromoCode.DiscountType.PERCENTAGE, new BigDecimal("10"), null, Instant.now().minusSeconds(3600), true); // Expired 1 hour ago
+ when(promoCodeService.getActivePromoCode(promoCodeStr)).thenReturn(Optional.of(promo)); // Mock this to return it, recalc logic should handle expiry
+
+ // Act
+ Cart result = cartService.applyPromoCode(customerId, promoCodeStr); // applies then save calls recalculate
+
+ // Assert
+ // RecalculateTotals should detect expiry based on the logic added in CartService.
+ // If it removes the code:
+ assertNull(result.getAppliedPromoCode());
+ assertEquals(new BigDecimal("100.00").setScale(2), result.getSubTotal());
+ assertEquals(BigDecimal.ZERO.setScale(2), result.getDiscountAmount());
+ assertEquals(new BigDecimal("100.00").setScale(2), result.getTotalPrice());
+ }
- cartService.deleteCartByCustomerId(customerId);
- verify(cartRepository).findByCustomerId(customerId);
- verify(cartRepository, never()).delete(any());
+ @Test
+ void removePromoCode_codeExists_removesCodeAndRecalculates() {
+ cart.getItems().add(new CartItem(productId1, 2, new BigDecimal("10.00"))); // SubTotal = 20.00
+ cart.setAppliedPromoCode("SAVE10");
+ // Manually set initial discount for testing removal logic
+ cart.setSubTotal(new BigDecimal("20.00"));
+ cart.setDiscountAmount(new BigDecimal("2.00"));
+ cart.setTotalPrice(new BigDecimal("18.00"));
+ // No need to call cartService.saveCart(cart); as we are testing removePromoCode directly after setting this state.
+
+ // Act
+ Cart result = cartService.removePromoCode(customerId);
+
+ // Assert
+ assertNull(result.getAppliedPromoCode());
+ assertEquals(new BigDecimal("20.00").setScale(2), result.getSubTotal());
+ assertEquals(BigDecimal.ZERO.setScale(2), result.getDiscountAmount());
+ assertEquals(new BigDecimal("20.00").setScale(2), result.getTotalPrice());
}
+ // --- Checkout Tests ---
@Test
- void getCartByCustomerId_cartExists_returnsCart() {
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
+ void checkoutCart_validCartWithPromo_publishesEventWithCorrectTotalsAndClearsCart() {
+ cart.getItems().add(new CartItem(productId1, 1, new BigDecimal("100.00")));
+ cart.setAppliedPromoCode("SAVE10");
+ // Simulate totals as if SAVE10 (10%) was applied correctly before checkout
+ cart.setSubTotal(new BigDecimal("100.00"));
+ cart.setDiscountAmount(new BigDecimal("10.00"));
+ cart.setTotalPrice(new BigDecimal("90.00"));
- Cart result = cartService.getCartByCustomerId(customerId);
+ // Mock promoCodeService for the recalculateCartTotals call within checkoutCart
+ PromoCode promo = createTestPromoCode("SAVE10", PromoCode.DiscountType.PERCENTAGE, new BigDecimal("10"), null, null, true);
+ when(promoCodeService.getActivePromoCode("SAVE10")).thenReturn(Optional.of(promo));
+ doNothing().when(rabbitTemplate).convertAndSend(anyString(), anyString(), any(OrderRequest.class));
- assertEquals(cart, result);
- verify(cartRepository).findByCustomerId(customerId);
+
+ // Act
+ Cart result = cartService.checkoutCart(customerId);
+
+ // Assert Published Event
+ ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(OrderRequest.class);
+ verify(rabbitTemplate).convertAndSend(eq(exchangeName), eq(checkoutRoutingKey), eventCaptor.capture());
+ OrderRequest publishedEvent = eventCaptor.getValue();
+
+ assertEquals(customerId, publishedEvent.getCustomerId());
+ assertEquals(cartId, publishedEvent.getCartId());
+ assertEquals(1, publishedEvent.getItems().size());
+ assertEquals(new BigDecimal("100.00").setScale(2), publishedEvent.getSubTotal());
+ assertEquals(new BigDecimal("10.00").setScale(2), publishedEvent.getDiscountAmount());
+ assertEquals(new BigDecimal("90.00").setScale(2), publishedEvent.getTotalPrice());
+ assertEquals("SAVE10", publishedEvent.getAppliedPromoCode());
+
+ // Assert Cart State after Checkout (returned by method)
+ assertTrue(result.getItems().isEmpty());
+ assertNull(result.getAppliedPromoCode());
+ assertEquals(BigDecimal.ZERO.setScale(2), result.getSubTotal());
+ assertEquals(BigDecimal.ZERO.setScale(2), result.getDiscountAmount());
+ assertEquals(BigDecimal.ZERO.setScale(2), result.getTotalPrice());
+
+ verify(cartRepository, times(1)).save(any(Cart.class)); // Save for clearing
}
@Test
- void getCartByCustomerId_cartNotFound_throwsGlobalHandlerException() {
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.empty());
-
- GlobalHandlerException exception = assertThrows(GlobalHandlerException.class,
- () -> cartService.getCartByCustomerId(customerId));
- assertEquals(HttpStatus.NOT_FOUND, exception.getStatus());
- assertEquals("Cart not found", exception.getMessage());
- verify(cartRepository).findByCustomerId(customerId);
+ void checkoutCart_emptyCart_throwsException() {
+ // cart is empty by default in setup for this test
+ GlobalHandlerException ex = assertThrows(GlobalHandlerException.class,
+ () -> cartService.checkoutCart(customerId));
+ assertEquals(HttpStatus.BAD_REQUEST, ex.getStatus());
+ assertEquals("Cannot checkout an empty cart.", ex.getMessage());
+ verify(rabbitTemplate, never()).convertAndSend(eq(exchangeName), eq(checkoutRoutingKey), any(OrderRequest.class));
}
@Test
- void clearCart_cartExists_clearsItems() {
- cart.getItems().add(new CartItem(productId, 1));
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
- when(cartRepository.save(any(Cart.class))).thenReturn(cart);
+ void checkoutCart_rabbitMqFails_throwsRuntimeExceptionAndCartNotCleared() {
+ cart.getItems().add(item1Input);
+ cartService.saveCart(cart); // Initial state
- cartService.clearCart(customerId);
+ doThrow(new RuntimeException("RabbitMQ publish error")).when(rabbitTemplate)
+ .convertAndSend(anyString(), anyString(), any(OrderRequest.class));
- assertTrue(cart.getItems().isEmpty());
- verify(cartRepository).findByCustomerId(customerId);
- verify(cartRepository).save(cart);
+ RuntimeException ex = assertThrows(RuntimeException.class,
+ () -> cartService.checkoutCart(customerId));
+ assertTrue(ex.getMessage().contains("Checkout process failed: Could not publish event."));
+
+ // Verify cart state is NOT cleared
+ Cart cartAfterFailedCheckout = cartService.getCartByCustomerId(customerId); // Re-fetch
+ assertEquals(1, cartAfterFailedCheckout.getItems().size());
+ assertNotNull(cartAfterFailedCheckout.getSubTotal()); // Should still have its subtotal from before failure
}
+
+ // --- Old tests to adapt or verify ---
@Test
- void clearCart_cartNotFound_throwsGlobalHandlerException() {
+ void getCartByCustomerId_cartNotFound_throwsGlobalHandlerException() {
when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.empty());
-
- GlobalHandlerException exception = assertThrows(GlobalHandlerException.class,
- () -> cartService.clearCart(customerId));
- assertEquals(HttpStatus.NOT_FOUND, exception.getStatus());
- assertEquals("Cart not found", exception.getMessage());
- verify(cartRepository).findByCustomerId(customerId);
- verify(cartRepository, never()).save(any());
+ assertThrows(GlobalHandlerException.class, () -> cartService.getCartByCustomerId(customerId));
}
@Test
void archiveCart_activeCart_archivesCart() {
when(cartRepository.findByCustomerIdAndArchived(customerId, false)).thenReturn(Optional.of(cart));
- when(cartRepository.save(any(Cart.class))).thenReturn(cart);
Cart result = cartService.archiveCart(customerId);
assertTrue(result.isArchived());
- verify(cartRepository).findByCustomerIdAndArchived(customerId, false);
verify(cartRepository).save(cart);
}
@Test
void archiveCart_noActiveCart_throwsNoSuchElementException() {
when(cartRepository.findByCustomerIdAndArchived(customerId, false)).thenReturn(Optional.empty());
-
assertThrows(NoSuchElementException.class, () -> cartService.archiveCart(customerId));
- verify(cartRepository).findByCustomerIdAndArchived(customerId, false);
- verify(cartRepository, never()).save(any());
}
@Test
void unarchiveCart_archivedCart_unarchivesCart() {
cart.setArchived(true);
when(cartRepository.findByCustomerIdAndArchived(customerId, true)).thenReturn(Optional.of(cart));
- when(cartRepository.save(any(Cart.class))).thenReturn(cart);
+ when(cartRepository.findByCustomerIdAndArchived(customerId, false)).thenReturn(Optional.empty());
Cart result = cartService.unarchiveCart(customerId);
assertFalse(result.isArchived());
- verify(cartRepository).findByCustomerIdAndArchived(customerId, true);
verify(cartRepository).save(cart);
}
-
- @Test
- void unarchiveCart_noArchivedCart_throwsNoSuchElementException() {
- when(cartRepository.findByCustomerIdAndArchived(customerId, true)).thenReturn(Optional.empty());
-
- assertThrows(NoSuchElementException.class, () -> cartService.unarchiveCart(customerId));
- verify(cartRepository).findByCustomerIdAndArchived(customerId, true);
- verify(cartRepository, never()).save(any());
- }
-
- @Test
- void checkoutCart_validCart_sendsToOrderServiceAndClearsCart() {
- cart.getItems().add(new CartItem(productId, 1));
- when(cartRepository.findByCustomerIdAndArchived(customerId, false)).thenReturn(Optional.of(cart));
- when(cartRepository.save(any(Cart.class))).thenReturn(cart);
- when(restTemplate.postForObject(eq(orderServiceUrl + "/orders"), any(OrderRequest.class), eq(Void.class)))
- .thenReturn(null);
-
- Cart result = cartService.checkoutCart(customerId);
-
- assertTrue(result.getItems().isEmpty());
- verify(cartRepository).findByCustomerIdAndArchived(customerId, false);
- verify(restTemplate).postForObject(eq(orderServiceUrl + "/orders"), any(OrderRequest.class), eq(Void.class));
- verify(cartRepository).save(cart);
- }
-
- @Test
- void checkoutCart_noActiveCart_throwsNoSuchElementException() {
- when(cartRepository.findByCustomerIdAndArchived(customerId, false)).thenReturn(Optional.empty());
-
- assertThrows(NoSuchElementException.class, () -> cartService.checkoutCart(customerId));
- verify(cartRepository).findByCustomerIdAndArchived(customerId, false);
- verify(restTemplate, never()).postForObject(any(), any(), any());
- verify(cartRepository, never()).save(any());
- }
-
- @Test
- void checkoutCart_orderServiceFails_throwsRuntimeException() {
- cart.getItems().add(new CartItem(productId, 1));
- when(cartRepository.findByCustomerIdAndArchived(customerId, false)).thenReturn(Optional.of(cart));
- when(restTemplate.postForObject(eq(orderServiceUrl + "/orders"), any(OrderRequest.class), eq(Void.class)))
- .thenThrow(new RuntimeException("Order Service error"));
-
- RuntimeException exception = assertThrows(RuntimeException.class, () -> cartService.checkoutCart(customerId));
- assertEquals("Error communicating with Order Service", exception.getMessage());
- verify(cartRepository).findByCustomerIdAndArchived(customerId, false);
- verify(restTemplate).postForObject(eq(orderServiceUrl + "/orders"), any(OrderRequest.class), eq(Void.class));
- verify(cartRepository, never()).save(any());
- }
-}
\ No newline at end of file
+}
From 0285b6d82b1b6f012d4924af66c278fb2bdaf621 Mon Sep 17 00:00:00 2001
From: Abdelrahman Elmeky <65960126+Aelmeky@users.noreply.github.com>
Date: Tue, 13 May 2025 17:41:52 +0300
Subject: [PATCH 22/24] fix & test: fixing cart service and the cart service
tests
---
pom.xml | 2 +-
src/main/java/cart/service/CartService.java | 181 +++++--------
src/test/java/service/CartServiceTest.java | 284 ++++++++++----------
src/test/resources/application.properties | 12 +-
4 files changed, 218 insertions(+), 261 deletions(-)
diff --git a/pom.xml b/pom.xml
index baaece5..660f2f7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -92,7 +92,7 @@
org.mockito
mockito-core
- 5.5.0
+ 5.12.0
test
diff --git a/src/main/java/cart/service/CartService.java b/src/main/java/cart/service/CartService.java
index fb9c8e7..7cba59c 100644
--- a/src/main/java/cart/service/CartService.java
+++ b/src/main/java/cart/service/CartService.java
@@ -12,8 +12,6 @@
import org.springframework.stereotype.Service;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
-
-
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
@@ -26,7 +24,6 @@
public class CartService {
private final CartRepository cartRepository;
-
private final RabbitTemplate rabbitTemplate;
private final PromoCodeService promoCodeService;
@@ -35,7 +32,6 @@ public class CartService {
@Value("${rabbitmq.routing.key.checkout}")
private String checkoutRoutingKey;
- // Constructor Injection
public CartService(CartRepository cartRepository, RabbitTemplate rabbitTemplate, PromoCodeService promoCodeService) {
this.cartRepository = cartRepository;
this.rabbitTemplate = rabbitTemplate;
@@ -43,85 +39,79 @@ public CartService(CartRepository cartRepository, RabbitTemplate rabbitTemplate,
}
public Cart createCart(final String customerId) {
- log.debug("Entering createCart"
- + " with customerId:", customerId);
+ log.debug("Entering createCart with customerId: {}", customerId);
Cart cart = cartRepository.findByCustomerId(customerId)
.orElseGet(() -> {
- Cart newCart = new Cart(UUID.randomUUID()
- .toString(), customerId, new
- ArrayList<>(), false, null, BigDecimal.ZERO, BigDecimal.ZERO,BigDecimal.ZERO);
- log.debug("Cart created:", newCart);
+ Cart newCart = new Cart(
+ UUID.randomUUID().toString(),
+ customerId,
+ new ArrayList<>(),
+ false,
+ null,
+ BigDecimal.ZERO.setScale(2),
+ BigDecimal.ZERO.setScale(2),
+ BigDecimal.ZERO.setScale(2)
+ );
+ log.debug("Cart created: {}", newCart);
return cartRepository.save(newCart);
});
- log.debug("Cart retrieved:", cart);
+ log.debug("Cart retrieved: {}", cart);
return cart;
}
- public Cart addItemToCart(final String customerId,
- final CartItem newItem) {
- log.debug("Entering addItemToCart "
- + "with customerId:, newItem:",
- customerId, newItem);
+ public Cart addItemToCart(final String customerId, final CartItem newItem) {
+ log.debug("Entering addItemToCart with customerId: {}, newItem: {}", customerId, newItem);
CartCommand command = new AddItemCommand(this, customerId, newItem);
return command.execute();
}
-
- public Cart updateItemQuantity(final String customerId,
- final String productId, final int quantity) {
- log.debug("Entering updateItemQuantity with"
- + " customerId:, productId:, quantity: ",
- customerId, productId, quantity);
+ public Cart updateItemQuantity(final String customerId, final String productId, final int quantity) {
+ log.debug("Entering updateItemQuantity with customerId: {}, productId: {}, quantity: {}", customerId, productId, quantity);
CartCommand command = new UpdateQuantityCommand(this, customerId, productId, quantity);
return command.execute();
}
-
- public Cart removeItemFromCart(final String customerId,
- final String productId) {
- log.debug("Entering removeItemFromCart"
- + " with customerId:, productId:", customerId, productId);
-
+ public Cart removeItemFromCart(final String customerId, final String productId) {
+ log.debug("Entering removeItemFromCart with customerId: {}, productId: {}", customerId, productId);
CartCommand command = new RemoveItemCommand(this, customerId, productId);
return command.execute();
}
public void deleteCartByCustomerId(final String customerId) {
- log.debug("Entering deleteCartByCustomerId"
- + " with customerId:", customerId);
+ log.debug("Entering deleteCartByCustomerId with customerId: {}", customerId);
cartRepository.findByCustomerId(customerId)
.ifPresent(cart -> {
- log.debug("Deleting cart for customerId:", customerId);
+ log.debug("Deleting cart for customerId: {}", customerId);
cartRepository.delete(cart);
});
- log.debug("Cart deletion completed for"
- + " customerId:", customerId);
+ log.debug("Cart deletion completed for customerId: {}", customerId);
}
public Cart getCartByCustomerId(final String customerId) {
- log.debug("Entering getCartByCustomerId"
- + " with customerId:", customerId);
+ log.debug("Entering getCartByCustomerId with customerId: {}", customerId);
Cart cart = cartRepository.findByCustomerId(customerId)
.orElseThrow(() -> {
- log.error("Cart not found for customerId:", customerId);
- throw new GlobalHandlerException(
- HttpStatus.NOT_FOUND, "Cart not found");
+ log.error("Cart not found for customerId: {}", customerId);
+ throw new GlobalHandlerException(HttpStatus.NOT_FOUND, "Cart not found");
});
- log.debug("Cart retrieved:", cart);
+ log.debug("Cart retrieved: {}", cart);
return cart;
-
}
public void clearCart(final String customerId) {
- log.debug("Entering clearCart with customerId:", customerId);
+ log.debug("Entering clearCart with customerId: {}", customerId);
Cart cart = getCartByCustomerId(customerId);
cart.getItems().clear();
+ cart.setAppliedPromoCode(null);
+ cart.setSubTotal(BigDecimal.ZERO.setScale(2));
+ cart.setDiscountAmount(BigDecimal.ZERO.setScale(2));
+ cart.setTotalPrice(BigDecimal.ZERO.setScale(2));
cartRepository.save(cart);
- log.debug("Cart cleared for customerId:", customerId);
+ log.debug("Cart cleared for customerId: {}", customerId);
}
public Cart archiveCart(final String customerId) {
- log.debug("Entering archiveCart with customerId:", customerId);
+ log.debug("Entering archiveCart with customerId: {}", customerId);
Cart cart = getActiveCart(customerId);
cart.setArchived(true);
Cart archivedCart = cartRepository.save(cart);
@@ -130,84 +120,65 @@ public Cart archiveCart(final String customerId) {
}
public Cart unarchiveCart(final String customerId) {
- log.debug("Entering unarchiveCart with customerId:", customerId);
+ log.debug("Entering unarchiveCart with customerId: {}", customerId);
Cart cart = getArchivedCart(customerId);
cart.setArchived(false);
Cart activeCart = cartRepository.save(cart);
- log.debug("Cart unarchived:", activeCart);
+ log.debug("Cart unarchived: {}", activeCart);
return activeCart;
}
-
private Cart getActiveCart(final String customerId) {
- log.debug("Entering getActiveCart with customerId:", customerId);
- Cart cart = cartRepository.findByCustomerIdAndArchived(customerId,
- false)
+ log.debug("Entering getActiveCart with customerId: {}", customerId);
+ Cart cart = cartRepository.findByCustomerIdAndArchived(customerId, false)
.orElseThrow(() -> {
- log.error("Active cart not found"
- + " for customerId:", customerId);
- return new NoSuchElementException("Cart not"
- + " found for customer ID: " + customerId);
+ log.error("Active cart not found for customerId: {}", customerId);
+ return new NoSuchElementException("Cart not found for customer ID: " + customerId);
});
- log.debug("Active cart retrieved:", cart);
+ log.debug("Active cart retrieved: {}", cart);
return cart;
}
private Cart getArchivedCart(final String customerId) {
- log.debug("Entering getArchivedCart with customerId:", customerId);
+ log.debug("Entering getArchivedCart with customerId: {}", customerId);
Cart cart = cartRepository.findByCustomerIdAndArchived(customerId, true)
.orElseThrow(() -> {
- log.error("Archived cart not found"
- + " for customerId:", customerId);
- return new NoSuchElementException("No archived "
- + "cart found for customer ID: " + customerId);
+ log.error("Archived cart not found for customerId: {}", customerId);
+ return new NoSuchElementException("No archived cart found for customer ID: " + customerId);
});
- log.debug("Archived cart retrieved:", cart);
+ log.debug("Archived cart retrieved: {}", cart);
return cart;
}
-// public Cart saveCart(final Cart cart) {
-// return cartRepository.save(cart);
-// }
-
-
public Cart applyPromoCode(final String customerId, final String promoCodeInput) {
- log.debug("Entering applyPromoCode for customerId: {},"
- + " promoCode: {}", customerId, promoCodeInput);
+ log.debug("Entering applyPromoCode for customerId: {}, promoCode: {}", customerId, promoCodeInput);
Cart cart = getActiveCart(customerId);
String promoCodeUpper = promoCodeInput.toUpperCase();
PromoCode promoCode = promoCodeService.getActivePromoCode(promoCodeUpper)
.orElseThrow(() -> new GlobalHandlerException(
- HttpStatus.BAD_REQUEST, "Invalid, inactive, or expired promo code: "
- + promoCodeInput));
+ HttpStatus.BAD_REQUEST, "Invalid, inactive, or expired promo code: " + promoCodeInput));
- log.info("Applying valid promo code '{}' to "
- + "cartId: {}", promoCodeUpper, cart.getId());
+ log.info("Applying valid promo code '{}' to cartId: {}", promoCodeUpper, cart.getId());
cart.setAppliedPromoCode(promoCodeUpper);
return saveCart(cart);
}
-
public Cart removePromoCode(final String customerId) {
- log.debug("Entering removePromoCode for customerId:"
- + " {}", customerId);
+ log.debug("Entering removePromoCode for customerId: {}", customerId);
Cart cart = getActiveCart(customerId);
if (cart.getAppliedPromoCode() != null) {
- log.info("Removing applied promo code '{}' from cartId: {}",
- cart.getAppliedPromoCode(), cart.getId());
+ log.info("Removing applied promo code '{}' from cartId: {}", cart.getAppliedPromoCode(), cart.getId());
cart.setAppliedPromoCode(null);
return saveCart(cart);
} else {
- log.debug("No promo code to remove from cartId:"
- + " {}", cart.getId());
+ log.debug("No promo code to remove from cartId: {}", cart.getId());
return cart;
}
}
-
public Cart saveCart(final Cart cart) {
log.debug("Preparing to save cartId: {}", cart.getId());
recalculateCartTotals(cart);
@@ -215,50 +186,39 @@ public Cart saveCart(final Cart cart) {
return cartRepository.save(cart);
}
-
private void recalculateCartTotals(Cart cart) {
log.debug("Recalculating totals for cartId: {}", cart.getId());
-
BigDecimal subTotal = calculateSubTotal(cart);
String formattedSubTotal = String.format("%.2f", subTotal);
cart.setSubTotal(new BigDecimal(formattedSubTotal));
-
BigDecimal discountAmount = BigDecimal.ZERO;
if (cart.getAppliedPromoCode() != null) {
- Optional promoOpt =
- promoCodeService.getActivePromoCode(
- cart.getAppliedPromoCode());
+ Optional promoOpt = promoCodeService.getActivePromoCode(cart.getAppliedPromoCode());
if (promoOpt.isPresent()) {
PromoCode promo = promoOpt.get();
boolean validForCart = true;
- if (promo.getExpiryDate() != null && promo
- .getExpiryDate().isBefore(Instant.now())) {
- log.warn("Applied promo code {} is expired."
- + " Removing.", cart.getAppliedPromoCode());
+ if (promo.getExpiryDate() != null && promo.getExpiryDate().isBefore(Instant.now())) {
+ log.warn("Applied promo code {} is expired. Removing.", cart.getAppliedPromoCode());
+ cart.setAppliedPromoCode(null);
validForCart = false;
}
if (validForCart) {
- if (promo.getDiscountType() ==
- PromoCode.DiscountType.PERCENTAGE) {
- BigDecimal percentageValue = promo.getDiscountValue()
- .divide(new BigDecimal("100"));
- String formattedPercentage = String
- .format("%.2f", percentageValue);
- BigDecimal percentage = new BigDecimal(formattedPercentage); discountAmount = subTotal.multiply(percentage);
- } else if (promo.getDiscountType() ==
- PromoCode.DiscountType.FIXED_AMOUNT) {
+ if (promo.getDiscountType() == PromoCode.DiscountType.PERCENTAGE) {
+ BigDecimal percentageValue = promo.getDiscountValue().divide(new BigDecimal("100"));
+ String formattedPercentage = String.format("%.2f", percentageValue);
+ BigDecimal percentage = new BigDecimal(formattedPercentage);
+ discountAmount = subTotal.multiply(percentage);
+ } else if (promo.getDiscountType() == PromoCode.DiscountType.FIXED_AMOUNT) {
discountAmount = promo.getDiscountValue();
}
}
-
} else {
- log.warn("Applied promo code {} is no longer valid."
- + " Removing.", cart.getAppliedPromoCode());
+ log.warn("Applied promo code {} is no longer valid. Removing.", cart.getAppliedPromoCode());
cart.setAppliedPromoCode(null);
}
}
@@ -272,10 +232,8 @@ private void recalculateCartTotals(Cart cart) {
String formattedTotalPrice = String.format("%.2f", totalPrice);
cart.setTotalPrice(new BigDecimal(formattedTotalPrice));
- log.debug("Recalculated totals for cartId: {}:"
- + " SubTotal={}, Discount={}, Total={}",
- cart.getId(), cart.getSubTotal(),
- cart.getDiscountAmount(), cart.getTotalPrice());
+ log.debug("Recalculated totals for cartId: {}: SubTotal={}, Discount={}, Total={}",
+ cart.getId(), cart.getSubTotal(), cart.getDiscountAmount(), cart.getTotalPrice());
}
private BigDecimal calculateSubTotal(Cart cart) {
@@ -283,13 +241,11 @@ private BigDecimal calculateSubTotal(Cart cart) {
return BigDecimal.ZERO;
}
return cart.getItems().stream()
- .filter(item -> item.getUnitPrice() != null
- && item.getQuantity() > 0)
+ .filter(item -> item.getUnitPrice() != null && item.getQuantity() > 0)
.map(CartItem::getItemTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
-
public Cart checkoutCart(final String customerId) {
log.debug("Entering checkoutCart [RabbitMQ] for customerId: {}", customerId);
Cart cart = getActiveCart(customerId);
@@ -297,13 +253,10 @@ public Cart checkoutCart(final String customerId) {
recalculateCartTotals(cart);
if (cart.getItems().isEmpty()) {
- log.warn("Attempted checkout for customerId: "
- + "{} with an empty cart.", customerId);
- throw new GlobalHandlerException(HttpStatus.BAD_REQUEST,
- "Cannot checkout an empty cart.");
+ log.warn("Attempted checkout for customerId: {} with an empty cart.", customerId);
+ throw new GlobalHandlerException(HttpStatus.BAD_REQUEST, "Cannot checkout an empty cart.");
}
- // Create the event payload using the potentially recalculated fields from the cart
OrderRequest checkoutEvent = new OrderRequest(
UUID.randomUUID().toString(),
customerId,
@@ -333,4 +286,4 @@ public Cart checkoutCart(final String customerId) {
throw new RuntimeException("Checkout process failed: Could not publish event.", e);
}
}
-}
+}
\ No newline at end of file
diff --git a/src/test/java/service/CartServiceTest.java b/src/test/java/service/CartServiceTest.java
index 97cd60c..96638bc 100644
--- a/src/test/java/service/CartServiceTest.java
+++ b/src/test/java/service/CartServiceTest.java
@@ -6,17 +6,14 @@
import cart.model.OrderRequest;
import cart.model.PromoCode;
import cart.repository.CartRepository;
-
import cart.service.CartService;
import cart.service.PromoCodeService;
-
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
-import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.http.HttpStatus;
@@ -30,9 +27,7 @@
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@@ -47,12 +42,11 @@ class CartServiceTest {
@Mock
private PromoCodeService promoCodeService;
- @Spy
@InjectMocks
private CartService cartService;
private Cart cart;
- private CartItem item1Input; // Item as input to service methods
+ private CartItem item1Input;
private CartItem item2Input;
private final String customerId = "cust123";
@@ -65,303 +59,287 @@ class CartServiceTest {
private final String exchangeName = "test.cart.events";
private final String checkoutRoutingKey = "test.order.checkout.initiate";
- private Cart createTestCartInstance() {
- return new Cart(cartId, customerId, new ArrayList<>(), false, null,
+ private Cart createNewTestCart(String cId, String crtId) {
+ return new Cart(crtId, cId, new ArrayList<>(), false, null,
BigDecimal.ZERO.setScale(2), BigDecimal.ZERO.setScale(2), BigDecimal.ZERO.setScale(2));
}
- private PromoCode createTestPromoCode(String code, PromoCode.DiscountType type, BigDecimal value, BigDecimal minPurchase, Instant expiryDate, boolean isActive) {
+ private PromoCode createTestPromoCode(String code, PromoCode.DiscountType type, BigDecimal value, BigDecimal minPurchase, Instant expiry, boolean active) {
PromoCode promo = new PromoCode();
promo.setCode(code.toUpperCase());
- promo.setActive(isActive);
promo.setDiscountType(type);
promo.setDiscountValue(value);
promo.setMinimumPurchaseAmount(minPurchase);
- promo.setExpiryDate(expiryDate);
+ promo.setExpiryDate(expiry);
+ promo.setActive(active);
return promo;
}
@BeforeEach
void setUp() {
- cart = createTestCartInstance();
+ cart = createNewTestCart(customerId, cartId);
+
item1Input = new CartItem(productId1, 1, price1);
item2Input = new CartItem(productId2, 2, price2);
ReflectionTestUtils.setField(cartService, "exchangeName", exchangeName);
ReflectionTestUtils.setField(cartService, "checkoutRoutingKey", checkoutRoutingKey);
+ lenient().when(cartRepository.save(any(Cart.class))).thenAnswer(invocation -> invocation.getArgument(0));
- lenient().when(cartRepository.save(any(Cart.class))).thenAnswer(invocation -> {
- Cart cartToSave = invocation.getArgument(0);
-
- return cartToSave;
- });
-
-
-
- lenient().when(cartService.getCartByCustomerId(customerId)).thenReturn(cart);
- lenient().when(cartRepository.findByCustomerIdAndArchived(customerId, false)).thenReturn(Optional.of(cart));
- lenient().when(cartRepository.findByCustomerIdAndArchived(customerId, true))
- .thenReturn(Optional.of(new Cart(cartId, customerId, new ArrayList<>(), true, null, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO)));
+ lenient().when(cartRepository.findByCustomerId(anyString())).thenReturn(Optional.empty());
+ lenient().when(cartRepository.findByCustomerId(eq(customerId))).thenReturn(Optional.of(cart));
+ lenient().when(cartRepository.findByCustomerIdAndArchived(anyString(), anyBoolean())).thenReturn(Optional.empty());
+ lenient().when(cartRepository.findByCustomerIdAndArchived(eq(customerId), eq(false))).thenReturn(Optional.of(cart));
+ lenient().when(cartRepository.findByCustomerIdAndArchived(eq(customerId), eq(true))).thenReturn(Optional.of(createNewTestCart(customerId, cartId + "_archived")));
}
@Test
void createCart_existingCart_returnsCartAndDoesNotSave() {
- Cart existingCart = createTestCartInstance();
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(existingCart));
-
Cart result = cartService.createCart(customerId);
- assertEquals(existingCart, result);
+ assertEquals(cart, result);
verify(cartRepository).findByCustomerId(customerId);
verify(cartRepository, never()).save(any());
}
@Test
void createCart_noExistingCart_createsAndSavesNewCartWithZeroTotals() {
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.empty());
+ String newCustId = "newCust456";
+ when(cartRepository.findByCustomerId(eq(newCustId))).thenReturn(Optional.empty());
when(cartRepository.save(any(Cart.class))).thenAnswer(invocation -> {
Cart newCart = invocation.getArgument(0);
- newCart.setId(cartId);
+ if (newCart.getId() == null) newCart.setId(UUID.randomUUID().toString());
return newCart;
});
+ Cart result = cartService.createCart(newCustId);
- Cart result = cartService.createCart(customerId);
-
- assertEquals(customerId, result.getCustomerId());
+ assertEquals(newCustId, result.getCustomerId());
assertNotNull(result.getId());
assertFalse(result.isArchived());
assertTrue(result.getItems().isEmpty());
- assertEquals(BigDecimal.ZERO.setScale(2), result.getSubTotal());
- assertEquals(BigDecimal.ZERO.setScale(2), result.getDiscountAmount());
- assertEquals(BigDecimal.ZERO.setScale(2), result.getTotalPrice());
+ ArgumentCaptor cartCaptor = ArgumentCaptor.forClass(Cart.class);
+ verify(cartRepository).save(cartCaptor.capture());
+ Cart savedCart = cartCaptor.getValue();
+ assertEquals(BigDecimal.ZERO.setScale(2), savedCart.getSubTotal());
+ assertEquals(BigDecimal.ZERO.setScale(2), savedCart.getDiscountAmount());
+ assertEquals(BigDecimal.ZERO.setScale(2), savedCart.getTotalPrice());
assertNull(result.getAppliedPromoCode());
- verify(cartRepository).findByCustomerId(customerId);
- verify(cartRepository).save(any(Cart.class));
- }
+ verify(cartRepository).findByCustomerId(eq(newCustId));
+ }
@Test
void addItemToCart_newItem_addsItemAndRecalculatesTotals() {
- // Act
- Cart result = cartService.addItemToCart(customerId, item1Input); // 1 x 10.50
+ Cart result = cartService.addItemToCart(customerId, item1Input);
- // Assert
assertEquals(1, result.getItems().size());
- assertTrue(result.getItems().stream().anyMatch(i -> i.getProductId().equals(productId1) && i.getQuantity() == 1));
+ CartItem added = result.getItems().get(0);
+ assertEquals(productId1, added.getProductId());
+ assertEquals(1, added.getQuantity());
+ assertEquals(price1, added.getUnitPrice());
+
assertEquals(new BigDecimal("10.50").setScale(2), result.getSubTotal());
assertEquals(BigDecimal.ZERO.setScale(2), result.getDiscountAmount());
assertEquals(new BigDecimal("10.50").setScale(2), result.getTotalPrice());
- verify(cartRepository, atLeastOnce()).save(any(Cart.class)); // saveCart is called by command
+ verify(cartRepository, times(1)).save(any(Cart.class));
}
@Test
void addItemToCart_existingItem_updatesQuantityAndRecalculatesTotals() {
- // Initial state: cart has item1
cart.getItems().add(new CartItem(productId1, 1, price1));
- cartService.saveCart(cart); // Save to set initial totals
- // Act: Add more of item1
CartItem additionalItem1 = new CartItem(productId1, 2, price1);
- Cart result = cartService.addItemToCart(customerId, additionalItem1); // Adds 2, total qty = 3
+ Cart result = cartService.addItemToCart(customerId, additionalItem1);
- // Assert
assertEquals(1, result.getItems().size());
CartItem updatedItem = result.getItems().get(0);
assertEquals(productId1, updatedItem.getProductId());
assertEquals(3, updatedItem.getQuantity());
- assertEquals(new BigDecimal("31.50").setScale(2), result.getSubTotal()); // 10.50 * 3
+ assertEquals(new BigDecimal("31.50").setScale(2), result.getSubTotal());
assertEquals(BigDecimal.ZERO.setScale(2), result.getDiscountAmount());
assertEquals(new BigDecimal("31.50").setScale(2), result.getTotalPrice());
- verify(cartRepository, atLeastOnce()).save(any(Cart.class));
+ verify(cartRepository, times(1)).save(any(Cart.class));
}
-
@Test
void updateItemQuantity_existingItem_updatesAndRecalculates() {
- cart.getItems().add(new CartItem(productId1, 2, price1)); // 2 * 10.50 = 21.00
- cartService.saveCart(cart); // Set initial totals
+ cart.getItems().add(new CartItem(productId1, 2, price1));
- // Act
- Cart result = cartService.updateItemQuantity(customerId, productId1, 5); // Update to 5 * 10.50 = 52.50
+ Cart result = cartService.updateItemQuantity(customerId, productId1, 5);
- // Assert
assertEquals(1, result.getItems().size());
assertEquals(5, result.getItems().get(0).getQuantity());
assertEquals(new BigDecimal("52.50").setScale(2), result.getSubTotal());
+ assertEquals(BigDecimal.ZERO.setScale(2), result.getDiscountAmount());
assertEquals(new BigDecimal("52.50").setScale(2), result.getTotalPrice());
- verify(cartRepository, atLeastOnce()).save(any(Cart.class));
+ verify(cartRepository, times(1)).save(any(Cart.class));
}
@Test
void updateItemQuantity_quantityToZero_removesItemAndRecalculates() {
cart.getItems().add(new CartItem(productId1, 2, price1));
- cart.getItems().add(new CartItem(productId2, 1, price2)); // p1: 21.00, p2: 5.00. Sub: 26.00
- cartService.saveCart(cart);
+ cart.getItems().add(new CartItem(productId2, 1, price2));
- // Act: Remove productId1
Cart result = cartService.updateItemQuantity(customerId, productId1, 0);
- // Assert
assertEquals(1, result.getItems().size());
assertEquals(productId2, result.getItems().get(0).getProductId());
assertEquals(new BigDecimal("5.00").setScale(2), result.getSubTotal());
+ assertEquals(BigDecimal.ZERO.setScale(2), result.getDiscountAmount());
assertEquals(new BigDecimal("5.00").setScale(2), result.getTotalPrice());
- verify(cartRepository, atLeastOnce()).save(any(Cart.class));
+ verify(cartRepository, times(1)).save(any(Cart.class));
}
@Test
void removeItemFromCart_itemExists_removesAndRecalculates() {
cart.getItems().add(new CartItem(productId1, 2, price1));
cart.getItems().add(new CartItem(productId2, 1, price2));
- cartService.saveCart(cart);
- // Act
Cart result = cartService.removeItemFromCart(customerId, productId1);
- // Assert
assertEquals(1, result.getItems().size());
assertEquals(productId2, result.getItems().get(0).getProductId());
assertEquals(new BigDecimal("5.00").setScale(2), result.getSubTotal());
+ assertEquals(BigDecimal.ZERO.setScale(2), result.getDiscountAmount());
assertEquals(new BigDecimal("5.00").setScale(2), result.getTotalPrice());
- verify(cartRepository, atLeastOnce()).save(any(Cart.class));
+ verify(cartRepository, times(1)).save(any(Cart.class));
}
@Test
- void clearCart_itemsExist_clearsItemsAndResetsTotals() {
+ void clearCart_itemsExist_clearsItemsAndResetsTotalsAndPromo() {
cart.getItems().add(new CartItem(productId1, 1, price1));
cart.setAppliedPromoCode("TESTCODE");
- cartService.saveCart(cart); // Initial save to set totals
-
- // Act
- cartService.clearCart(customerId); // This modifies 'cart' in place due to getCartByCustomerId
-
- // Assert
- // The cart object itself should be modified. saveCart is called inside clearCart.
- assertTrue(cart.getItems().isEmpty());
- assertNull(cart.getAppliedPromoCode());
- assertEquals(BigDecimal.ZERO.setScale(2), cart.getSubTotal());
- assertEquals(BigDecimal.ZERO.setScale(2), cart.getDiscountAmount());
- assertEquals(BigDecimal.ZERO.setScale(2), cart.getTotalPrice());
- verify(cartRepository, times(2)).save(cart); // Initial save + save in clearCart
+ cart.setSubTotal(new BigDecimal("10.50"));
+ cart.setDiscountAmount(new BigDecimal("1.00"));
+ cart.setTotalPrice(new BigDecimal("9.50"));
+
+ cartService.clearCart(customerId);
+
+ ArgumentCaptor cartCaptor = ArgumentCaptor.forClass(Cart.class);
+ verify(cartRepository).save(cartCaptor.capture());
+ Cart savedCart = cartCaptor.getValue();
+ assertTrue(savedCart.getItems().isEmpty());
+ assertNull(savedCart.getAppliedPromoCode());
+ assertEquals(BigDecimal.ZERO.setScale(2), savedCart.getSubTotal());
+ assertEquals(BigDecimal.ZERO.setScale(2), savedCart.getDiscountAmount());
+ assertEquals(BigDecimal.ZERO.setScale(2), savedCart.getTotalPrice());
}
- // --- Promo Code Tests ---
@Test
void applyPromoCode_validPercentageCode_calculatesDiscount() {
- cart.getItems().add(new CartItem(productId1, 2, new BigDecimal("10.00"))); // SubTotal = 20.00
- cartService.saveCart(cart); // Initial calculation
+ cart.getItems().add(new CartItem(productId1, 2, new BigDecimal("10.00")));
String promoCodeStr = "SAVE10";
PromoCode promo = createTestPromoCode(promoCodeStr, PromoCode.DiscountType.PERCENTAGE, new BigDecimal("10"), null, null, true);
- when(promoCodeService.getActivePromoCode(promoCodeStr)).thenReturn(Optional.of(promo));
+ when(promoCodeService.getActivePromoCode(promoCodeStr.toUpperCase())).thenReturn(Optional.of(promo));
- // Act
Cart result = cartService.applyPromoCode(customerId, promoCodeStr);
- // Assert
- assertEquals(promoCodeStr, result.getAppliedPromoCode());
+ assertEquals(promoCodeStr.toUpperCase(), result.getAppliedPromoCode());
assertEquals(new BigDecimal("20.00").setScale(2), result.getSubTotal());
- assertEquals(new BigDecimal("2.00").setScale(2), result.getDiscountAmount()); // 10% of 20.00
+ assertEquals(new BigDecimal("2.00").setScale(2), result.getDiscountAmount());
assertEquals(new BigDecimal("18.00").setScale(2), result.getTotalPrice());
- verify(cartRepository, atLeastOnce()).save(any(Cart.class));
+ verify(cartRepository, times(1)).save(any(Cart.class));
}
@Test
void applyPromoCode_validFixedCode_calculatesDiscount() {
- cart.getItems().add(new CartItem(productId1, 3, new BigDecimal("10.00"))); // SubTotal = 30.00
- cartService.saveCart(cart);
+ cart.getItems().add(new CartItem(productId1, 3, new BigDecimal("10.00")));
String promoCodeStr = "5OFF";
PromoCode promo = createTestPromoCode(promoCodeStr, PromoCode.DiscountType.FIXED_AMOUNT, new BigDecimal("5.00"), null, null, true);
- when(promoCodeService.getActivePromoCode(promoCodeStr)).thenReturn(Optional.of(promo));
+ when(promoCodeService.getActivePromoCode(promoCodeStr.toUpperCase())).thenReturn(Optional.of(promo));
- // Act
Cart result = cartService.applyPromoCode(customerId, promoCodeStr);
- // Assert
- assertEquals(promoCodeStr, result.getAppliedPromoCode());
+ assertEquals(promoCodeStr.toUpperCase(), result.getAppliedPromoCode());
assertEquals(new BigDecimal("30.00").setScale(2), result.getSubTotal());
assertEquals(new BigDecimal("5.00").setScale(2), result.getDiscountAmount());
assertEquals(new BigDecimal("25.00").setScale(2), result.getTotalPrice());
+ verify(cartRepository, times(1)).save(any(Cart.class));
+ }
+
+ @Test
+ void applyPromoCode_fixedDiscountExceedsSubtotal_discountCapped() {
+ cart.getItems().add(new CartItem(productId1, 1, new BigDecimal("3.00")));
+
+ String promoCodeStr = "BIGOFF";
+ PromoCode promo = createTestPromoCode(promoCodeStr, PromoCode.DiscountType.FIXED_AMOUNT, new BigDecimal("5.00"), null, null, true);
+ when(promoCodeService.getActivePromoCode(promoCodeStr.toUpperCase())).thenReturn(Optional.of(promo));
+
+ Cart result = cartService.applyPromoCode(customerId, promoCodeStr);
+
+ assertEquals(new BigDecimal("3.00").setScale(2), result.getSubTotal());
+ assertEquals(new BigDecimal("3.00").setScale(2), result.getDiscountAmount());
+ assertEquals(BigDecimal.ZERO.setScale(2), result.getTotalPrice());
}
@Test
- void applyPromoCode_invalidCode_throwsException() {
+ void applyPromoCode_invalidCode_throwsGlobalHandlerException() {
String invalidCode = "FAKECODE";
when(promoCodeService.getActivePromoCode(invalidCode.toUpperCase())).thenReturn(Optional.empty());
GlobalHandlerException ex = assertThrows(GlobalHandlerException.class,
() -> cartService.applyPromoCode(customerId, invalidCode));
+
assertEquals(HttpStatus.BAD_REQUEST, ex.getStatus());
assertTrue(ex.getMessage().contains("Invalid, inactive, or expired promo code"));
- assertNull(cart.getAppliedPromoCode()); // Should not be set
+ verify(cartRepository, never()).save(any());
}
@Test
- void applyPromoCode_expiredCode_noDiscountCodeRemoved() {
+ void applyPromoCode_expiredCode_removesCodeAndNoDiscount() {
cart.getItems().add(new CartItem(productId1, 1, new BigDecimal("100.00")));
- cartService.saveCart(cart);
String promoCodeStr = "EXPIRED";
- PromoCode promo = createTestPromoCode(promoCodeStr, PromoCode.DiscountType.PERCENTAGE, new BigDecimal("10"), null, Instant.now().minusSeconds(3600), true); // Expired 1 hour ago
- when(promoCodeService.getActivePromoCode(promoCodeStr)).thenReturn(Optional.of(promo)); // Mock this to return it, recalc logic should handle expiry
+ PromoCode promo = createTestPromoCode(promoCodeStr, PromoCode.DiscountType.PERCENTAGE, new BigDecimal("10"),
+ null, Instant.now().minusSeconds(3600), true);
+ when(promoCodeService.getActivePromoCode(promoCodeStr.toUpperCase())).thenReturn(Optional.of(promo));
- // Act
- Cart result = cartService.applyPromoCode(customerId, promoCodeStr); // applies then save calls recalculate
+ Cart result = cartService.applyPromoCode(customerId, promoCodeStr);
- // Assert
- // RecalculateTotals should detect expiry based on the logic added in CartService.
- // If it removes the code:
assertNull(result.getAppliedPromoCode());
assertEquals(new BigDecimal("100.00").setScale(2), result.getSubTotal());
assertEquals(BigDecimal.ZERO.setScale(2), result.getDiscountAmount());
assertEquals(new BigDecimal("100.00").setScale(2), result.getTotalPrice());
+ verify(cartRepository, times(1)).save(any(Cart.class));
}
-
@Test
- void removePromoCode_codeExists_removesCodeAndRecalculates() {
- cart.getItems().add(new CartItem(productId1, 2, new BigDecimal("10.00"))); // SubTotal = 20.00
+ void removePromoCode_codeExists_removesCodeAndResetsDiscount() {
+ cart.getItems().add(new CartItem(productId1, 2, new BigDecimal("10.00")));
cart.setAppliedPromoCode("SAVE10");
- // Manually set initial discount for testing removal logic
cart.setSubTotal(new BigDecimal("20.00"));
cart.setDiscountAmount(new BigDecimal("2.00"));
cart.setTotalPrice(new BigDecimal("18.00"));
- // No need to call cartService.saveCart(cart); as we are testing removePromoCode directly after setting this state.
- // Act
Cart result = cartService.removePromoCode(customerId);
- // Assert
assertNull(result.getAppliedPromoCode());
assertEquals(new BigDecimal("20.00").setScale(2), result.getSubTotal());
assertEquals(BigDecimal.ZERO.setScale(2), result.getDiscountAmount());
assertEquals(new BigDecimal("20.00").setScale(2), result.getTotalPrice());
+ verify(cartRepository, times(1)).save(any(Cart.class));
}
- // --- Checkout Tests ---
@Test
- void checkoutCart_validCartWithPromo_publishesEventWithCorrectTotalsAndClearsCart() {
+ void checkoutCart_validCartWithPromo_publishesEventAndClearsCart() {
cart.getItems().add(new CartItem(productId1, 1, new BigDecimal("100.00")));
cart.setAppliedPromoCode("SAVE10");
- // Simulate totals as if SAVE10 (10%) was applied correctly before checkout
cart.setSubTotal(new BigDecimal("100.00"));
cart.setDiscountAmount(new BigDecimal("10.00"));
cart.setTotalPrice(new BigDecimal("90.00"));
- // Mock promoCodeService for the recalculateCartTotals call within checkoutCart
PromoCode promo = createTestPromoCode("SAVE10", PromoCode.DiscountType.PERCENTAGE, new BigDecimal("10"), null, null, true);
when(promoCodeService.getActivePromoCode("SAVE10")).thenReturn(Optional.of(promo));
- doNothing().when(rabbitTemplate).convertAndSend(anyString(), anyString(), any(OrderRequest.class));
+ doNothing().when(rabbitTemplate).convertAndSend(anyString(), anyString(), any(OrderRequest.class));
- // Act
Cart result = cartService.checkoutCart(customerId);
- // Assert Published Event
ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(OrderRequest.class);
verify(rabbitTemplate).convertAndSend(eq(exchangeName), eq(checkoutRoutingKey), eventCaptor.capture());
OrderRequest publishedEvent = eventCaptor.getValue();
@@ -374,58 +352,74 @@ void checkoutCart_validCartWithPromo_publishesEventWithCorrectTotalsAndClearsCar
assertEquals(new BigDecimal("90.00").setScale(2), publishedEvent.getTotalPrice());
assertEquals("SAVE10", publishedEvent.getAppliedPromoCode());
- // Assert Cart State after Checkout (returned by method)
assertTrue(result.getItems().isEmpty());
assertNull(result.getAppliedPromoCode());
assertEquals(BigDecimal.ZERO.setScale(2), result.getSubTotal());
assertEquals(BigDecimal.ZERO.setScale(2), result.getDiscountAmount());
assertEquals(BigDecimal.ZERO.setScale(2), result.getTotalPrice());
- verify(cartRepository, times(1)).save(any(Cart.class)); // Save for clearing
+ verify(cartRepository, times(1)).save(any(Cart.class));
}
@Test
- void checkoutCart_emptyCart_throwsException() {
- // cart is empty by default in setup for this test
+ void checkoutCart_emptyCart_throwsGlobalHandlerException() {
+ when(cartRepository.findByCustomerIdAndArchived(customerId, false)).thenReturn(Optional.of(cart));
+
GlobalHandlerException ex = assertThrows(GlobalHandlerException.class,
() -> cartService.checkoutCart(customerId));
+
assertEquals(HttpStatus.BAD_REQUEST, ex.getStatus());
assertEquals("Cannot checkout an empty cart.", ex.getMessage());
- verify(rabbitTemplate, never()).convertAndSend(eq(exchangeName), eq(checkoutRoutingKey), any(OrderRequest.class));
+ verify(rabbitTemplate, never()).convertAndSend(anyString(), anyString(), any(OrderRequest.class));
}
@Test
void checkoutCart_rabbitMqFails_throwsRuntimeExceptionAndCartNotCleared() {
cart.getItems().add(item1Input);
- cartService.saveCart(cart); // Initial state
+
+ BigDecimal subTotal = item1Input.getUnitPrice();
+ String formattedSubTotal = String.format("%.2f", subTotal);
+ BigDecimal bigZero = BigDecimal.ZERO;
+ String formattedBigZero = String.format("%.2f", bigZero);
+ cart.setSubTotal(new BigDecimal(formattedSubTotal));
+ cart.setTotalPrice(new BigDecimal(formattedSubTotal));
+ cart.setDiscountAmount(new BigDecimal(formattedBigZero));
doThrow(new RuntimeException("RabbitMQ publish error")).when(rabbitTemplate)
- .convertAndSend(anyString(), anyString(), any(OrderRequest.class));
+ .convertAndSend(eq(exchangeName), eq(checkoutRoutingKey), any(OrderRequest.class));
RuntimeException ex = assertThrows(RuntimeException.class,
() -> cartService.checkoutCart(customerId));
+
assertTrue(ex.getMessage().contains("Checkout process failed: Could not publish event."));
+ verify(cartRepository, never()).save(any(Cart.class));
- // Verify cart state is NOT cleared
- Cart cartAfterFailedCheckout = cartService.getCartByCustomerId(customerId); // Re-fetch
- assertEquals(1, cartAfterFailedCheckout.getItems().size());
- assertNotNull(cartAfterFailedCheckout.getSubTotal()); // Should still have its subtotal from before failure
+ assertEquals(1, cart.getItems().size());
+ assertEquals(new BigDecimal(formattedSubTotal), cart.getSubTotal());
}
+ @Test
+ void getCartByCustomerId_cartExists_returnsCart() {
+ Cart result = cartService.getCartByCustomerId(customerId);
+ assertEquals(cart, result);
+ verify(cartRepository).findByCustomerId(customerId);
+ }
- // --- Old tests to adapt or verify ---
@Test
void getCartByCustomerId_cartNotFound_throwsGlobalHandlerException() {
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.empty());
- assertThrows(GlobalHandlerException.class, () -> cartService.getCartByCustomerId(customerId));
+ String nonExistentCustId = "ghost";
+ when(cartRepository.findByCustomerId(eq(nonExistentCustId))).thenReturn(Optional.empty());
+
+ GlobalHandlerException exception = assertThrows(GlobalHandlerException.class,
+ () -> cartService.getCartByCustomerId(nonExistentCustId));
+ assertEquals(HttpStatus.NOT_FOUND, exception.getStatus());
+ assertEquals("Cart not found", exception.getMessage());
+ verify(cartRepository).findByCustomerId(eq(nonExistentCustId));
}
@Test
void archiveCart_activeCart_archivesCart() {
- when(cartRepository.findByCustomerIdAndArchived(customerId, false)).thenReturn(Optional.of(cart));
-
Cart result = cartService.archiveCart(customerId);
-
assertTrue(result.isArchived());
verify(cartRepository).save(cart);
}
@@ -438,13 +432,13 @@ void archiveCart_noActiveCart_throwsNoSuchElementException() {
@Test
void unarchiveCart_archivedCart_unarchivesCart() {
- cart.setArchived(true);
- when(cartRepository.findByCustomerIdAndArchived(customerId, true)).thenReturn(Optional.of(cart));
- when(cartRepository.findByCustomerIdAndArchived(customerId, false)).thenReturn(Optional.empty());
+ Cart archivedCart = createNewTestCart(customerId, "archivedCrt");
+ archivedCart.setArchived(true);
+ when(cartRepository.findByCustomerIdAndArchived(customerId, true)).thenReturn(Optional.of(archivedCart));
Cart result = cartService.unarchiveCart(customerId);
assertFalse(result.isArchived());
- verify(cartRepository).save(cart);
+ verify(cartRepository).save(archivedCart);
}
-}
+}
\ No newline at end of file
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
index a38ed3d..f3afe00 100644
--- a/src/test/resources/application.properties
+++ b/src/test/resources/application.properties
@@ -1,4 +1,14 @@
spring.data.mongodb.uri=mongodb://localhost:27017/testdb
spring.data.mongodb.database=testdb
spring.main.banner-mode= off
-spring.main.log-startup-info=false
\ No newline at end of file
+spring.main.log-startup-info=false
+# RabbitMQ Configuration
+spring.rabbitmq.host=localhost
+spring.rabbitmq.port=5672
+spring.rabbitmq.username=guest # Use appropriate credentials
+spring.rabbitmq.password=guest # Use appropriate credentials
+# spring.rabbitmq.virtual-host=/ # Optional
+
+# Custom properties for exchange/routing keys
+rabbitmq.exchange.name=cart.events
+rabbitmq.routing.key.checkout=order.checkout.initiate
\ No newline at end of file
From 5cd549e4a055d64d2ec34bcaf4843e3b42c6063d Mon Sep 17 00:00:00 2001
From: Abdelrahman Elmeky <65960126+Aelmeky@users.noreply.github.com>
Date: Tue, 13 May 2025 17:46:58 +0300
Subject: [PATCH 23/24] tests: adding unit tests for the promocode service:
---
.../java/service/PromoCodeServiceTest.java | 192 ++++++++++++++++++
1 file changed, 192 insertions(+)
create mode 100644 src/test/java/service/PromoCodeServiceTest.java
diff --git a/src/test/java/service/PromoCodeServiceTest.java b/src/test/java/service/PromoCodeServiceTest.java
new file mode 100644
index 0000000..aa6116a
--- /dev/null
+++ b/src/test/java/service/PromoCodeServiceTest.java
@@ -0,0 +1,192 @@
+package service;
+
+import cart.exception.GlobalHandlerException;
+import cart.model.PromoCode;
+import cart.repository.PromoCodeRepository;
+import cart.service.PromoCodeService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.HttpStatus;
+
+import java.math.BigDecimal;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class PromoCodeServiceTest {
+
+ @Mock
+ private PromoCodeRepository promoCodeRepository;
+
+ @InjectMocks
+ private PromoCodeService promoCodeService;
+
+ private PromoCode promoCode;
+
+ @BeforeEach
+ void setUp() {
+ promoCode = new PromoCode();
+ promoCode.setId("promo1");
+ promoCode.setCode("SAVE10");
+ promoCode.setDescription("10% off");
+ promoCode.setDiscountType(PromoCode.DiscountType.PERCENTAGE);
+ promoCode.setDiscountValue(new BigDecimal("10.00"));
+ promoCode.setActive(true);
+ promoCode.setExpiryDate(Instant.now().plusSeconds(3600));
+ promoCode.setMinimumPurchaseAmount(new BigDecimal("50.00"));
+ }
+
+ @Test
+ void createOrUpdatePromoCode_newPromoCode_createsAndSaves() {
+ PromoCode input = new PromoCode();
+ input.setCode("NEWCODE");
+ input.setDiscountType(PromoCode.DiscountType.FIXED_AMOUNT);
+ input.setDiscountValue(new BigDecimal("5.00"));
+ input.setActive(true);
+
+ when(promoCodeRepository.findByCode("NEWCODE")).thenReturn(Optional.empty());
+ when(promoCodeRepository.save(any(PromoCode.class))).thenAnswer(invocation -> invocation.getArgument(0));
+
+ PromoCode result = promoCodeService.createOrUpdatePromoCode(input);
+
+ assertEquals("NEWCODE", result.getCode());
+ assertEquals(PromoCode.DiscountType.FIXED_AMOUNT, result.getDiscountType());
+ assertEquals(new BigDecimal("5.00"), result.getDiscountValue());
+ assertTrue(result.isActive());
+ verify(promoCodeRepository).findByCode("NEWCODE");
+ verify(promoCodeRepository).save(input);
+ }
+
+ @Test
+ void createOrUpdatePromoCode_existingPromoCode_updatesAndSaves() {
+ PromoCode input = new PromoCode();
+ input.setCode("save10"); // Mixed case to test uppercase conversion
+ input.setDiscountType(PromoCode.DiscountType.FIXED_AMOUNT);
+ input.setDiscountValue(new BigDecimal("15.00"));
+ input.setActive(false);
+
+ when(promoCodeRepository.findByCode("SAVE10")).thenReturn(Optional.of(promoCode));
+ when(promoCodeRepository.save(any(PromoCode.class))).thenAnswer(invocation -> invocation.getArgument(0));
+
+ PromoCode result = promoCodeService.createOrUpdatePromoCode(input);
+
+ assertEquals("promo1", result.getId()); // Preserves existing ID
+ assertEquals("SAVE10", result.getCode());
+ assertEquals(PromoCode.DiscountType.FIXED_AMOUNT, result.getDiscountType());
+ assertEquals(new BigDecimal("15.00"), result.getDiscountValue());
+ assertFalse(result.isActive());
+ verify(promoCodeRepository).findByCode("SAVE10");
+ verify(promoCodeRepository).save(input);
+ }
+
+ @Test
+ void findByCode_existingCode_returnsPromoCode() {
+ when(promoCodeRepository.findByCode("SAVE10")).thenReturn(Optional.of(promoCode));
+
+ Optional result = promoCodeService.findByCode("save10"); // Mixed case
+
+ assertTrue(result.isPresent());
+ assertEquals(promoCode, result.get());
+ verify(promoCodeRepository).findByCode("SAVE10");
+ }
+
+ @Test
+ void findByCode_nonExistentCode_returnsEmpty() {
+ when(promoCodeRepository.findByCode("FAKECODE")).thenReturn(Optional.empty());
+
+ Optional result = promoCodeService.findByCode("fakecode");
+
+ assertTrue(result.isEmpty());
+ verify(promoCodeRepository).findByCode("FAKECODE");
+ }
+
+ @Test
+ void findAll_promoCodesExist_returnsList() {
+ List promoCodes = List.of(promoCode);
+ when(promoCodeRepository.findAll()).thenReturn(promoCodes);
+
+ List result = promoCodeService.findAll();
+
+ assertEquals(1, result.size());
+ assertEquals(promoCode, result.get(0));
+ verify(promoCodeRepository).findAll();
+ }
+
+ @Test
+ void findAll_noPromoCodes_returnsEmptyList() {
+ when(promoCodeRepository.findAll()).thenReturn(Collections.emptyList());
+
+ List result = promoCodeService.findAll();
+
+ assertTrue(result.isEmpty());
+ verify(promoCodeRepository).findAll();
+ }
+
+ @Test
+ void deletePromoCode_existingCode_deletesPromoCode() {
+ when(promoCodeRepository.findByCode("SAVE10")).thenReturn(Optional.of(promoCode));
+ doNothing().when(promoCodeRepository).delete(promoCode);
+
+ promoCodeService.deletePromoCode("save10");
+
+ verify(promoCodeRepository).findByCode("SAVE10");
+ verify(promoCodeRepository).delete(promoCode);
+ }
+
+ @Test
+ void deletePromoCode_nonExistentCode_throwsGlobalHandlerException() {
+ when(promoCodeRepository.findByCode("FAKECODE")).thenReturn(Optional.empty());
+
+ GlobalHandlerException ex = assertThrows(GlobalHandlerException.class,
+ () -> promoCodeService.deletePromoCode("fakecode"));
+
+ assertEquals(HttpStatus.NOT_FOUND, ex.getStatus());
+ assertEquals("Promo code not found: fakecode", ex.getMessage());
+ verify(promoCodeRepository).findByCode("FAKECODE");
+ verify(promoCodeRepository, never()).delete(any());
+ }
+
+ @Test
+ void getActivePromoCode_activePromoCode_returnsPromoCode() {
+ when(promoCodeRepository.findByCode("SAVE10")).thenReturn(Optional.of(promoCode));
+
+ Optional result = promoCodeService.getActivePromoCode("save10");
+
+ assertTrue(result.isPresent());
+ assertEquals(promoCode, result.get());
+ verify(promoCodeRepository).findByCode("SAVE10");
+ }
+
+ @Test
+ void getActivePromoCode_inactivePromoCode_returnsEmpty() {
+ PromoCode inactivePromo = new PromoCode();
+ inactivePromo.setCode("INACTIVE");
+ inactivePromo.setActive(false);
+ when(promoCodeRepository.findByCode("INACTIVE")).thenReturn(Optional.of(inactivePromo));
+
+ Optional result = promoCodeService.getActivePromoCode("inactive");
+
+ assertTrue(result.isEmpty());
+ verify(promoCodeRepository).findByCode("INACTIVE");
+ }
+
+ @Test
+ void getActivePromoCode_nonExistentCode_returnsEmpty() {
+ when(promoCodeRepository.findByCode("FAKECODE")).thenReturn(Optional.empty());
+
+ Optional result = promoCodeService.getActivePromoCode("fakecode");
+
+ assertTrue(result.isEmpty());
+ verify(promoCodeRepository).findByCode("FAKECODE");
+ }
+}
From 6c207a5d65e5591c93a2ac9675f27290bd3a84b8 Mon Sep 17 00:00:00 2001
From: Abdelrahman Elmeky <65960126+Aelmeky@users.noreply.github.com>
Date: Tue, 13 May 2025 18:10:07 +0300
Subject: [PATCH 24/24] fix: fixing lint and syntax erros
---
src/main/java/cart/config/RabbitMQConfig.java | 8 ++++----
.../cart/controller/PromoCodeController.java | 9 ++++++++-
src/main/java/cart/model/Cart.java | 1 -
src/main/java/cart/service/CartService.java | 19 ++++++++++++-------
4 files changed, 24 insertions(+), 13 deletions(-)
diff --git a/src/main/java/cart/config/RabbitMQConfig.java b/src/main/java/cart/config/RabbitMQConfig.java
index 79b49e1..a47a570 100644
--- a/src/main/java/cart/config/RabbitMQConfig.java
+++ b/src/main/java/cart/config/RabbitMQConfig.java
@@ -1,8 +1,8 @@
package cart.config;
-import org.springframework.amqp.core.Binding;
-import org.springframework.amqp.core.BindingBuilder;
-import org.springframework.amqp.core.Queue;
+//import org.springframework.amqp.core.Binding;
+//import org.springframework.amqp.core.BindingBuilder;
+//import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
@@ -47,4 +47,4 @@ Binding orderBinding(Queue orderQueue, TopicExchange cartEventsExchange) {
// You might also need a MessageConverter bean (e.g., Jackson2JsonMessageConverter)
// if you haven't configured one globally, to ensure your CheckoutEvent object
// is serialized correctly (usually auto-configured by Spring Boot).
-}
\ No newline at end of file
+}
diff --git a/src/main/java/cart/controller/PromoCodeController.java b/src/main/java/cart/controller/PromoCodeController.java
index ca51ad8..e2e280d 100644
--- a/src/main/java/cart/controller/PromoCodeController.java
+++ b/src/main/java/cart/controller/PromoCodeController.java
@@ -9,7 +9,14 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.*;
+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.DeleteMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
import java.util.List;
diff --git a/src/main/java/cart/model/Cart.java b/src/main/java/cart/model/Cart.java
index b073a4f..4c52b18 100644
--- a/src/main/java/cart/model/Cart.java
+++ b/src/main/java/cart/model/Cart.java
@@ -5,7 +5,6 @@
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
-import org.springframework.data.annotation.Transient;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
diff --git a/src/main/java/cart/service/CartService.java b/src/main/java/cart/service/CartService.java
index 4bd43e6..15a4413 100644
--- a/src/main/java/cart/service/CartService.java
+++ b/src/main/java/cart/service/CartService.java
@@ -4,18 +4,19 @@
import cart.model.Cart;
import cart.model.CartItem;
import cart.model.OrderRequest;
+import cart.model.PromoCode;
import cart.repository.CartRepository;
-import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
-import org.springframework.web.client.RestTemplate;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.NoSuchElementException;
+import java.util.Optional;
import java.util.UUID;
@Service
@@ -31,7 +32,9 @@ public class CartService {
@Value("${rabbitmq.routing.key.checkout}")
private String checkoutRoutingKey;
- public CartService(CartRepository cartRepository, RabbitTemplate rabbitTemplate, PromoCodeService promoCodeService) {
+ public CartService(final CartRepository cartRepository,
+ final RabbitTemplate rabbitTemplate,
+ final PromoCodeService promoCodeService) {
this.cartRepository = cartRepository;
this.rabbitTemplate = rabbitTemplate;
this.promoCodeService = promoCodeService;
@@ -65,7 +68,9 @@ public Cart addItemToCart(final String customerId, final CartItem newItem) {
}
public Cart updateItemQuantity(final String customerId, final String productId, final int quantity) {
- log.debug("Entering updateItemQuantity with customerId: {}, productId: {}, quantity: {}", customerId, productId, quantity);
+ log.debug("Entering updateItemQuantity with customerId:"
+ + " {}, productId: {}, quantity: {}", customerId,
+ productId, quantity);
CartCommand command = new UpdateQuantityCommand(this, customerId, productId, quantity);
return command.execute();
}
@@ -185,7 +190,7 @@ public Cart saveCart(final Cart cart) {
return cartRepository.save(cart);
}
- private void recalculateCartTotals(Cart cart) {
+ private void recalculateCartTotals(final Cart cart) {
log.debug("Recalculating totals for cartId: {}", cart.getId());
BigDecimal subTotal = calculateSubTotal(cart);
@@ -235,7 +240,7 @@ private void recalculateCartTotals(Cart cart) {
cart.getId(), cart.getSubTotal(), cart.getDiscountAmount(), cart.getTotalPrice());
}
- private BigDecimal calculateSubTotal(Cart cart) {
+ private BigDecimal calculateSubTotal(final Cart cart) {
if (cart.getItems() == null) {
return BigDecimal.ZERO;
}
@@ -285,4 +290,4 @@ public Cart checkoutCart(final String customerId) {
throw new RuntimeException("Checkout process failed: Could not publish event.", e);
}
}
-}
\ No newline at end of file
+}