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> + handleGlobalHandlerException(GlobalHandlerException + ex) { + Map errorResponse = new HashMap<>(); + errorResponse.put("status", ex.getStatus().value()); + errorResponse.put("error", ex.getStatus().getReasonPhrase()); + errorResponse.put("message", ex.getMessage()); + return new ResponseEntity<>(errorResponse, ex.getStatus()); + } +} diff --git a/src/main/java/cart/exception/GlobalHandlerException.java b/src/main/java/cart/exception/GlobalHandlerException.java new file mode 100644 index 0000000..b10ea67 --- /dev/null +++ b/src/main/java/cart/exception/GlobalHandlerException.java @@ -0,0 +1,25 @@ +package cart.exception; + +import org.springframework.http.HttpStatus; + +public class GlobalHandlerException extends + RuntimeException { + + private final HttpStatus status; + + public GlobalHandlerException(HttpStatus status, + String message) { + super(message); + this.status = status; + } + + public GlobalHandlerException(HttpStatus status, + String message, Throwable cause) { + super(message, cause); + this.status = status; + } + + public HttpStatus getStatus() { + return status; + } +} diff --git a/src/main/java/cart/service/CartService.java b/src/main/java/cart/service/CartService.java index 5f52353..edfce98 100644 --- a/src/main/java/cart/service/CartService.java +++ b/src/main/java/cart/service/CartService.java @@ -1,5 +1,6 @@ package cart.service; +import cart.exception.GlobalHandlerException; import cart.model.Cart; import cart.model.CartItem; import cart.model.OrderRequest; @@ -10,7 +11,6 @@ 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; import java.util.NoSuchElementException; @@ -92,7 +92,7 @@ public Cart updateItemQuantity(final String customerId, if (existingItemOpt.isEmpty()) { log.error("Product not found in" + " cart for productId:", productId); - throw new ResponseStatusException( + throw new GlobalHandlerException( HttpStatus.NOT_FOUND, "Product not found in cart"); } @@ -143,7 +143,8 @@ public Cart getCartByCustomerId(final String customerId) { Cart cart = cartRepository.findByCustomerId(customerId) .orElseThrow(() -> { log.error("Cart not found for customerId:", customerId); - return new NoSuchElementException("Cart not found"); + throw new GlobalHandlerException( + HttpStatus.NOT_FOUND, "Cart not found"); }); log.debug("Cart retrieved:", cart); return cart; From e09f575b4b7517c38385b114886a44b6a7d2d895 Mon Sep 17 00:00:00 2001 From: Abdelrahman Elmeky <65960126+Aelmeky@users.noreply.github.com> Date: Thu, 8 May 2025 15:50:17 +0300 Subject: [PATCH 16/24] fix : fixing lint errors --- src/main/java/cart/exception/GlobalExceptionHandler.java | 4 ++-- src/main/java/cart/exception/GlobalHandlerException.java | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/cart/exception/GlobalExceptionHandler.java b/src/main/java/cart/exception/GlobalExceptionHandler.java index cacdeb6..4b03133 100644 --- a/src/main/java/cart/exception/GlobalExceptionHandler.java +++ b/src/main/java/cart/exception/GlobalExceptionHandler.java @@ -13,8 +13,8 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(GlobalHandlerException.class) public ResponseEntity> - handleGlobalHandlerException(GlobalHandlerException - ex) { + handleGlobalHandlerException(final + GlobalHandlerException ex) { Map errorResponse = new HashMap<>(); errorResponse.put("status", ex.getStatus().value()); errorResponse.put("error", ex.getStatus().getReasonPhrase()); diff --git a/src/main/java/cart/exception/GlobalHandlerException.java b/src/main/java/cart/exception/GlobalHandlerException.java index b10ea67..d6b665b 100644 --- a/src/main/java/cart/exception/GlobalHandlerException.java +++ b/src/main/java/cart/exception/GlobalHandlerException.java @@ -7,14 +7,15 @@ public class GlobalHandlerException extends private final HttpStatus status; - public GlobalHandlerException(HttpStatus status, - String message) { + public GlobalHandlerException(final + HttpStatus status, final String message) { super(message); this.status = status; } - public GlobalHandlerException(HttpStatus status, - String message, Throwable cause) { + public GlobalHandlerException(final + HttpStatus status, final String message, + final Throwable cause) { super(message, cause); this.status = status; } From 1e78cd7430602e05f9d31eaf21598d33b9e249ca Mon Sep 17 00:00:00 2001 From: Abdelrahman Elmeky <65960126+Aelmeky@users.noreply.github.com> Date: Thu, 8 May 2025 15:58:00 +0300 Subject: [PATCH 17/24] tests: fixing tests after adding the handler --- src/test/java/service/CartServiceTest.java | 39 +++++++++++++++------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/src/test/java/service/CartServiceTest.java b/src/test/java/service/CartServiceTest.java index 983f455..1e1318f 100644 --- a/src/test/java/service/CartServiceTest.java +++ b/src/test/java/service/CartServiceTest.java @@ -1,5 +1,6 @@ package service; +import cart.exception.GlobalHandlerException; import cart.model.Cart; import cart.model.CartItem; import cart.model.OrderRequest; @@ -46,6 +47,7 @@ class CartServiceTest { @BeforeEach void setUp() throws NoSuchFieldException, IllegalAccessException { + // Initialize test data cart = new Cart(cartId, customerId, new ArrayList<>(), false); cartItem = new CartItem(); cartItem.setProductId(productId); @@ -113,10 +115,13 @@ void addItemToCart_existingItem_updatesQuantity() { } @Test - void addItemToCart_cartNotFound_throwsNoSuchElementException() { + void addItemToCart_cartNotFound_throwsGlobalHandlerException() { when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.empty()); - assertThrows(NoSuchElementException.class, () -> cartService.addItemToCart(customerId, cartItem)); + 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()); } @@ -148,12 +153,13 @@ void updateItemQuantity_quantityZero_removesItem() { } @Test - void updateItemQuantity_itemNotFound_throwsResponseStatusException() { + void updateItemQuantity_itemNotFound_throwsGlobalHandlerException() { when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart)); - ResponseStatusException exception = assertThrows(ResponseStatusException.class, + GlobalHandlerException exception = assertThrows(GlobalHandlerException.class, () -> cartService.updateItemQuantity(customerId, productId, 5)); - assertEquals(HttpStatus.NOT_FOUND, exception.getStatusCode()); + assertEquals(HttpStatus.NOT_FOUND, exception.getStatus()); + assertEquals("Product not found in cart", exception.getMessage()); verify(cartRepository).findByCustomerId(customerId); verify(cartRepository, never()).save(any()); } @@ -172,10 +178,13 @@ void removeItemFromCart_itemExists_removesItem() { } @Test - void removeItemFromCart_cartNotFound_throwsNoSuchElementException() { + void removeItemFromCart_cartNotFound_throwsGlobalHandlerException() { when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.empty()); - assertThrows(NoSuchElementException.class, () -> cartService.removeItemFromCart(customerId, productId)); + 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()); } @@ -211,10 +220,13 @@ void getCartByCustomerId_cartExists_returnsCart() { } @Test - void getCartByCustomerId_cartNotFound_throwsNoSuchElementException() { + void getCartByCustomerId_cartNotFound_throwsGlobalHandlerException() { when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.empty()); - assertThrows(NoSuchElementException.class, () -> cartService.getCartByCustomerId(customerId)); + GlobalHandlerException exception = assertThrows(GlobalHandlerException.class, + () -> cartService.getCartByCustomerId(customerId)); + assertEquals(HttpStatus.NOT_FOUND, exception.getStatus()); + assertEquals("Cart not found", exception.getMessage()); verify(cartRepository).findByCustomerId(customerId); } @@ -232,10 +244,13 @@ void clearCart_cartExists_clearsItems() { } @Test - void clearCart_cartNotFound_throwsNoSuchElementException() { + void clearCart_cartNotFound_throwsGlobalHandlerException() { when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.empty()); - assertThrows(NoSuchElementException.class, () -> cartService.clearCart(customerId)); + 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()); } @@ -322,4 +337,4 @@ void checkoutCart_orderServiceFails_throwsRuntimeException() { verify(restTemplate).postForObject(eq("http://localhost:8080/orders"), any(OrderRequest.class), eq(Void.class)); verify(cartRepository, never()).save(any()); } -} +} \ No newline at end of file From 84a5d71bc729460a55365649f0ddfc87c4186e1f Mon Sep 17 00:00:00 2001 From: Abdelrahman Elmeky <65960126+Aelmeky@users.noreply.github.com> Date: Thu, 8 May 2025 16:10:28 +0300 Subject: [PATCH 18/24] Delete logs/app.log --- logs/app.log | 367 --------------------------------------------------- 1 file changed, 367 deletions(-) delete mode 100644 logs/app.log diff --git a/logs/app.log b/logs/app.log deleted file mode 100644 index 20570ce..0000000 --- a/logs/app.log +++ /dev/null @@ -1,367 +0,0 @@ -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 From 53468506402fb8a842aca3090de4ef14d340deaf Mon Sep 17 00:00:00 2001 From: Abdelrahman Elmeky <65960126+Aelmeky@users.noreply.github.com> Date: Sun, 11 May 2025 22:17:33 +0300 Subject: [PATCH 19/24] feat : adding command design pattern and singlton and updating tests --- pom.xml | 35 ++ src/main/java/cart/CartApplication.java | 2 + .../java/cart/service/AddItemCommand.java | 66 ++++ src/main/java/cart/service/CartCommand.java | 8 + src/main/java/cart/service/CartService.java | 69 +--- .../java/cart/service/RemoveItemCommand.java | 57 +++ .../cart/service/UpdateQuantityCommand.java | 81 +++++ src/test/java/api/CartControllerTests.java | 325 ++++++++++++++++++ src/test/java/service/CartServiceTest.java | 48 ++- 9 files changed, 606 insertions(+), 85 deletions(-) create mode 100644 src/main/java/cart/service/AddItemCommand.java create mode 100644 src/main/java/cart/service/CartCommand.java create mode 100644 src/main/java/cart/service/RemoveItemCommand.java create mode 100644 src/main/java/cart/service/UpdateQuantityCommand.java create mode 100644 src/test/java/api/CartControllerTests.java diff --git a/pom.xml b/pom.xml index 32d7846..337a47c 100644 --- a/pom.xml +++ b/pom.xml @@ -110,8 +110,43 @@ spring-security-test test + + + + com.github.tomakehurst + wiremock-jre8 + 2.35.0 + test + + + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + 4.9.3 + test + + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.github.tomakehurst + wiremock-jre8 + 2.35.0 + test + + 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 +}