diff --git a/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java b/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java index a31b3d0d9..c3e71b849 100644 --- a/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java +++ b/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java @@ -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 cargoes, boolean required) { for (int index = 0; index < cargoes.size(); index++) { OrderRequests.Cargo cargo = cargoes.get(index); diff --git a/order/src/main/resources/openapi/order-v1.yaml b/order/src/main/resources/openapi/order-v1.yaml index c31a53cb7..c1ea54b99 100644 --- a/order/src/main/resources/openapi/order-v1.yaml +++ b/order/src/main/resources/openapi/order-v1.yaml @@ -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]*$'} diff --git a/order/src/test/java/com/baiyue/order/service/OrderApplicationServiceStateTest.java b/order/src/test/java/com/baiyue/order/service/OrderApplicationServiceStateTest.java index 00f440b1f..1417e4afc 100644 --- a/order/src/test/java/com/baiyue/order/service/OrderApplicationServiceStateTest.java +++ b/order/src/test/java/com/baiyue/order/service/OrderApplicationServiceStateTest.java @@ -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);