FulfillmentService.java
package com.rbctcsworld.ecommerce.fulfillment;
import com.rbctcsworld.ecommerce.audit.AuditEvent;
import com.rbctcsworld.ecommerce.common.exception.NotFoundException;
import com.rbctcsworld.ecommerce.order.CustomerOrder;
import com.rbctcsworld.ecommerce.order.OrderDtos.OrderResponse;
import com.rbctcsworld.ecommerce.order.OrderDtos.TrackingResponse;
import com.rbctcsworld.ecommerce.order.OrderRepository;
import com.rbctcsworld.ecommerce.order.OrderStatus;
import com.rbctcsworld.ecommerce.notification.OrderNotificationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.security.SecureRandom;
/**
* Warehouse side (ADMIN): ship a PAID order, mark a SHIPPED order delivered.
* Plus the PUBLIC tracking lookup by tracking number.
*/
@Service
@Transactional
public class FulfillmentService {
private static final SecureRandom RANDOM = new SecureRandom();
private final OrderRepository orders;
private final ApplicationEventPublisher events;
public FulfillmentService(OrderRepository orders, ApplicationEventPublisher events) {
this.orders = orders;
this.events = events;
}
/** 200 SHIPPED | 404 | 409 unless PAID (an unpaid or cancelled order must never leave the warehouse). */
public OrderResponse ship(Long orderId, String carrier) {
CustomerOrder order = order(orderId);
order.assertCanMoveTo(OrderStatus.SHIPPED);
String from = order.getStatus();
order.ship(carrier, newTrackingNumber(carrier));
events.publishEvent(AuditEvent.orderStatus(order.getId(), from, order.getStatus()));
events.publishEvent(OrderNotificationEvent.of(order.getId(), OrderNotificationEvent.Type.ORDER_SHIPPED));
return OrderResponse.from(order);
}
/** 200 DELIVERED | 404 | 409 unless SHIPPED. */
public OrderResponse deliver(Long orderId) {
CustomerOrder order = order(orderId);
String from = order.getStatus();
order.deliver();
events.publishEvent(AuditEvent.orderStatus(order.getId(), from, order.getStatus()));
events.publishEvent(OrderNotificationEvent.of(order.getId(), OrderNotificationEvent.Type.ORDER_DELIVERED));
return OrderResponse.from(order);
}
/** Public: anyone with the tracking number sees status + timeline, but no personal or price data. */
@Transactional(readOnly = true)
public TrackingResponse track(String trackingNumber) {
return orders.findByTrackingNumber(trackingNumber.trim().toUpperCase())
.map(TrackingResponse::from)
.orElseThrow(() -> new NotFoundException("Tracking number not found"));
}
private CustomerOrder order(Long id) {
return orders.findById(id).orElseThrow(() -> new NotFoundException("Order not found: " + id));
}
/** e.g. UPS-482910384756 (carrier + 12 random digits, unique). */
String newTrackingNumber(String carrier) {
String tn;
do {
StringBuilder sb = new StringBuilder(carrier).append('-');
for (int i = 0; i < 12; i++) sb.append(RANDOM.nextInt(10));
tn = sb.toString();
} while (orders.existsByTrackingNumber(tn));
return tn;
}
}