diff --git a/order/src/main/java/com/baiyue/order/dto/OrderRequests.java b/order/src/main/java/com/baiyue/order/dto/OrderRequests.java index 9dc21d26d..6a6e8a2aa 100644 --- a/order/src/main/java/com/baiyue/order/dto/OrderRequests.java +++ b/order/src/main/java/com/baiyue/order/dto/OrderRequests.java @@ -44,10 +44,10 @@ public final class OrderRequests { LocalDate expectedShipDate, @Schema(description = "Customer remark / 客户备注", example = "Notify before pickup / 提货前联系", maxLength = 1000) @Size(max = 1000) String customerRemark, - @Schema(description = "Sender information / 发货方信息", requiredMode = Schema.RequiredMode.REQUIRED) - @NotNull @Valid Party sender, - @Schema(description = "Receiver information / 收货方信息", requiredMode = Schema.RequiredMode.REQUIRED) - @NotNull @Valid Party receiver, + @Schema(description = "Sender information; required when pickupMethod is PICKUP / 发货方信息,取货方式为 PICKUP 时必填") + @Valid Party sender, + @Schema(description = "Receiver information; required for PRIVATE destination / 收货方信息,私人地址目的地时必填") + @Valid Party receiver, @Schema(description = "Cargo lines / 货物明细", requiredMode = Schema.RequiredMode.REQUIRED) @NotEmpty @Valid List cargoes, @NotBlank @Pattern(regexp = "SELF_EXPRESS|PICKUP|SELF_TRUCK") String pickupMethod, 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 423f6152b..ffbf990f6 100644 --- a/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java +++ b/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java @@ -145,6 +145,12 @@ public class OrderApplicationService { } private void validatePrototypeFields(OrderRequests.SaveRequest request) { + if ("PICKUP".equals(request.pickupMethod()) && request.sender() == null) { + throw new BizException("上门提货时必须填写发货信息"); + } + if ("PRIVATE".equals(request.destinationType()) && request.receiver() == null) { + throw new BizException("私人地址必须填写收货信息"); + } if ("FBA".equals(request.destinationType())) { if (!StringUtils.hasText(request.amazonWarehouseCode()) || !StringUtils.hasText(request.shipmentId()) || !StringUtils.hasText(request.referenceId())) { @@ -168,6 +174,13 @@ public class OrderApplicationService { } var cargoes = repository.findCargo(tenantId, order.getId()); if (cargoes == null || cargoes.isEmpty()) throw new BizException("At least one cargo line is required"); + var parties = repository.findParties(tenantId, order.getId()); + if ("PICKUP".equals(order.getPickupMethod()) && !hasParty(parties, "SENDER")) { + throw new BizException("上门提货时必须填写发货信息"); + } + if ("PRIVATE".equals(order.getDestinationType()) && !hasParty(parties, "RECEIVER")) { + throw new BizException("私人地址必须填写收货信息"); + } if ("FBA".equals(order.getDestinationType())) { if (!StringUtils.hasText(order.getAmazonWarehouseCode()) || !StringUtils.hasText(order.getShipmentId()) || !StringUtils.hasText(order.getReferenceId())) { @@ -181,6 +194,11 @@ public class OrderApplicationService { } } + private boolean hasParty(List parties, String partyType) { + return parties != null && parties.stream() + .anyMatch(party -> party != null && partyType.equals(party.getPartyType())); + } + private OrderEntity require(long tenantId, long id) { OrderEntity order = repository.findOwned(tenantId, id); if (order == null) throw new BizException(com.baiyue.common.core.api.ResultCode.NOT_FOUND, "Order not found"); @@ -272,7 +290,8 @@ public class OrderApplicationService { }; } private void apply(OrderEntity order, OrderRequests.SaveRequest request, OrderMasterDataGuard.Snapshots snapshots) { - order.setCustomerId(snapshots.partner().id()); order.setCustomerName(snapshots.partner().partnerName()); + order.setCustomerId(snapshots.partner() == null ? null : snapshots.partner().id()); + order.setCustomerName(snapshots.partner() == null ? request.customerName() : snapshots.partner().partnerName()); order.setTransportMode(request.transportMode()); order.setTradeTerm(request.tradeTerm()); order.setDestinationCountryCode(snapshots.destination() == null ? request.destinationCountryCode() : snapshots.destination().warehouse().countryCode()); @@ -324,8 +343,8 @@ public class OrderApplicationService { item.setProductUsage(cargo.usage()); item.setModel(cargo.model()); item.setBrand(cargo.brand()); item.setCreatedBy(userId); item.setUpdatedBy(userId); repository.insertCargo(item); } - saveParty(tenantId, userId, orderId, "SENDER", request.sender()); - saveParty(tenantId, userId, orderId, "RECEIVER", request.receiver()); + if (request.sender() != null) saveParty(tenantId, userId, orderId, "SENDER", request.sender()); + if (request.receiver() != null) saveParty(tenantId, userId, orderId, "RECEIVER", request.receiver()); } private void saveParty(long tenantId, long userId, long orderId, String type, OrderRequests.Party party) { OrderPartyEntity item = new OrderPartyEntity(); diff --git a/order/src/main/java/com/baiyue/order/service/OrderMasterDataGuard.java b/order/src/main/java/com/baiyue/order/service/OrderMasterDataGuard.java index 01a6fe9d4..b81015a2b 100644 --- a/order/src/main/java/com/baiyue/order/service/OrderMasterDataGuard.java +++ b/order/src/main/java/com/baiyue/order/service/OrderMasterDataGuard.java @@ -28,9 +28,8 @@ public class OrderMasterDataGuard { this.warehouses=warehouses; this.partners=partners; this.token=token; } public Snapshots validate(long tenantId, OrderRequests.SaveRequest request) { - if (request.customerId() == null) throw new BizException(ResultCode.VALIDATION_FAILED, "必须选择往来单位"); try { - var partner=loadPartner(tenantId,request.customerId()); + PartnerClient.Partner partner=request.customerId()==null ? null : loadPartner(tenantId,request.customerId()); var receiving=loadWarehouse(request.receivingWarehouseId(),"收货仓库"); requireWarehouseType(receiving, "DOMESTIC", "收货仓库"); if (receiving.addresses()==null || receiving.addresses().stream().noneMatch(Objects::nonNull)) @@ -62,8 +61,7 @@ public class OrderMasterDataGuard { public void validatePersisted(long tenantId, Long partnerId, Long receivingWarehouseId, Long destinationWarehouseId, String destinationType) { try { - if (partnerId==null) throw new BizException(ResultCode.NOT_FOUND,"往来单位已停用或不可用"); - loadPartner(tenantId,partnerId); + if (partnerId!=null) loadPartner(tenantId,partnerId); if (receivingWarehouseId==null) throw new BizException(ResultCode.NOT_FOUND,"收货仓库已停用或不可用"); WarehouseClient.Detail receiving=loadWarehouse(receivingWarehouseId,"收货仓库"); requireWarehouseType(receiving,"DOMESTIC","收货仓库"); 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 3a940e572..58c04218c 100644 --- a/order/src/test/java/com/baiyue/order/service/OrderApplicationServiceStateTest.java +++ b/order/src/test/java/com/baiyue/order/service/OrderApplicationServiceStateTest.java @@ -6,6 +6,7 @@ import com.baiyue.order.client.PartnerClient; import com.baiyue.order.client.WarehouseClient; import com.baiyue.order.dto.OrderRequests; import com.baiyue.order.entity.OrderEntity; +import com.baiyue.order.entity.OrderCargoEntity; import com.baiyue.order.entity.OrderStatus; import com.baiyue.order.repository.OrderRepository; import com.fasterxml.jackson.databind.ObjectMapper; @@ -98,6 +99,68 @@ class OrderApplicationServiceStateTest { verify(repository,never()).saveOrder(any()); } + @Test + void publicDestinationCanBeSavedWithoutPartnerSenderOrReceiver() { + OrderRepository repository = mock(OrderRepository.class); + OrderEligibilityGuard eligibility = mock(OrderEligibilityGuard.class); + OrderMasterDataGuard masterData = mock(OrderMasterDataGuard.class); + OrderRequests.SaveRequest request = request(null, "SELF_EXPRESS", null, null, "OVERSEAS"); + when(masterData.validate(20002L, request)).thenReturn(snapshots(null)); + when(repository.completeIdempotency(any(), anyLong())).thenReturn(1); + + service(repository, eligibility, masterData) + .createDraft(20002L, 2L, request, "public-destination-no-parties"); + + verify(repository).insertOrder(argThat(order -> order.getCustomerId() == null + && "当前企业".equals(order.getCustomerName()))); + verify(repository, never()).insertParty(any()); + } + + @Test + void pickupStillRequiresSender() { + OrderRepository repository = mock(OrderRepository.class); + OrderMasterDataGuard masterData = mock(OrderMasterDataGuard.class); + OrderRequests.SaveRequest request = request(null, "PICKUP", null, null, "OVERSEAS"); + + assertThatThrownBy(() -> service(repository, mock(OrderEligibilityGuard.class), masterData) + .createDraft(20002L, 2L, request, "pickup-without-sender")) + .isInstanceOf(BizException.class) + .hasMessageContaining("上门提货时必须填写发货信息"); + + verifyNoInteractions(masterData); + } + + @Test + void privateDestinationStillRequiresReceiver() { + OrderRepository repository = mock(OrderRepository.class); + OrderMasterDataGuard masterData = mock(OrderMasterDataGuard.class); + OrderRequests.SaveRequest request = request(null, "SELF_EXPRESS", null, null, "PRIVATE"); + + assertThatThrownBy(() -> service(repository, mock(OrderEligibilityGuard.class), masterData) + .createDraft(20002L, 2L, request, "private-without-receiver")) + .isInstanceOf(BizException.class) + .hasMessageContaining("私人地址必须填写收货信息"); + + verifyNoInteractions(masterData); + } + + @Test + void submittingPersistedPickupDraftRequiresSenderSnapshot() { + OrderRepository repository = mock(OrderRepository.class); + OrderEntity order = persistedDraft("PICKUP", "OVERSEAS"); + OrderCargoEntity cargo = new OrderCargoEntity(); + when(repository.findOwned(20002L, 1L)).thenReturn(order); + when(repository.findCargo(20002L, 1L)).thenReturn(List.of(cargo)); + when(repository.findParties(20002L, 1L)).thenReturn(List.of()); + + assertThatThrownBy(() -> service(repository, mock(OrderEligibilityGuard.class), mock(OrderMasterDataGuard.class)) + .transit(20002L, 2L, "u", 1L, OrderStatus.SUBMITTED, null)) + .isInstanceOf(BizException.class) + .hasMessageContaining("上门提货时必须填写发货信息"); + + verify(repository, never()).saveOrder(any()); + } + private OrderApplicationService service(OrderRepository repository, OrderEligibilityGuard eligibility, OrderMasterDataGuard masterData) { OrderApplicationService service = new OrderApplicationService(); @@ -121,12 +184,44 @@ class OrderApplicationServiceStateTest { "BUY_DECLARATION", "LCL", false, "OVERSEAS", null, null, null); } + private OrderRequests.SaveRequest request(Long customerId, String pickupMethod, + OrderRequests.Party sender, OrderRequests.Party receiver, + String destinationType) { + var cargo = new OrderRequests.Cargo( + "货物", null, "CARTON", 1, BigDecimal.ONE, BigDecimal.ONE, + null, null, null, null, null, false, null, null, null, null, + null, null, null, null, null, null, null, null, null); + return new OrderRequests.SaveRequest( + customerId, "当前企业", "SEA", null, "US", null, "目的仓", null, null, + sender, receiver, List.of(cargo), pickupMethod, 8L, "收货仓", "CH", "渠道", + "BUY_DECLARATION", "LCL", false, destinationType, null, null, null); + } + private OrderMasterDataGuard.Snapshots snapshots() { - var partner = new PartnerClient.Partner(7L, 10001L, "客户", "ACTIVE", "SHIPPER", List.of("SHIPPER")); + return snapshots(new PartnerClient.Partner(7L, 10001L, "客户", "ACTIVE", "SHIPPER", List.of("SHIPPER"))); + } + + private OrderMasterDataGuard.Snapshots snapshots(PartnerClient.Partner partner) { var warehouse = new WarehouseClient.Warehouse(8L, 10001L, "CN-SZX-01", "收货仓", "DOMESTIC", "CN", "ACTIVE"); var address = new WarehouseClient.Address("CN", "广东", "深圳", "南山", "518000", "测试路1号", null); var capability = new WarehouseClient.Capability("RECEIVING", true, LocalTime.of(16, 0)); var receiving = new WarehouseClient.Detail(warehouse, List.of(address), List.of(capability)); return new OrderMasterDataGuard.Snapshots(partner, receiving, null); } + + private OrderEntity persistedDraft(String pickupMethod, String destinationType) { + OrderEntity order = new OrderEntity(); + order.setId(1L); + order.setTenantId(20002L); + order.setStatus(OrderStatus.DRAFT); + order.setPickupMethod(pickupMethod); + order.setReceivingWarehouseId(8L); + order.setReceivingWarehouseName("收货仓"); + order.setChannelCode("CH"); + order.setChannelName("渠道"); + order.setTaxMode("BUY_DECLARATION"); + order.setCustomsInspection(false); + order.setDestinationType(destinationType); + return order; + } } diff --git a/order/src/test/java/com/baiyue/order/service/OrderMasterDataGuardTest.java b/order/src/test/java/com/baiyue/order/service/OrderMasterDataGuardTest.java index 89a72cc32..d8362fb75 100644 --- a/order/src/test/java/com/baiyue/order/service/OrderMasterDataGuardTest.java +++ b/order/src/test/java/com/baiyue/order/service/OrderMasterDataGuardTest.java @@ -168,6 +168,19 @@ class OrderMasterDataGuardTest { verify(warehouses).active("token",PUBLIC_TENANT_ID,8); } @Test + void orderWithoutPartnerUsesPublicWarehouseWithoutPartnerLookup() { + WarehouseClient warehouses=mock(WarehouseClient.class); PartnerClient partners=mock(PartnerClient.class); + when(warehouses.active("token",PUBLIC_TENANT_ID,8)).thenReturn(ApiResponse.success( + warehouse(8L,"DOMESTIC",true))); + + var result=new OrderMasterDataGuard(warehouses,partners,"token") + .validate(20002,request(null,8L)); + + assertThat(result.partner()).isNull(); + verifyNoInteractions(partners); + verify(warehouses).active("token",PUBLIC_TENANT_ID,8); + } + @Test void fbaDestinationMustReferenceMatchingPublicWarehouse() { WarehouseClient warehouses=mock(WarehouseClient.class); PartnerClient partners=mock(PartnerClient.class); when(partners.active("token",20002,7)).thenReturn(ApiResponse.success(