ReturnService.java

1
package com.rbctcsworld.ecommerce.returns;
2
3
import com.rbctcsworld.ecommerce.audit.AuditEvent;
4
import com.rbctcsworld.ecommerce.common.exception.BusinessRuleException;
5
import com.rbctcsworld.ecommerce.common.exception.ConflictException;
6
import com.rbctcsworld.ecommerce.common.exception.NotFoundException;
7
import com.rbctcsworld.ecommerce.inventory.InventoryService;
8
import com.rbctcsworld.ecommerce.inventory.StockMovement;
9
import com.rbctcsworld.ecommerce.order.CustomerOrder;
10
import com.rbctcsworld.ecommerce.order.OrderItem;
11
import com.rbctcsworld.ecommerce.order.OrderRepository;
12
import com.rbctcsworld.ecommerce.order.OrderService;
13
import com.rbctcsworld.ecommerce.order.OrderStatus;
14
import com.rbctcsworld.ecommerce.payment.PaymentService;
15
import org.springframework.beans.factory.annotation.Value;
16
import com.rbctcsworld.ecommerce.notification.OrderNotificationEvent;
17
import org.springframework.context.ApplicationEventPublisher;
18
import org.springframework.stereotype.Service;
19
import org.springframework.transaction.annotation.Transactional;
20
21
import java.math.BigDecimal;
22
import java.time.Clock;
23
import java.time.LocalDateTime;
24
import java.util.Comparator;
25
import java.util.List;
26
27
/**
28
 * Customer: request a return for a DELIVERED order within the return window (one request per order).
29
 * Admin:    approve (refund per ReturnPolicy + optional restock, order -> RETURNED)
30
 *           or reject (order goes back to DELIVERED).
31
 */
