| 1 | package com.rbctcsworld.ecommerce.wishlist; | |
| 2 | ||
| 3 | import com.rbctcsworld.ecommerce.auth.AppUser; | |
| 4 | import com.rbctcsworld.ecommerce.auth.UserRepository; | |
| 5 | import com.rbctcsworld.ecommerce.cart.CartDtos.CartResponse; | |
| 6 | import com.rbctcsworld.ecommerce.cart.CartService; | |
| 7 | import com.rbctcsworld.ecommerce.common.exception.BusinessRuleException; | |
| 8 | import com.rbctcsworld.ecommerce.common.exception.NotFoundException; | |
| 9 | import com.rbctcsworld.ecommerce.product.Product; | |
| 10 | import com.rbctcsworld.ecommerce.product.ProductRepository; | |
| 11 | import com.rbctcsworld.ecommerce.wishlist.WishlistDtos.AddResult; | |
| 12 | import com.rbctcsworld.ecommerce.wishlist.WishlistDtos.WishlistLine; | |
| 13 | import com.rbctcsworld.ecommerce.wishlist.WishlistDtos.WishlistResponse; | |
| 14 | import org.springframework.stereotype.Service; | |
| 15 | import org.springframework.transaction.annotation.Transactional; | |
| 16 | ||
| 17 | import java.math.BigDecimal; | |
| 18 | import java.time.Clock; | |
| 19 | import java.time.LocalDateTime; | |
| 20 | import java.util.List; | |
| 21 | ||
| 22 | /** | |
| 23 | * Business rules: | |
| 24 | * 1. Only active products can be added (404 otherwise). | |
| 25 | * 2. Adding the same product twice is idempotent: no duplicate, 200 instead of 201. | |
| 26 | * 3. At most MAX_ITEMS products per wishlist (400). | |
| 27 | * 4. The price at the moment of adding is kept, so the customer sees a price drop. | |
| 28 | * 5. Move to cart = add 1 to the cart (all cart rules apply, e.g. 409 out of stock) and remove from | |
| 29 | * the wishlist - in one transaction: if the cart refuses, the product STAYS on the wishlist. | |
| 30 | * 6. Removing a product that is not on the wishlist gives 404. | |
| 31 | */ | |
| 32 | @Service | |
| 33 | @Transactional | |
| 34 | public class WishlistService { | |
| 35 | ||
| 36 | public static final int MAX_ITEMS = 50; | |
| 37 | ||
| 38 | private final WishlistItemRepository items; | |
| 39 | private final ProductRepository products; | |
| 40 | private final UserRepository users; | |
| 41 | private final CartService cart; | |
| 42 | private final Clock clock; | |
| 43 | ||
| 44 | public WishlistService(WishlistItemRepository items, ProductRepository products, UserRepository users, | |
| 45 | CartService cart, Clock clock) { | |
| 46 | this.items = items; | |
| 47 | this.products = products; | |
| 48 | this.users = users; | |
| 49 | this.cart = cart; | |
| 50 | this.clock = clock; | |
| 51 | } | |
| 52 | ||
| 53 | @Transactional(readOnly = true) | |
| 54 | public WishlistResponse get(String email) { | |
| 55 |
1
1. get : replaced return value with null for com/rbctcsworld/ecommerce/wishlist/WishlistService::get → KILLED |
return build(user(email).getId()); |
| 56 | } | |
| 57 | ||
| 58 | public AddResult add(String email, Long productId) { | |
| 59 | AppUser user = user(email); | |
| 60 | Product product = products.findByIdAndActiveTrue(productId) | |
| 61 |
1
1. lambda$add$0 : replaced return value with null for com/rbctcsworld/ecommerce/wishlist/WishlistService::lambda$add$0 → KILLED |
.orElseThrow(() -> new NotFoundException("Product not found: " + productId)); |
| 62 |
1
1. add : removed conditional - replaced equality check with false → KILLED |
if (items.findByUserIdAndProductId(user.getId(), productId).isPresent()) { |
| 63 |
1
1. add : replaced return value with null for com/rbctcsworld/ecommerce/wishlist/WishlistService::add → KILLED |
return new AddResult(false, build(user.getId())); |
| 64 | } | |
| 65 |
2
1. add : removed conditional - replaced comparison check with false → KILLED 2. add : changed conditional boundary → KILLED |
if (items.countByUserId(user.getId()) >= MAX_ITEMS) { |
| 66 | throw new BusinessRuleException("A wishlist can hold at most " + MAX_ITEMS + " products"); | |
| 67 | } | |
| 68 | items.save(new WishlistItem(user.getId(), product, LocalDateTime.now(clock))); | |
| 69 |
1
1. add : replaced return value with null for com/rbctcsworld/ecommerce/wishlist/WishlistService::add → NO_COVERAGE |
return new AddResult(true, build(user.getId())); |
| 70 | } | |
| 71 | ||
| 72 | public WishlistResponse remove(String email, Long productId) { | |
| 73 | AppUser user = user(email); | |
| 74 |
1
1. remove : removed call to com/rbctcsworld/ecommerce/wishlist/WishlistItemRepository::delete → NO_COVERAGE |
items.delete(owned(user.getId(), productId)); |
| 75 |
1
1. remove : replaced return value with null for com/rbctcsworld/ecommerce/wishlist/WishlistService::remove → NO_COVERAGE |
return build(user.getId()); |
| 76 | } | |
| 77 | ||
| 78 | public CartResponse moveToCart(String email, Long productId) { | |
| 79 | AppUser user = user(email); | |
| 80 | WishlistItem item = owned(user.getId(), productId); | |
| 81 | CartResponse result = cart.addItem(email, productId, 1); // throws -> rollback, item stays | |
| 82 |
1
1. moveToCart : removed call to com/rbctcsworld/ecommerce/wishlist/WishlistItemRepository::delete → KILLED |
items.delete(item); |
| 83 |
1
1. moveToCart : replaced return value with null for com/rbctcsworld/ecommerce/wishlist/WishlistService::moveToCart → KILLED |
return result; |
| 84 | } | |
| 85 | ||
| 86 | private WishlistItem owned(Long userId, Long productId) { | |
| 87 |
1
1. owned : replaced return value with null for com/rbctcsworld/ecommerce/wishlist/WishlistService::owned → KILLED |
return items.findByUserIdAndProductId(userId, productId) |
| 88 |
1
1. lambda$owned$1 : replaced return value with null for com/rbctcsworld/ecommerce/wishlist/WishlistService::lambda$owned$1 → KILLED |
.orElseThrow(() -> new NotFoundException("Product " + productId + " is not on your wishlist")); |
| 89 | } | |
| 90 | ||
| 91 | private WishlistResponse build(Long userId) { | |
| 92 | List<WishlistLine> lines = items.findByUserIdOrderByIdDesc(userId).stream().map(WishlistService::toLine).toList(); | |
| 93 |
1
1. build : replaced return value with null for com/rbctcsworld/ecommerce/wishlist/WishlistService::build → KILLED |
return new WishlistResponse(lines, lines.size()); |
| 94 | } | |
| 95 | ||
| 96 | static WishlistLine toLine(WishlistItem i) { | |
| 97 | Product p = i.getProduct(); | |
| 98 | BigDecimal drop = i.getPriceWhenAdded().subtract(p.currentPrice()); | |
| 99 |
2
1. toLine : changed conditional boundary → SURVIVED 2. toLine : removed conditional - replaced comparison check with false → KILLED |
if (drop.signum() < 0) drop = BigDecimal.ZERO; |
| 100 |
1
1. toLine : replaced return value with null for com/rbctcsworld/ecommerce/wishlist/WishlistService::toLine → KILLED |
return new WishlistLine(p.getId(), p.getSku(), p.displayName(), i.getPriceWhenAdded(), p.currentPrice(), |
| 101 |
3
1. toLine : removed conditional - replaced equality check with false → KILLED 2. toLine : removed conditional - replaced comparison check with false → KILLED 3. toLine : changed conditional boundary → KILLED |
drop.setScale(2), p.isActive() && p.getStock() > 0, p.getRatingAverage(), p.getRatingCount(), |
| 102 | i.getCreatedAt()); | |
| 103 | } | |
| 104 | ||
| 105 | private AppUser user(String email) { | |
| 106 |
2
1. lambda$user$2 : replaced return value with null for com/rbctcsworld/ecommerce/wishlist/WishlistService::lambda$user$2 → NO_COVERAGE 2. user : replaced return value with null for com/rbctcsworld/ecommerce/wishlist/WishlistService::user → KILLED |
return users.findByEmail(email).orElseThrow(() -> new NotFoundException("User not found")); |
| 107 | } | |
| 108 | } | |
Mutations | ||
| 55 |
1.1 |
|
| 61 |
1.1 |
|
| 62 |
1.1 |
|
| 63 |
1.1 |
|
| 65 |
1.1 2.2 |
|
| 69 |
1.1 |
|
| 74 |
1.1 |
|
| 75 |
1.1 |
|
| 82 |
1.1 |
|
| 83 |
1.1 |
|
| 87 |
1.1 |
|
| 88 |
1.1 |
|
| 93 |
1.1 |
|
| 99 |
1.1 2.2 |
|
| 100 |
1.1 |
|
| 101 |
1.1 2.2 3.3 |
|
| 106 |
1.1 2.2 |