diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java index 07cbf6a04..075c443e8 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java @@ -151,12 +151,6 @@ public class WarehouseTransferService { validateDestination(request.destinationType(), request.destinationWarehouseCode(), request.fbaWarehouseCode(), request.privateDestinationAddress(), request.privateDestinationPostalCode(), request.privateDestinationContactName(), request.privateDestinationContactPhone()); - // A split relabel transfer may intentionally defer the whole-order - // destination: relabeled cartons go through relabel dispatch while the - // remaining cartons are bound later from warehouse-transfer outbound. - // All other transfer types must still select FBA or PRIVATE up front. - validateTransferDestinationRequired(request.destinationType(), request.fbaWarehouseCode(), - request.needRelabel() && request.relabelItems() != null && !request.relabelItems().isEmpty()); String key = request.idempotencyKey().trim(); String requestHash = hash(request); WarehouseTransferOrder replay = transfers.findKey(customerId, key); @@ -292,8 +286,6 @@ public class WarehouseTransferService { validateDestination(request.destinationType(), request.destinationWarehouseCode(), request.fbaWarehouseCode(), request.privateDestinationAddress(), request.privateDestinationPostalCode(), request.privateDestinationContactName(), request.privateDestinationContactPhone()); - validateTransferDestinationRequired(request.destinationType(), request.fbaWarehouseCode(), - request.needRelabel() && request.relabelItems() != null && !request.relabelItems().isEmpty()); LastlegOrder order = requireTailOrder(customerId, transfer); LocalDateTime now = LocalDateTime.now(); @@ -1508,15 +1500,6 @@ public class WarehouseTransferService { } } - private static void validateTransferDestinationRequired(String destinationType, String fbaWarehouseCode, - boolean allowDeferredForRelabel) { - String type = trim(destinationType); - if (type == null && StringUtils.hasText(fbaWarehouseCode)) type = "FBA"; - if ((type == null || "NONE".equalsIgnoreCase(type)) && !allowDeferredForRelabel) { - throw badRequest("仓储转运必须选择 FBA 仓或私人仓"); - } - } - private static void applyDestination(WarehouseTransferOrder transfer, WarehouseTransferRequests.Create request) { String type = trim(request.destinationType()); if (type == null && StringUtils.hasText(request.fbaWarehouseCode())) type = "FBA"; diff --git a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml index 578855e18..d47a01f02 100644 --- a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml @@ -1214,7 +1214,7 @@ paths: responses: { '200': { description: 当前客户仓储转运单分页 }, '403': { description: 缺少仓储转运查看权限 } } post: summary: 客户创建仓储转运单 - description: 权限 merchant:overseas-warehouse:transfer:create;客户范围只取认证 Token。 + description: 权限 merchant:overseas-warehouse:transfer:create;客户范围只取认证 Token。目的去向可暂未确定(destinationType 为 NONE 或不传),允许保存草稿、编辑和提交;入库确认后可在出库时绑定 FBA 仓或私人仓,也可选择入库库存发起贴标申请出库;普通转运出库前必须补齐目的仓。 security: [{ bearerAuth: [] }] responses: { '200': { description: 创建后的仓储转运单 }, '403': { description: 缺少仓储转运创建权限 }, '409': { description: 幂等、库存或状态冲突 } } /api/merchant/v1/last-leg/warehouse-transfers/{transferNo}: diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java index daa5b3abb..d3c3dc809 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java @@ -47,6 +47,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; @@ -438,20 +441,65 @@ class WarehouseTransferServiceTest { verifyNoInteractions(transfers); } - @Test - void transferRejectsRequestWithoutDestination() { + @ParameterizedTest + @NullSource + @ValueSource(strings = {"NONE"}) + void transferWithoutDestinationCanBeCreatedAndSubmitted(String destinationType) { WarehouseTransferRequests.Create request = new WarehouseTransferRequests.Create( - "IN-1", List.of(11L), "WH-1", null, "123 Pickup Road", 5, + "IN-1", List.of(11L), "WH-1", null, destinationType, null, null, null, + null, null, null, null, "123 Pickup Road", null, null, 5, new BigDecimal("0.2500"), "WAREHOUSE_PICKUP", LocalDateTime.of(2026, 9, 12, 10, 0), - null, null, null, "FBA destination will be supplied later", false, "key-no-fba", - List.of(new WarehouseTransferRequests.Item("SKU-1", "Product", 5, "MARK-1"))); + null, null, null, null, "Destination will be supplied before outbound", false, + null, null, null, null, null, "key-no-fba", + List.of(new WarehouseTransferRequests.Item("SKU-1", "Product", 5, "MARK-1")), null); - ResponseStatusException exception = assertThrows(ResponseStatusException.class, - () -> service.create("TENANT-A", "USER-1", request)); + when(inboundSnapshots.findId("TENANT-A", "IN-1")).thenReturn(101L); + when(orders.selectList(any())).thenReturn(List.of(historicalOrder())); + WarehouseTransferOrder draft = transfer(WarehouseTransferService.DRAFT); + draft.setFbaWarehouseCode(null); + when(transfers.findNo(eq("TENANT-A"), anyString())).thenReturn(draft); + when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(draft); + when(orders.selectById(draft.getLastlegOrderId())).thenReturn(tailOrder()); + when(transferItems.selectList(any())).thenReturn(List.of()); + when(sourceOrders.selectList(any())).thenReturn(List.of()); - assertEquals(400, exception.getStatusCode().value()); - assertEquals("仓储转运必须选择 FBA 仓或私人仓", exception.getReason()); - verifyNoInteractions(transfers); + service.create("TENANT-A", "USER-1", request); + var result = service.submit("TENANT-A", "WTR-1", "USER-1"); + + ArgumentCaptor created = ArgumentCaptor.forClass(WarehouseTransferOrder.class); + verify(transfers).insert(created.capture()); + assertNull(created.getValue().getDestinationType()); + assertNull(created.getValue().getFbaWarehouseCode()); + assertEquals(WarehouseTransferService.PENDING_REVIEW, result.status()); + verify(relabelApplications, never()).insert(any(RelabelApplication.class)); + } + + @Test + void merchantCanClearDestinationWhenEditingDraft() { + WarehouseTransferOrder transfer = transfer(WarehouseTransferService.DRAFT); + transfer.setDestinationType("FBA"); + transfer.setDestinationWarehouseCode("ONT8"); + LastlegOrder order = tailOrder(); + when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer); + when(transfers.findNo("TENANT-A", "WTR-1")).thenReturn(transfer); + when(orders.selectById(transfer.getLastlegOrderId())).thenReturn(order); + when(transferItems.selectList(any())).thenReturn(List.of()); + when(sourceOrders.selectList(any())).thenReturn(List.of()); + WarehouseTransferRequests.Create request = new WarehouseTransferRequests.Create( + "IN-1", List.of(11L), "WH-1", null, "NONE", null, null, null, + null, null, null, null, "123 Pickup Road", null, null, 5, + new BigDecimal("0.2500"), "WAREHOUSE_PICKUP", LocalDateTime.of(2026, 9, 12, 10, 0), + null, null, null, null, null, false, null, null, null, null, null, "edit-no-fba", + List.of(new WarehouseTransferRequests.Item("SKU-1", "Product", 5, "MARK-1")), null); + + var result = service.updateDraft("TENANT-A", "WTR-1", "USER-1", request); + + assertEquals(WarehouseTransferService.DRAFT, result.status()); + assertNull(transfer.getDestinationType()); + assertNull(transfer.getDestinationWarehouseCode()); + assertNull(transfer.getFbaWarehouseCode()); + assertNull(order.getFbaWarehouseCode()); + verify(transfers).updateById(transfer); } @Test