32
@Service
33
@Transactional
34
public class ReturnService {
35
36
    private final ReturnRequestRepository returns;
37
    private final OrderRepository orders;
38
    private final OrderService orderService;
39
    private final InventoryService inventory;
40
    private final PaymentService payments;
41
    private final Clock clock;
42
    private final int windowDays;
43
    private final ApplicationEventPublisher events;
44
45
    public ReturnService(ReturnRequestRepository returns, OrderRepository orders, OrderService orderService,
46
                         InventoryService inventory, PaymentService payments, Clock clock,
47
                         @Value("${app.returns.window-days}") int windowDays, ApplicationEventPublisher events) {
48
        this.returns = returns;
49
        this.orders = orders;
50
        this.orderService = orderService;
51
        this.inventory = inventory;
52
        this.payments = payments;
53
        this.clock = clock;
54
        this.windowDays = windowDays;
55
        this.events = events;
56
    }
57
58
    /** 201 | 400 window closed | 404 not mine | 409 not delivered / already requested */
59
    public ReturnRequest request(String email, Long orderId, String reason, String comment) {
60
        CustomerOrder order = orderService.owned(email, orderId);
61 1 1. request : removed conditional - replaced equality check with false → KILLED
        if (returns.existsByOrderId(order.getId())) {
62
            throw new ConflictException("A return was already requested for order " + order.getOrderNumber());
63
        }
64 1 1. request : removed call to com/rbctcsworld/ecommerce/order/CustomerOrder::assertCanMoveTo → KILLED
        order.assertCanMoveTo(OrderStatus.RETURN_REQUESTED);          // only DELIVERED orders
65
        LocalDateTime deadline = order.getDeliveredAt().plusDays(windowDays);
66 1 1. request : removed conditional - replaced equality check with false → KILLED
        if (LocalDateTime.now(clock).isAfter(deadline)) {
67
            throw new BusinessRuleException("Return window of " + windowDays + " days has closed for order "
68
                    + order.getOrderNumber());
69
        }
70
        String from = order.getStatus();
71 1 1. request : removed call to com/rbctcsworld/ecommerce/order/CustomerOrder::transitionTo → KILLED
        order.transitionTo(OrderStatus.RETURN_REQUESTED, "Return requested: " + reason);
72 1 1. request : removed call to org/springframework/context/ApplicationEventPublisher::publishEvent → KILLED
        events.publishEvent(AuditEvent.orderStatus(order.getId(), from, order.getStatus()));
73 1 1. request : replaced return value with null for com/rbctcsworld/ecommerce/returns/ReturnService::request → KILLED
        return returns.save(new ReturnRequest(order.getId(), reason, comment));
74
    }
75
76
    public ReturnRequest approve(Long returnId, String note) {
77
        ReturnRequest r = pending(returnId);
78
        CustomerOrder order = orders.findById(r.getOrderId()).orElseThrow();
79
        boolean restock = ReturnPolicy.restock(r.getReason());
80
        BigDecimal refund = ReturnPolicy.refundAmount(r.getReason(), order);
81
82 1 1. approve : removed conditional - replaced equality check with false → KILLED
        if (restock) {
83
            order.getItems().stream()
84
                    .sorted(Comparator.comparing(OrderItem::getProductId))
85 2 1. lambda$approve$0 : removed call to com/rbctcsworld/ecommerce/inventory/InventoryService::release → KILLED
2. approve : removed call to java/util/stream/Stream::forEach → KILLED
                    .forEach(i -> inventory.release(i.getProductId(), i.getQuantity(), order.getId(),
86
                            StockMovement.RETURN_RESTOCK));
87
        }
88
        payments.refund(order.getId(), refund);
89
        String from = order.getStatus();
90 1 1. approve : removed call to com/rbctcsworld/ecommerce/order/CustomerOrder::transitionTo → KILLED
        order.transitionTo(OrderStatus.RETURNED, "Return approved, refunded " + refund);
91 1 1. approve : removed call to org/springframework/context/ApplicationEventPublisher::publishEvent → KILLED
        events.publishEvent(AuditEvent.orderStatus(order.getId(), from, order.getStatus()));
92 1 1. approve : removed call to com/rbctcsworld/ecommerce/returns/ReturnRequest::approve → KILLED
        r.approve(refund, restock, note);
93 1 1. approve : removed call to org/springframework/context/ApplicationEventPublisher::publishEvent → KILLED
        events.publishEvent(new OrderNotificationEvent(order.getId(), OrderNotificationEvent.Type.RETURN_APPROVED, refund));
94 1 1. approve : replaced return value with null for com/rbctcsworld/ecommerce/returns/ReturnService::approve → KILLED
        return r;
95
    }
96
97
    public ReturnRequest reject(Long returnId, String note) {
98
        ReturnRequest r = pending(returnId);
99
        CustomerOrder order = orders.findById(r.getOrderId()).orElseThrow();
100
        String from = order.getStatus();
101 2 1. reject : removed conditional - replaced equality check with false → KILLED
2. reject : removed call to com/rbctcsworld/ecommerce/order/CustomerOrder::transitionTo → KILLED
        order.transitionTo(OrderStatus.DELIVERED, "Return rejected" + (note == null ? "" : ": " + note));
102 1 1. reject : removed call to org/springframework/context/ApplicationEventPublisher::publishEvent → KILLED
        events.publishEvent(AuditEvent.orderStatus(order.getId(), from, order.getStatus()));
103 1 1. reject : removed call to com/rbctcsworld/ecommerce/returns/ReturnRequest::reject → KILLED
        r.reject(note);
104 1 1. reject : removed call to org/springframework/context/ApplicationEventPublisher::publishEvent → KILLED
        events.publishEvent(OrderNotificationEvent.of(order.getId(), OrderNotificationEvent.Type.RETURN_REJECTED));
105 1 1. reject : replaced return value with null for com/rbctcsworld/ecommerce/returns/ReturnService::reject → KILLED
        return r;
106
    }
107
108
    @Transactional(readOnly = true)
109
    public List<ReturnRequest> list(String status) {
110 3 1. list : removed conditional - replaced equality check with false → NO_COVERAGE
2. list : replaced return value with Collections.emptyList for com/rbctcsworld/ecommerce/returns/ReturnService::list → NO_COVERAGE
3. list : removed conditional - replaced equality check with false → NO_COVERAGE
        return status == null || status.isBlank()
111
                ? returns.findAllByOrderByIdAsc()
112
                : returns.findByStatusOrderByIdAsc(status.trim().toUpperCase());
113
    }
114
115
    @Transactional(readOnly = true)
116
    public ReturnRequest forOrder(String email, Long orderId) {
117
        CustomerOrder order = orderService.owned(email, orderId);
118 1 1. forOrder : replaced return value with null for com/rbctcsworld/ecommerce/returns/ReturnService::forOrder → NO_COVERAGE
        return returns.findByOrderId(order.getId())
119 1 1. lambda$forOrder$1 : replaced return value with null for com/rbctcsworld/ecommerce/returns/ReturnService::lambda$forOrder$1 → NO_COVERAGE
                .orElseThrow(() -> new NotFoundException("No return for order " + order.getOrderNumber()));
120
    }
121
122
    private ReturnRequest pending(Long id) {
123 1 1. lambda$pending$2 : replaced return value with null for com/rbctcsworld/ecommerce/returns/ReturnService::lambda$pending$2 → NO_COVERAGE
        ReturnRequest r = returns.findById(id).orElseThrow(() -> new NotFoundException("Return not found: " + id));
124 1 1. pending : removed conditional - replaced equality check with false → KILLED
        if (!ReturnRequest.REQUESTED.equals(r.getStatus())) {
125
            throw new ConflictException("Return " + id + " is already " + r.getStatus());
126
        }
127 1 1. pending : replaced return value with null for com/rbctcsworld/ecommerce/returns/ReturnService::pending → KILLED
        return r;
128
    }
129
}

