CatalogSnapshotService.java
package com.ecommerce.trade.application.service;
import com.ecommerce.trade.application.exception.TradeError;
import com.ecommerce.trade.application.exception.TradeException;
import com.ecommerce.trade.application.model.TradeModels.OrderLineCommand;
import com.ecommerce.trade.application.port.CatalogPort;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Service
public class CatalogSnapshotService {
private static final long MAX_QUANTITY = 1_000_000_000L;
private final CatalogPort catalogPort;
public CatalogSnapshotService(CatalogPort catalogPort) {
this.catalogPort = catalogPort;
}
public ResolvedLine resolveOne(Long productId, Long skuId, long quantity) {
return resolve(List.of(new OrderLineCommand(productId, skuId, quantity))).get(0);
}
public List<ResolvedLine> resolve(List<OrderLineCommand> requestedItems) {
if (requestedItems == null || requestedItems.isEmpty()) {
throw new TradeException(TradeError.PRODUCT_UNAVAILABLE);
}
Map<ItemKey, Long> quantities = new LinkedHashMap<>();
for (OrderLineCommand item : requestedItems) {
if (item.productId() == null || item.skuId() == null || item.quantity() <= 0) {
throw new TradeException(TradeError.PRODUCT_UNAVAILABLE);
}
try {
quantities.merge(new ItemKey(item.productId(), item.skuId()), item.quantity(), Math::addExact);
} catch (ArithmeticException exception) {
throw new TradeException(TradeError.PRODUCT_UNAVAILABLE, exception);
}
}
if (quantities.values().stream().anyMatch(quantity -> quantity > MAX_QUANTITY)) {
throw new TradeException(TradeError.PRODUCT_UNAVAILABLE);
}
Map<Long, CatalogPort.ProductSnapshot> products = new LinkedHashMap<>();
List<ResolvedLine> resolved = new ArrayList<>();
for (Map.Entry<ItemKey, Long> entry : quantities.entrySet()) {
ItemKey key = entry.getKey();
CatalogPort.ProductSnapshot product = products.computeIfAbsent(key.productId(), catalogPort::getProduct);
if (!"ACTIVE".equals(product.status())) {
throw new TradeException(TradeError.PRODUCT_UNAVAILABLE);
}
CatalogPort.SkuSnapshot sku = product.skus().stream()
.filter(candidate -> candidate.id().equals(key.skuId()) && "ACTIVE".equals(candidate.status()))
.findFirst()
.orElseThrow(() -> new TradeException(TradeError.PRODUCT_UNAVAILABLE));
String imageObjectKey = product.media().stream()
.filter(media -> media.skuId() == null || media.skuId().equals(sku.id()))
.sorted(Comparator.comparingInt(CatalogPort.MediaSnapshot::sortOrder))
.map(CatalogPort.MediaSnapshot::objectKey)
.findFirst()
.orElse(null);
resolved.add(new ResolvedLine(
product.id(), sku.id(), product.title(), sku.skuCode(), sku.name(), sku.specJson(),
imageObjectKey, sku.salePrice(), entry.getValue()));
}
return resolved.stream().sorted(Comparator.comparing(ResolvedLine::skuId)).toList();
}
private record ItemKey(Long productId, Long skuId) {
}
public record ResolvedLine(
Long productId,
Long skuId,
String productTitle,
String skuCode,
String skuName,
String specJson,
String imageObjectKey,
BigDecimal unitPrice,
long quantity
) {
public BigDecimal lineAmount() {
return unitPrice.multiply(BigDecimal.valueOf(quantity));
}
}
}