| 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 |
|
| 64 |
1.1 |
|
| 66 |
1.1 |
|
| 71 |
1.1 |
|
| 72 |
1.1 |
|
| 73 |
1.1 |
|
| 82 |
1.1 |
|
| 85 |
1.1 2.2 |
|
| 90 |
1.1 |
|
| 91 |
1.1 |
|
| 92 |
1.1 |
|
| 93 |
1.1 |
|
| 94 |
1.1 |
|
| 101 |
1.1 2.2 |
|
| 102 |
1.1 |
|
| 103 |
1.1 |
|
| 104 |
1.1 |
|
| 105 |
1.1 |
|
| 110 |
1.1 2.2 3.3 |
|
| 118 |
1.1 |
|
| 119 |
1.1 |
|
| 123 |
1.1 |
|
| 124 |
1.1 |
|
| 127 |
1.1 |