Mutations

61

1.1
Location : request
Killed by : com.rbctcsworld.ecommerce.returns.ReturnServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.returns.ReturnServiceTest]/[method:onlyOneReturnPerOrder()]
removed conditional - replaced equality check with false → KILLED

64

1.1
Location : request
Killed by : com.rbctcsworld.ecommerce.returns.ReturnServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.returns.ReturnServiceTest]/[method:notYetDeliveredCannotBeReturned()]
removed call to com/rbctcsworld/ecommerce/order/CustomerOrder::assertCanMoveTo → KILLED

66

1.1
Location : request
Killed by : com.rbctcsworld.ecommerce.returns.ReturnServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.returns.ReturnServiceTest]/[method:oneSecondAfterTheWindowIsRejected()]
removed conditional - replaced equality check with false → KILLED

71

1.1
Location : request
Killed by : com.rbctcsworld.ecommerce.returns.ReturnServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.returns.ReturnServiceTest]/[method:lastMomentOfTheWindowIsAccepted()]
removed call to com/rbctcsworld/ecommerce/order/CustomerOrder::transitionTo → KILLED

72

1.1
Location : request
Killed by : com.rbctcsworld.ecommerce.returns.ReturnServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.returns.ReturnServiceTest]/[method:lastMomentOfTheWindowIsAccepted()]
removed call to org/springframework/context/ApplicationEventPublisher::publishEvent → KILLED

73

1.1
Location : request
Killed by : com.rbctcsworld.ecommerce.returns.ReturnServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.returns.ReturnServiceTest]/[method:lastMomentOfTheWindowIsAccepted()]
replaced return value with null for com/rbctcsworld/ecommerce/returns/ReturnService::request → KILLED

82

1.1
Location : approve
Killed by : com.rbctcsworld.ecommerce.returns.ReturnServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.returns.ReturnServiceTest]/[test-template:approvalFollowsTheDecisionTable(java.lang.String, java.lang.String, boolean)]/[test-template-invocation:#3]
removed conditional - replaced equality check with false → KILLED

85

1.1
Location : lambda$approve$0
Killed by : com.rbctcsworld.ecommerce.returns.ReturnServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.returns.ReturnServiceTest]/[test-template:approvalFollowsTheDecisionTable(java.lang.String, java.lang.String, boolean)]/[test-template-invocation:#3]
removed call to com/rbctcsworld/ecommerce/inventory/InventoryService::release → KILLED

2.2
Location : approve
Killed by : com.rbctcsworld.ecommerce.returns.ReturnServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.returns.ReturnServiceTest]/[test-template:approvalFollowsTheDecisionTable(java.lang.String, java.lang.String, boolean)]/[test-template-invocation:#3]
removed call to java/util/stream/Stream::forEach → KILLED

90

1.1
Location : approve
Killed by : com.rbctcsworld.ecommerce.returns.ReturnServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.returns.ReturnServiceTest]/[test-template:approvalFollowsTheDecisionTable(java.lang.String, java.lang.String, boolean)]/[test-template-invocation:#3]
removed call to com/rbctcsworld/ecommerce/order/CustomerOrder::transitionTo → KILLED

91

1.1
Location : approve
Killed by : com.rbctcsworld.ecommerce.returns.ReturnServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.returns.ReturnServiceTest]/[test-template:approvalFollowsTheDecisionTable(java.lang.String, java.lang.String, boolean)]/[test-template-invocation:#3]
removed call to org/springframework/context/ApplicationEventPublisher::publishEvent → KILLED

92

1.1
Location : approve
Killed by : com.rbctcsworld.ecommerce.returns.ReturnServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.returns.ReturnServiceTest]/[test-template:approvalFollowsTheDecisionTable(java.lang.String, java.lang.String, boolean)]/[test-template-invocation:#3]
removed call to com/rbctcsworld/ecommerce/returns/ReturnRequest::approve → KILLED

93

1.1
Location : approve
Killed by : com.rbctcsworld.ecommerce.returns.ReturnServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.returns.ReturnServiceTest]/[test-template:approvalFollowsTheDecisionTable(java.lang.String, java.lang.String, boolean)]/[test-template-invocation:#3]
removed call to org/springframework/context/ApplicationEventPublisher::publishEvent → KILLED

94

1.1
Location : approve
Killed by : com.rbctcsworld.ecommerce.returns.ReturnServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.returns.ReturnServiceTest]/[test-template:approvalFollowsTheDecisionTable(java.lang.String, java.lang.String, boolean)]/[test-template-invocation:#3]
replaced return value with null for com/rbctcsworld/ecommerce/returns/ReturnService::approve → KILLED

101

1.1
Location : reject
Killed by : com.rbctcsworld.ecommerce.returns.ReturnServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.returns.ReturnServiceTest]/[method:rejectionNoteOnTheOrderTimeline()]
removed conditional - replaced equality check with false → KILLED

