fix(order): require dropship SKU before submission

This commit is contained in:
2026-09-30 16:51:49 +08:00
parent 94881d23fd
commit babc7de4b6
3 changed files with 78 additions and 2 deletions
@@ -401,6 +401,7 @@ public class OrderApplicationService {
"提交订单前必须完善货物品名、包装类型、箱数、毛重和体积");
}
validatePersistedCargoQuantity(cargo, cargoIndex, true);
requireDropshipSku(order.getFulfillmentType(), cargo.getSkuNo(), cargoIndex);
if (Boolean.TRUE.equals(cargo.getWoodenProduct()) && !Boolean.TRUE.equals(order.getCustomsInspection())) {
throw new BizException(ResultCode.VALIDATION_FAILED, "木制品必须选择商检");
}
@@ -441,7 +442,8 @@ public class OrderApplicationService {
}
validateWoodInspection(request);
validateRequestCargoQuantities(request.cargoes(), true);
for (var cargo : request.cargoes()) {
for (int cargoIndex = 0; cargoIndex < request.cargoes().size(); cargoIndex++) {
var cargo = request.cargoes().get(cargoIndex);
if (!StringUtils.hasText(cargo.cargoName()) || !StringUtils.hasText(cargo.packageType())
|| cargo.packageCount() < 1 || cargo.grossWeightKg() == null
|| cargo.grossWeightKg().signum() <= 0 || cargo.volumeCbm() == null
@@ -449,6 +451,7 @@ public class OrderApplicationService {
throw new BizException(ResultCode.VALIDATION_FAILED,
"提交订单前必须完善货物品名、包装类型、箱数、毛重和体积");
}
requireDropshipSku(request.fulfillmentType(), cargo.skuNo(), cargoIndex);
}
validateMixedCartons(request);
if ("FBA".equals(request.destinationType())) {
@@ -467,6 +470,13 @@ public class OrderApplicationService {
}
}
private static void requireDropshipSku(String fulfillmentType, String skuNo, int cargoIndex) {
if ("ONE_CLICK_DROPSHIP".equals(fulfillmentType) && !StringUtils.hasText(skuNo)) {
throw new BizException(ResultCode.VALIDATION_FAILED,
"第 " + (cargoIndex + 1) + " 条一件代发货物必须填写 SKU NO.");
}
}
private void validateRequestCargoQuantities(List<OrderRequests.Cargo> cargoes, boolean required) {
for (int index = 0; index < cargoes.size(); index++) {
OrderRequests.Cargo cargo = cargoes.get(index);
@@ -1399,7 +1399,7 @@ components:
type: object
required: [packageCount, woodenProduct]
properties:
skuNo: {type: string, maxLength: 64}
skuNo: {type: string, maxLength: 64, description: 草稿可空;FBA 或 ONE_CLICK_DROPSHIP 订单提交及已提交编辑时每条货物必填}
cargoName: {type: string, maxLength: 128}
cargoNameEn: {type: string, maxLength: 128}
productImageFileId: {type: string, pattern: '^[1-9][0-9]*$'}
@@ -375,6 +375,72 @@ class OrderApplicationServiceStateTest {
verify(repository).saveOrder(20002L, order);
}
@Test
void submittingDropshipDraftRequiresEachPersistedSkuBeforeAnyStateWriteForBothTaxModes() {
for (String taxMode : List.of("BUY_DECLARATION", "TAX_REFUND")) {
for (String sku : java.util.Arrays.asList(null, "", " ")) {
OrderRepository repository = mock(OrderRepository.class);
OrderEntity order = persistedDraft("SELF_EXPRESS", "OVERSEAS");
order.setFulfillmentType("ONE_CLICK_DROPSHIP"); order.setTaxMode(taxMode);
OrderCargoEntity valid = completeCargo(); valid.setSkuNo("SKU-VALID");
OrderCargoEntity missing = completeCargo(); missing.setSkuNo(sku);
when(repository.findOwned(20002L, 1L)).thenReturn(order);
when(repository.findCargo(20002L, 1L)).thenReturn(List.of(valid, missing));
assertThatThrownBy(() -> service(repository, mock(OrderEligibilityGuard.class), mock(OrderMasterDataGuard.class))
.submit(20002L, 2L, "u", 1L, "missing-sku"))
.isInstanceOf(BizException.class).hasMessage("第 2 条一件代发货物必须填写 SKU NO.")
.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()).insertOutbox(any());
verify(repository, never()).insertHistory(any());
verify(repository, never()).completeIdempotency(anyLong(), any(), anyLong());
}
}
}
@Test
void submittingDropshipDraftWithSkuKeepsUpcOptional() {
OrderRepository repository = mock(OrderRepository.class);
OrderEntity order = persistedDraft("SELF_EXPRESS", "OVERSEAS");
order.setFulfillmentType("ONE_CLICK_DROPSHIP");
OrderCargoEntity cargo = completeCargo(); cargo.setSkuNo("SKU-VALID"); cargo.setUpcNo(null);
when(repository.findOwned(20002L, 1L)).thenReturn(order);
when(repository.findCargo(20002L, 1L)).thenReturn(List.of(cargo));
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);
}
@Test
void submittedDropshipEditorCannotRemoveRequiredSku() {
OrderRepository repository = mock(OrderRepository.class);
OrderEntity order = persistedOrder(OrderStatus.SUBMITTED);
when(repository.findOwned(10001L, 1L)).thenReturn(order);
OrderRequests.SaveRequest source = request();
OrderRequests.SaveRequest missingSku = 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(), source.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");
assertThatThrownBy(() -> service(repository, mock(OrderEligibilityGuard.class), mock(OrderMasterDataGuard.class))
.updateSubmittedOrder(10001L, 2L, 1L, missingSku))
.isInstanceOf(BizException.class).hasMessage("第 1 条一件代发货物必须填写 SKU NO.")
.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());
}
@Test
void submittingDraftPreparesInboundForWarehouseMeasurementBeforeReview() {
OrderRepository repository = mock(OrderRepository.class);