优化
This commit is contained in:
@@ -27,6 +27,7 @@ import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
@@ -406,6 +407,9 @@ public class OrderApplicationService {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "木制品必须选择商检");
|
||||
}
|
||||
}
|
||||
validateMixedCartons(order.getFulfillmentType(), cargoes.stream().map(cargo -> new MixedCarton(
|
||||
cargo.getPackingGroupNo(), cargo.getPackageCount(), cargo.getPackageType(), cargo.getLengthCm(),
|
||||
cargo.getWidthCm(), cargo.getHeightCm(), cargo.getGrossWeightKg(), cargo.getNetWeightKg(), cargo.getVolumeCbm())).toList());
|
||||
var parties = repository.findParties(tenantId, order.getId());
|
||||
if ("PICKUP".equals(order.getPickupMethod()) && !hasParty(parties, "SENDER")) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "上门提货时必须填写发货信息");
|
||||
@@ -887,13 +891,21 @@ public class OrderApplicationService {
|
||||
return (int) total;
|
||||
}
|
||||
private void validateMixedCartons(OrderRequests.SaveRequest request) {
|
||||
if (!"ONE_CLICK_DROPSHIP".equals(request.fulfillmentType())) return;
|
||||
Map<String, OrderRequests.Cargo> firstByGroup = new LinkedHashMap<>();
|
||||
for (OrderRequests.Cargo cargo : request.cargoes()) {
|
||||
validateMixedCartons(request.fulfillmentType(), request.cargoes().stream().map(cargo -> new MixedCarton(
|
||||
cargo.packingGroupNo(), cargo.packageCount(), cargo.packageType(), cargo.lengthCm(),
|
||||
cargo.widthCm(), cargo.heightCm(), cargo.grossWeightKg(), cargo.netWeightKg(), cargo.volumeCbm())).toList());
|
||||
}
|
||||
private record MixedCarton(String packingGroupNo, Integer packageCount, String packageType,
|
||||
BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm,
|
||||
BigDecimal grossWeightKg, BigDecimal netWeightKg, BigDecimal volumeCbm) { }
|
||||
private void validateMixedCartons(String fulfillmentType, List<MixedCarton> cargoes) {
|
||||
if (!"ONE_CLICK_DROPSHIP".equals(fulfillmentType)) return;
|
||||
Map<String, MixedCarton> firstByGroup = new LinkedHashMap<>();
|
||||
for (MixedCarton cargo : cargoes) {
|
||||
String group = normalize(cargo.packingGroupNo());
|
||||
if (group == null) continue;
|
||||
OrderRequests.Cargo first = firstByGroup.putIfAbsent(group, cargo);
|
||||
if (first != null && (first.packageCount() != cargo.packageCount()
|
||||
MixedCarton first = firstByGroup.putIfAbsent(group, cargo);
|
||||
if (first != null && (!Objects.equals(first.packageCount(), cargo.packageCount())
|
||||
|| !Objects.equals(first.packageType(), cargo.packageType())
|
||||
|| !sameDecimal(first.lengthCm(), cargo.lengthCm())
|
||||
|| !sameDecimal(first.widthCm(), cargo.widthCm())
|
||||
|
||||
@@ -441,6 +441,85 @@ class OrderApplicationServiceStateTest {
|
||||
verify(repository, never()).insertHistory(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void submittingPersistedMixedCartonRejectsEveryPhysicalMismatchWithoutWritingForBothTaxModes() {
|
||||
List<java.util.function.Consumer<OrderCargoEntity>> conflicts = List.of(
|
||||
cargo -> cargo.setLengthCm(new BigDecimal("31")),
|
||||
cargo -> { cargo.setPackageCount(3); cargo.setTotalQuantity(cargo.getUnitsPerPackage() * 3); },
|
||||
cargo -> cargo.setPackageType("PALLET"),
|
||||
cargo -> cargo.setWidthCm(new BigDecimal("21")),
|
||||
cargo -> cargo.setHeightCm(new BigDecimal("21")),
|
||||
cargo -> cargo.setGrossWeightKg(new BigDecimal("21")),
|
||||
cargo -> cargo.setNetWeightKg(new BigDecimal("19")),
|
||||
cargo -> cargo.setVolumeCbm(new BigDecimal("0.025")));
|
||||
for (String taxMode : List.of("BUY_DECLARATION", "TAX_REFUND")) {
|
||||
for (var conflict : conflicts) {
|
||||
OrderRepository repository = mock(OrderRepository.class);
|
||||
OrderEntity order = persistedDraft("SELF_EXPRESS", "OVERSEAS");
|
||||
order.setFulfillmentType("ONE_CLICK_DROPSHIP"); order.setTaxMode(taxMode);
|
||||
OrderCargoEntity third = mixedCargo("SKU-3", 10); conflict.accept(third);
|
||||
when(repository.findOwned(20002L, 1L)).thenReturn(order);
|
||||
when(repository.findCargo(20002L, 1L)).thenReturn(List.of(
|
||||
mixedCargo("SKU-1", 10), mixedCargo("SKU-2", 11), third));
|
||||
assertThatThrownBy(() -> service(repository, mock(OrderEligibilityGuard.class), mock(OrderMasterDataGuard.class))
|
||||
.submit(20002L, 2L, "u", 1L, "mixed-carton-conflict"))
|
||||
.isInstanceOf(BizException.class).hasMessage("同箱 SKU 的箱数、包装类型、箱规、重量和体积必须一致")
|
||||
.extracting(exception -> ((BizException) exception).getResultCode()).isEqualTo(ResultCode.VALIDATION_FAILED);
|
||||
assertThat(order.getStatus()).isEqualTo(OrderStatus.DRAFT);
|
||||
assertThat(order.getSubmittedAt()).isNull(); assertThat(order.getInboundCode()).isNull();
|
||||
verify(repository, never()).saveOrder(anyLong(), any());
|
||||
verify(repository, never()).insertHistory(any()); verify(repository, never()).insertOutbox(any());
|
||||
verify(repository, never()).completeIdempotency(anyLong(), any(), anyLong());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void submittingConsistentMixedCartonAllowsDifferentSkuQuantitiesAndDecimalScalesAndCountsBoxesOnce() {
|
||||
OrderRepository repository = mock(OrderRepository.class);
|
||||
OrderEntity order = persistedDraft("SELF_EXPRESS", "OVERSEAS");
|
||||
order.setFulfillmentType("ONE_CLICK_DROPSHIP");
|
||||
OrderCargoEntity third = mixedCargo("SKU-3", 12);
|
||||
third.setPackingGroupNo(" GROUP-1 "); third.setLengthCm(new BigDecimal("30.00"));
|
||||
third.setGrossWeightKg(new BigDecimal("20.0")); third.setVolumeCbm(new BigDecimal("0.0240"));
|
||||
when(repository.findOwned(20002L, 1L)).thenReturn(order);
|
||||
when(repository.findCargo(20002L, 1L)).thenReturn(List.of(mixedCargo("SKU-1", 10), mixedCargo("SKU-2", 11), third));
|
||||
when(repository.findParties(20002L, 1L)).thenReturn(List.of());
|
||||
when(repository.saveOrder(20002L, order)).thenReturn(1);
|
||||
service(repository, mock(OrderEligibilityGuard.class), mock(OrderMasterDataGuard.class))
|
||||
.transit(20002L, 2L, "u", 1L, OrderStatus.SUBMITTED, null);
|
||||
assertThat(order.getStatus()).isEqualTo(OrderStatus.SUBMITTED);
|
||||
verify(repository).saveOrder(20002L, order);
|
||||
verify(repository).insertOutbox(argThat(event -> event.getPayloadJson().contains("\"totalPackageCount\":2")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void submittedMixedCartonEditorRejectsThirdSkuLengthConflictBeforeReplacingCargo() {
|
||||
OrderRepository repository = mock(OrderRepository.class);
|
||||
OrderEntity order = persistedOrder(OrderStatus.SUBMITTED);
|
||||
when(repository.findOwned(10001L, 1L)).thenReturn(order);
|
||||
var request = mixedCartonRequest(31);
|
||||
assertThatThrownBy(() -> service(repository, mock(OrderEligibilityGuard.class), mock(OrderMasterDataGuard.class))
|
||||
.updateSubmittedOrder(10001L, 2L, 1L, request))
|
||||
.isInstanceOf(BizException.class).hasMessage("同箱 SKU 的箱数、包装类型、箱规、重量和体积必须一致")
|
||||
.extracting(exception -> ((BizException) exception).getResultCode()).isEqualTo(ResultCode.VALIDATION_FAILED);
|
||||
verify(repository, never()).saveOrder(anyLong(), any());
|
||||
verify(repository, never()).replaceDraftChildren(anyLong(), anyLong());
|
||||
verify(repository, never()).insertHistory(any()); verify(repository, never()).insertOutbox(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void mixedCartonDraftStillAllowsIncompletePhysicalGroupBeforeSubmission() {
|
||||
OrderRepository repository = mock(OrderRepository.class);
|
||||
OrderMasterDataGuard masterData = mock(OrderMasterDataGuard.class);
|
||||
var request = mixedCartonRequest(31);
|
||||
when(masterData.validate(10001L, request)).thenReturn(snapshots());
|
||||
when(repository.completeIdempotency(anyLong(), any(), anyLong())).thenReturn(1);
|
||||
service(repository, mock(OrderEligibilityGuard.class), masterData)
|
||||
.createDraft(10001L, 2L, request, "mixed-carton-draft");
|
||||
verify(repository).insertOrder(argThat(order -> order.getStatus() == OrderStatus.DRAFT));
|
||||
}
|
||||
|
||||
@Test
|
||||
void submittingDraftPreparesInboundForWarehouseMeasurementBeforeReview() {
|
||||
OrderRepository repository = mock(OrderRepository.class);
|
||||
@@ -1013,6 +1092,39 @@ class OrderApplicationServiceStateTest {
|
||||
return cargo;
|
||||
}
|
||||
|
||||
private OrderCargoEntity mixedCargo(String skuNo, int unitsPerPackage) {
|
||||
OrderCargoEntity cargo = completeCargo();
|
||||
cargo.setSkuNo(skuNo); cargo.setPackingGroupNo("GROUP-1"); cargo.setPackageCount(2);
|
||||
cargo.setUnitsPerPackage(unitsPerPackage); cargo.setTotalQuantity(unitsPerPackage * 2);
|
||||
cargo.setLengthCm(new BigDecimal("30")); cargo.setWidthCm(new BigDecimal("20")); cargo.setHeightCm(new BigDecimal("20"));
|
||||
cargo.setGrossWeightKg(new BigDecimal("20")); cargo.setNetWeightKg(new BigDecimal("18"));
|
||||
cargo.setVolumeCbm(new BigDecimal("0.024"));
|
||||
return cargo;
|
||||
}
|
||||
|
||||
private OrderRequests.SaveRequest mixedCartonRequest(int thirdLength) {
|
||||
OrderRequests.SaveRequest source = request();
|
||||
var mapper = new ObjectMapper();
|
||||
var cargoes = java.util.stream.IntStream.rangeClosed(1, 3).mapToObj(index -> {
|
||||
var json = mapper.valueToTree(source.cargoes().get(0));
|
||||
var cargo = (com.fasterxml.jackson.databind.node.ObjectNode) json;
|
||||
cargo.put("skuNo", "SKU-" + index); cargo.put("packingGroupNo", "GROUP-1"); cargo.put("packageCount", 2);
|
||||
cargo.put("unitsPerPackage", index == 2 ? 11 : 10); cargo.put("totalQuantity", index == 2 ? 22 : 20);
|
||||
cargo.put("lengthCm", index == 3 ? thirdLength : 30); cargo.put("widthCm", 20); cargo.put("heightCm", 20);
|
||||
cargo.put("grossWeightKg", 20); cargo.put("netWeightKg", 18); cargo.put("volumeCbm", new BigDecimal("0.024"));
|
||||
return mapper.convertValue(cargo, OrderRequests.Cargo.class);
|
||||
}).toList();
|
||||
return new OrderRequests.SaveRequest(
|
||||
source.customerId(), source.customerName(), source.transportMode(), source.tradeTerm(),
|
||||
source.destinationCountryCode(), source.destinationWarehouseId(), source.destinationWarehouseName(),
|
||||
source.expectedShipDate(), source.customerRemark(), source.sender(), source.receiver(), cargoes,
|
||||
source.pickupMethod(), source.requestedPickupAt(), source.receivingWarehouseId(),
|
||||
source.receivingWarehouseName(), source.channelCode(), source.channelName(), source.carrierId(),
|
||||
source.carrierName(), source.taxMode(), source.pricingTaxMode(), source.containerMode(),
|
||||
source.customsInspection(), source.destinationType(), source.amazonWarehouseCode(), source.shipmentId(),
|
||||
source.referenceId(), source.inboundCode(), "ONE_CLICK_DROPSHIP");
|
||||
}
|
||||
|
||||
private OrderEntity persistedOrder(OrderStatus status) {
|
||||
OrderEntity order = new OrderEntity();
|
||||
order.setId(1L);
|
||||
|
||||
Reference in New Issue
Block a user