WishlistService.java

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
Location : get
Killed by : com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest]/[method:wishlistShowsEveryLineAndItsCount()]
replaced return value with null for com/rbctcsworld/ecommerce/wishlist/WishlistService::get → KILLED

61

1.1
Location : lambda$add$0
Killed by : com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest]/[method:inactiveProductCannotBeAdded()]
replaced return value with null for com/rbctcsworld/ecommerce/wishlist/WishlistService::lambda$add$0 → KILLED

62

1.1
Location : add
Killed by : com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest]/[method:addingTwiceIsIdempotent()]
removed conditional - replaced equality check with false → KILLED

63

1.1
Location : add
Killed by : com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest]/[method:addingTwiceIsIdempotent()]
replaced return value with null for com/rbctcsworld/ecommerce/wishlist/WishlistService::add → KILLED

65

1.1
Location : add
Killed by : com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest]/[method:fullWishlistRejectsTheNextProduct()]
removed conditional - replaced comparison check with false → KILLED

2.2
Location : add
Killed by : com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest]/[method:fullWishlistRejectsTheNextProduct()]
changed conditional boundary → KILLED

69

1.1
Location : add
Killed by : none
replaced return value with null for com/rbctcsworld/ecommerce/wishlist/WishlistService::add → NO_COVERAGE

74

1.1
Location : remove
Killed by : none
removed call to com/rbctcsworld/ecommerce/wishlist/WishlistItemRepository::delete → NO_COVERAGE

75

1.1
Location : remove
Killed by : none
replaced return value with null for com/rbctcsworld/ecommerce/wishlist/WishlistService::remove → NO_COVERAGE

82

1.1
Location : moveToCart
Killed by : com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest]/[method:moveToCartRemovesFromWishlist()]
removed call to com/rbctcsworld/ecommerce/wishlist/WishlistItemRepository::delete → KILLED

83

1.1
Location : moveToCart
Killed by : com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest]/[method:moveToCartReturnsTheUpdatedCart()]
replaced return value with null for com/rbctcsworld/ecommerce/wishlist/WishlistService::moveToCart → KILLED

87

1.1
Location : owned
Killed by : com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest]/[method:moveToCartRemovesFromWishlist()]
replaced return value with null for com/rbctcsworld/ecommerce/wishlist/WishlistService::owned → KILLED

88

1.1
Location : lambda$owned$1
Killed by : com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest]/[method:removingSomethingNotOnTheWishlistIs404()]
replaced return value with null for com/rbctcsworld/ecommerce/wishlist/WishlistService::lambda$owned$1 → KILLED

93

1.1
Location : build
Killed by : com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest]/[method:wishlistShowsEveryLineAndItsCount()]
replaced return value with null for com/rbctcsworld/ecommerce/wishlist/WishlistService::build → KILLED

99

1.1
Location : toLine
Killed by : com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest]/[method:priceDropIsShownOnlyWhenThePriceWentDown()]
removed conditional - replaced comparison check with false → KILLED

2.2
Location : toLine
Killed by : none
changed conditional boundary → SURVIVED
Covering tests

100

1.1
Location : toLine
Killed by : com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest]/[method:outOfStockOrInactiveIsNotAvailable()]
replaced return value with null for com/rbctcsworld/ecommerce/wishlist/WishlistService::toLine → KILLED

101

1.1
Location : toLine
Killed by : com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest]/[method:lastUnitInStockIsAvailable()]
removed conditional - replaced equality check with false → KILLED

2.2
Location : toLine
Killed by : com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest]/[method:lastUnitInStockIsAvailable()]
removed conditional - replaced comparison check with false → KILLED

3.3
Location : toLine
Killed by : com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest]/[method:outOfStockOrInactiveIsNotAvailable()]
changed conditional boundary → KILLED

106

1.1
Location : lambda$user$2
Killed by : none
replaced return value with null for com/rbctcsworld/ecommerce/wishlist/WishlistService::lambda$user$2 → NO_COVERAGE

2.2
Location : user
Killed by : com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.wishlist.WishlistServiceTest]/[method:fullWishlistRejectsTheNextProduct()]
replaced return value with null for com/rbctcsworld/ecommerce/wishlist/WishlistService::user → KILLED

Active mutators

Tests examined


Report generated by PIT 1.30.0 support