diff --git a/pom.xml b/pom.xml
index 337a47c..660f2f7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -55,6 +55,11 @@
2.8.5
+
+ org.springframework.boot
+ spring-boot-starter-amqp
+
+
net.logstash.logback
@@ -87,7 +92,7 @@
org.mockito
mockito-core
- 5.5.0
+ 5.12.0
test
diff --git a/src/main/java/cart/config/RabbitMQConfig.java b/src/main/java/cart/config/RabbitMQConfig.java
new file mode 100644
index 0000000..a47a570
--- /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).
+}
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..e2e280d
--- /dev/null
+++ b/src/main/java/cart/controller/PromoCodeController.java
@@ -0,0 +1,72 @@
+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.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;
+
+@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..4c52b18 100644
--- a/src/main/java/cart/model/Cart.java
+++ b/src/main/java/cart/model/Cart.java
@@ -8,6 +8,7 @@
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 +29,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..15a4413 100644
--- a/src/main/java/cart/service/CartService.java
+++ b/src/main/java/cart/service/CartService.java
@@ -4,111 +4,118 @@
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
-@RequiredArgsConstructor
@Slf4j
public class CartService {
private final CartRepository cartRepository;
+ private final RabbitTemplate rabbitTemplate;
+ private final PromoCodeService promoCodeService;
- private final RestTemplate restTemplate;
-
- @Value("${order.service.url}")
- private String orderServiceUrl;
+ @Value("${rabbitmq.exchange.name}")
+ private String exchangeName;
+ @Value("${rabbitmq.routing.key.checkout}")
+ private String checkoutRoutingKey;
+ public CartService(final CartRepository cartRepository,
+ final RabbitTemplate rabbitTemplate,
+ final PromoCodeService promoCodeService) {
+ this.cartRepository = cartRepository;
+ this.rabbitTemplate = rabbitTemplate;
+ this.promoCodeService = promoCodeService;
+ }
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);
- 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);
@@ -117,68 +124,170 @@ 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;
}
- 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);
- 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 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(final 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());
+ 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) {
+ 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(final 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.");
+ }
+
+ 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..ac948a4 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
diff --git a/src/test/java/service/CartServiceTest.java b/src/test/java/service/CartServiceTest.java
index d7670d3..da519e5 100644
--- a/src/test/java/service/CartServiceTest.java
+++ b/src/test/java/service/CartServiceTest.java
@@ -3,26 +3,30 @@
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.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;
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.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@@ -32,32 +36,66 @@ class CartServiceTest {
private CartRepository cartRepository;
@Mock
- private RestTemplate restTemplate;
+ private RabbitTemplate rabbitTemplate;
+
+ @Mock
+ private PromoCodeService promoCodeService;
@InjectMocks
private CartService cartService;
private Cart cart;
- private CartItem cartItem;
+ private CartItem item1Input;
+ 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 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 expiry, boolean active) {
+ PromoCode promo = new PromoCode();
+ promo.setCode(code.toUpperCase());
+ promo.setDiscountType(type);
+ promo.setDiscountValue(value);
+ promo.setMinimumPurchaseAmount(minPurchase);
+ promo.setExpiryDate(expiry);
+ promo.setActive(active);
+ return promo;
+ }
@BeforeEach
void setUp() {
- // Initialize test data
- cart = new Cart(cartId, customerId, new ArrayList<>(), false);
- cartItem = new CartItem(productId, 1);
+ cart = createNewTestCart(customerId, cartId);
- // Set orderServiceUrl
- ReflectionTestUtils.setField(cartService, "orderServiceUrl", orderServiceUrl);
+ 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.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_returnsCart() {
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
-
+ void createCart_existingCart_returnsCartAndDoesNotSave() {
Cart result = cartService.createCart(customerId);
assertEquals(cart, result);
@@ -66,269 +104,340 @@ void createCart_existingCart_returnsCart() {
}
@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());
+ void createCart_noExistingCart_createsAndSavesNewCartWithZeroTotals() {
+ String newCustId = "newCust456";
+ when(cartRepository.findByCustomerId(eq(newCustId))).thenReturn(Optional.empty());
+ when(cartRepository.save(any(Cart.class))).thenAnswer(invocation -> {
+ Cart newCart = invocation.getArgument(0);
+ if (newCart.getId() == null) newCart.setId(UUID.randomUUID().toString());
+ return newCart;
+ });
+
+ Cart result = cartService.createCart(newCustId);
+
+ assertEquals(newCustId, result.getCustomerId());
+ assertNotNull(result.getId());
assertFalse(result.isArchived());
assertTrue(result.getItems().isEmpty());
- verify(cartRepository).findByCustomerId(customerId);
- verify(cartRepository).save(any(Cart.class));
+ 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(eq(newCustId));
}
@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);
+ void addItemToCart_newItem_addsItemAndRecalculatesTotals() {
+ Cart result = cartService.addItemToCart(customerId, item1Input);
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);
+ 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, times(1)).save(any(Cart.class));
}
@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() {
+ cart.getItems().add(new CartItem(productId1, 1, price1));
- Cart result = cartService.addItemToCart(customerId, newItem);
+ CartItem additionalItem1 = new CartItem(productId1, 2, price1);
+ Cart result = cartService.addItemToCart(customerId, additionalItem1);
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());
+ assertEquals(BigDecimal.ZERO.setScale(2), result.getDiscountAmount());
+ assertEquals(new BigDecimal("31.50").setScale(2), result.getTotalPrice());
+ verify(cartRepository, times(1)).save(any(Cart.class));
}
@Test
- void addItemToCart_cartNotFound_throwsGlobalHandlerException() {
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.empty());
+ void updateItemQuantity_existingItem_updatesAndRecalculates() {
+ cart.getItems().add(new CartItem(productId1, 2, price1));
- 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());
+ Cart result = cartService.updateItemQuantity(customerId, productId1, 5);
+
+ 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, times(1)).save(any(Cart.class));
}
@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_quantityToZero_removesItemAndRecalculates() {
+ cart.getItems().add(new CartItem(productId1, 2, price1));
+ cart.getItems().add(new CartItem(productId2, 1, price2));
- Cart result = cartService.updateItemQuantity(customerId, productId, 5);
+ Cart result = cartService.updateItemQuantity(customerId, productId1, 0);
- assertEquals(5, result.getItems().get(0).getQuantity());
- verify(cartRepository).findByCustomerId(customerId);
- verify(cartRepository).save(cart);
+ 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, times(1)).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 removeItemFromCart_itemExists_removesAndRecalculates() {
+ cart.getItems().add(new CartItem(productId1, 2, price1));
+ cart.getItems().add(new CartItem(productId2, 1, price2));
- Cart result = cartService.updateItemQuantity(customerId, productId, 0);
+ Cart result = cartService.removeItemFromCart(customerId, productId1);
- assertTrue(result.getItems().isEmpty());
- verify(cartRepository).findByCustomerId(customerId);
- verify(cartRepository).save(cart);
+ 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, times(1)).save(any(Cart.class));
}
@Test
- void updateItemQuantity_itemNotFound_throwsGlobalHandlerException() {
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
+ void clearCart_itemsExist_clearsItemsAndResetsTotalsAndPromo() {
+ cart.getItems().add(new CartItem(productId1, 1, price1));
+ cart.setAppliedPromoCode("TESTCODE");
+ cart.setSubTotal(new BigDecimal("10.50"));
+ cart.setDiscountAmount(new BigDecimal("1.00"));
+ cart.setTotalPrice(new BigDecimal("9.50"));
- 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());
+ 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());
}
@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 applyPromoCode_validPercentageCode_calculatesDiscount() {
+ cart.getItems().add(new CartItem(productId1, 2, new BigDecimal("10.00")));
- Cart result = cartService.removeItemFromCart(customerId, productId);
+ String promoCodeStr = "SAVE10";
+ PromoCode promo = createTestPromoCode(promoCodeStr, PromoCode.DiscountType.PERCENTAGE, new BigDecimal("10"), null, null, true);
+ when(promoCodeService.getActivePromoCode(promoCodeStr.toUpperCase())).thenReturn(Optional.of(promo));
- assertTrue(result.getItems().isEmpty());
- verify(cartRepository).findByCustomerId(customerId);
- verify(cartRepository).save(cart);
+ Cart result = cartService.applyPromoCode(customerId, promoCodeStr);
+
+ assertEquals(promoCodeStr.toUpperCase(), result.getAppliedPromoCode());
+ assertEquals(new BigDecimal("20.00").setScale(2), result.getSubTotal());
+ assertEquals(new BigDecimal("2.00").setScale(2), result.getDiscountAmount());
+ assertEquals(new BigDecimal("18.00").setScale(2), result.getTotalPrice());
+ verify(cartRepository, times(1)).save(any(Cart.class));
}
@Test
- void removeItemFromCart_cartNotFound_throwsGlobalHandlerException() {
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.empty());
+ void applyPromoCode_validFixedCode_calculatesDiscount() {
+ cart.getItems().add(new CartItem(productId1, 3, new BigDecimal("10.00")));
- 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());
+ String promoCodeStr = "5OFF";
+ 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(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 deleteCartByCustomerId_cartExists_deletesCart() {
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
+ void applyPromoCode_fixedDiscountExceedsSubtotal_discountCapped() {
+ cart.getItems().add(new CartItem(productId1, 1, new BigDecimal("3.00")));
- cartService.deleteCartByCustomerId(customerId);
+ 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));
- verify(cartRepository).findByCustomerId(customerId);
- verify(cartRepository).delete(cart);
+ 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 deleteCartByCustomerId_cartNotFound_doesNothing() {
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.empty());
+ void applyPromoCode_invalidCode_throwsGlobalHandlerException() {
+ String invalidCode = "FAKECODE";
+ when(promoCodeService.getActivePromoCode(invalidCode.toUpperCase())).thenReturn(Optional.empty());
- cartService.deleteCartByCustomerId(customerId);
+ GlobalHandlerException ex = assertThrows(GlobalHandlerException.class,
+ () -> cartService.applyPromoCode(customerId, invalidCode));
- verify(cartRepository).findByCustomerId(customerId);
- verify(cartRepository, never()).delete(any());
+ assertEquals(HttpStatus.BAD_REQUEST, ex.getStatus());
+ assertTrue(ex.getMessage().contains("Invalid, inactive, or expired promo code"));
+ verify(cartRepository, never()).save(any());
}
@Test
- void getCartByCustomerId_cartExists_returnsCart() {
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.of(cart));
+ void applyPromoCode_expiredCode_removesCodeAndNoDiscount() {
+ cart.getItems().add(new CartItem(productId1, 1, new BigDecimal("100.00")));
- Cart result = cartService.getCartByCustomerId(customerId);
+ String promoCodeStr = "EXPIRED";
+ 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));
- assertEquals(cart, result);
- verify(cartRepository).findByCustomerId(customerId);
+ Cart result = cartService.applyPromoCode(customerId, promoCodeStr);
+
+ 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 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 removePromoCode_codeExists_removesCodeAndResetsDiscount() {
+ cart.getItems().add(new CartItem(productId1, 2, new BigDecimal("10.00")));
+ cart.setAppliedPromoCode("SAVE10");
+ cart.setSubTotal(new BigDecimal("20.00"));
+ cart.setDiscountAmount(new BigDecimal("2.00"));
+ cart.setTotalPrice(new BigDecimal("18.00"));
+
+ Cart result = cartService.removePromoCode(customerId);
+
+ 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));
}
@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_validCartWithPromo_publishesEventAndClearsCart() {
+ cart.getItems().add(new CartItem(productId1, 1, new BigDecimal("100.00")));
+ cart.setAppliedPromoCode("SAVE10");
+ cart.setSubTotal(new BigDecimal("100.00"));
+ cart.setDiscountAmount(new BigDecimal("10.00"));
+ cart.setTotalPrice(new BigDecimal("90.00"));
- cartService.clearCart(customerId);
+ PromoCode promo = createTestPromoCode("SAVE10", PromoCode.DiscountType.PERCENTAGE, new BigDecimal("10"), null, null, true);
+ when(promoCodeService.getActivePromoCode("SAVE10")).thenReturn(Optional.of(promo));
- assertTrue(cart.getItems().isEmpty());
- verify(cartRepository).findByCustomerId(customerId);
- verify(cartRepository).save(cart);
- }
+ doNothing().when(rabbitTemplate).convertAndSend(anyString(), anyString(), any(OrderRequest.class));
- @Test
- void clearCart_cartNotFound_throwsGlobalHandlerException() {
- when(cartRepository.findByCustomerId(customerId)).thenReturn(Optional.empty());
+ Cart result = cartService.checkoutCart(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());
+ 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());
+
+ 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));
}
@Test
- void archiveCart_activeCart_archivesCart() {
+ void checkoutCart_emptyCart_throwsGlobalHandlerException() {
when(cartRepository.findByCustomerIdAndArchived(customerId, false)).thenReturn(Optional.of(cart));
- when(cartRepository.save(any(Cart.class))).thenReturn(cart);
- Cart result = cartService.archiveCart(customerId);
+ GlobalHandlerException ex = assertThrows(GlobalHandlerException.class,
+ () -> cartService.checkoutCart(customerId));
- assertTrue(result.isArchived());
- verify(cartRepository).findByCustomerIdAndArchived(customerId, false);
- verify(cartRepository).save(cart);
+ assertEquals(HttpStatus.BAD_REQUEST, ex.getStatus());
+ assertEquals("Cannot checkout an empty cart.", ex.getMessage());
+ verify(rabbitTemplate, never()).convertAndSend(anyString(), anyString(), any(OrderRequest.class));
}
@Test
- void archiveCart_noActiveCart_throwsNoSuchElementException() {
- when(cartRepository.findByCustomerIdAndArchived(customerId, false)).thenReturn(Optional.empty());
+ void checkoutCart_rabbitMqFails_throwsRuntimeExceptionAndCartNotCleared() {
+ cart.getItems().add(item1Input);
- assertThrows(NoSuchElementException.class, () -> cartService.archiveCart(customerId));
- verify(cartRepository).findByCustomerIdAndArchived(customerId, false);
- verify(cartRepository, never()).save(any());
- }
+ 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));
- @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);
+ doThrow(new RuntimeException("RabbitMQ publish error")).when(rabbitTemplate)
+ .convertAndSend(eq(exchangeName), eq(checkoutRoutingKey), any(OrderRequest.class));
- Cart result = cartService.unarchiveCart(customerId);
+ RuntimeException ex = assertThrows(RuntimeException.class,
+ () -> cartService.checkoutCart(customerId));
- assertFalse(result.isArchived());
- verify(cartRepository).findByCustomerIdAndArchived(customerId, true);
- verify(cartRepository).save(cart);
+ assertTrue(ex.getMessage().contains("Checkout process failed: Could not publish event."));
+ verify(cartRepository, never()).save(any(Cart.class));
+
+ assertEquals(1, cart.getItems().size());
+ assertEquals(new BigDecimal(formattedSubTotal), cart.getSubTotal());
}
@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());
+ void getCartByCustomerId_cartExists_returnsCart() {
+ Cart result = cartService.getCartByCustomerId(customerId);
+ assertEquals(cart, result);
+ verify(cartRepository).findByCustomerId(customerId);
}
@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);
+ void getCartByCustomerId_cartNotFound_throwsGlobalHandlerException() {
+ String nonExistentCustId = "ghost";
+ when(cartRepository.findByCustomerId(eq(nonExistentCustId))).thenReturn(Optional.empty());
- Cart result = cartService.checkoutCart(customerId);
+ 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));
+ }
- assertTrue(result.getItems().isEmpty());
- verify(cartRepository).findByCustomerIdAndArchived(customerId, false);
- verify(restTemplate).postForObject(eq(orderServiceUrl + "/orders"), any(OrderRequest.class), eq(Void.class));
+ @Test
+ void archiveCart_activeCart_archivesCart() {
+ Cart result = cartService.archiveCart(customerId);
+ assertTrue(result.isArchived());
verify(cartRepository).save(cart);
}
@Test
- void checkoutCart_noActiveCart_throwsNoSuchElementException() {
+ void archiveCart_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());
+ assertThrows(NoSuchElementException.class, () -> cartService.archiveCart(customerId));
}
@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"));
+ void unarchiveCart_archivedCart_unarchivesCart() {
+ Cart archivedCart = createNewTestCart(customerId, "archivedCrt");
+ archivedCart.setArchived(true);
+ when(cartRepository.findByCustomerIdAndArchived(customerId, true)).thenReturn(Optional.of(archivedCart));
- 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());
+ Cart result = cartService.unarchiveCart(customerId);
+
+ assertFalse(result.isArchived());
+ verify(cartRepository).save(archivedCart);
}
}
\ No newline at end of file
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");
+ }
+}
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
index a38ed3d..2fb525b 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