2.2
Location : reject
Killed by : com.rbctcsworld.ecommerce.returns.ReturnServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.returns.ReturnServiceTest]/[method:rejectionPutsOrderBackToDeliveredWithoutRefund()]
removed call to com/rbctcsworld/ecommerce/order/CustomerOrder::transitionTo → KILLED

102

1.1
Location : reject
Killed by : com.rbctcsworld.ecommerce.returns.ReturnServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.returns.ReturnServiceTest]/[method:rejectionPutsOrderBackToDeliveredWithoutRefund()]
removed call to org/springframework/context/ApplicationEventPublisher::publishEvent → KILLED

103

1.1
Location : reject
Killed by : com.rbctcsworld.ecommerce.returns.ReturnServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.returns.ReturnServiceTest]/[method:rejectionPutsOrderBackToDeliveredWithoutRefund()]
removed call to com/rbctcsworld/ecommerce/returns/ReturnRequest::reject → KILLED

104

1.1
Location : reject
Killed by : com.rbctcsworld.ecommerce.returns.ReturnServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.returns.ReturnServiceTest]/[method:rejectionPutsOrderBackToDeliveredWithoutRefund()]
removed call to org/springframework/context/ApplicationEventPublisher::publishEvent → KILLED

105

1.1
Location : reject
Killed by : com.rbctcsworld.ecommerce.returns.ReturnServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.returns.ReturnServiceTest]/[method:rejectionPutsOrderBackToDeliveredWithoutRefund()]
replaced return value with null for com/rbctcsworld/ecommerce/returns/ReturnService::reject → KILLED

110

1.1
Location : list
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

2.2
Location : list
Killed by : none
replaced return value with Collections.emptyList for com/rbctcsworld/ecommerce/returns/ReturnService::list → NO_COVERAGE

3.3
Location : list
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

118

1.1
Location : forOrder
Killed by : none
replaced return value with null for com/rbctcsworld/ecommerce/returns/ReturnService::forOrder → NO_COVERAGE

119

1.1
Location : lambda$forOrder$1
Killed by : none
replaced return value with null for com/rbctcsworld/ecommerce/returns/ReturnService::lambda$forOrder$1 → NO_COVERAGE

123

1.1
Location : lambda$pending$2
Killed by : none
replaced return value with null for com/rbctcsworld/ecommerce/returns/ReturnService::lambda$pending$2 → NO_COVERAGE

124

1.1
Location : pending
Killed by : com.rbctcsworld.ecommerce.returns.ReturnServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.returns.ReturnServiceTest]/[method:resolvedReturnCannotBeResolvedAgain()]
removed conditional - replaced equality check with false → KILLED

127

1.1
Location : pending
Killed by : com.rbctcsworld.ecommerce.returns.ReturnServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.returns.ReturnServiceTest]/[method:rejectionPutsOrderBackToDeliveredWithoutRefund()]
replaced return value with null for com/rbctcsworld/ecommerce/returns/ReturnService::pending → KILLED

Active mutators

Tests examined


Report generated by PIT 1.30.0 support