This commit is contained in:
2026-08-27 17:16:33 +08:00
parent 61a97cd057
commit 76c24867a4
19 changed files with 474 additions and 107 deletions
@@ -4,6 +4,7 @@ import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
import com.baiyue.lastleg.inboundplan.dto.ConvertInboundPlanRequest;
import com.baiyue.lastleg.inboundplan.dto.ConvertContainerInboundPlanRequest;
import com.baiyue.lastleg.inboundplan.dto.UpdateFbaAppointmentRequest;
import com.baiyue.lastleg.inboundplan.service.InboundFbaAppointmentService;
import com.baiyue.lastleg.inboundplan.service.InboundPlanService;
@@ -87,6 +88,23 @@ class InboundPlanControllerTest {
verify(service).convert("100", 2L);
}
@Test
void platformContainerConversionUsesStableBookingIdentity() {
controller.convertContainer(platform(), new ConvertContainerInboundPlanRequest(" BOOKING-1 "));
verify(service).convertContainer(" BOOKING-1 ");
}
@Test
void merchantCannotConvertAWholeContainer() {
AuthenticatedUser customer = new AuthenticatedUser(8L, 100L, 100L, 9L, "customer", "MERCHANT", "WEB", Set.of());
assertThatThrownBy(() -> controller.convertContainer(customer,
new ConvertContainerInboundPlanRequest("BOOKING-1")))
.isInstanceOf(ResponseStatusException.class);
verify(service, never()).convertContainer(org.mockito.ArgumentMatchers.any());
}
private static AuthenticatedUser platform() {
return new AuthenticatedUser(7L, 0L, 0L, 8L, "operator", "PLATFORM", "WEB",
Set.of("platform:overseas-warehouse:inbound-order:list"));
@@ -191,6 +191,93 @@ class InboundPlanServiceTest {
verify(planMapper, never()).insert(any(InboundPlan.class));
}
@Test
void containerConversionPrevalidatesEveryOrderBeforeWritingIndependentPlans() throws Exception {
assertNotNull(InboundPlanService.class.getMethod("convertContainer", String.class)
.getAnnotation(Transactional.class));
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT"),
new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "DESTINATION_ASSIGNED")));
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(order("100", 1L, "RK-1", "WH-A")));
when(orderMapper.lockOrder("200", 2L)).thenReturn(List.of(order("200", 2L, "RK-2", "WH-B")));
when(orderMapper.markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any()))
.thenReturn(1);
var result = service.convertContainer(" BOOKING-1 ");
assertEquals(2, result.size());
assertEquals(1L, result.get(0).sourceOrderId());
assertEquals(2L, result.get(1).sourceOrderId());
verify(planMapper, times(2)).insert(any(InboundPlan.class));
verify(orderMapper).lockContainerOrders("BOOKING-1");
verify(orderMapper).lockOrder("100", 1L);
verify(orderMapper).lockOrder("200", 2L);
}
@Test
void invalidLaterOrderCausesNoWritesBecauseWholeContainerIsPrevalidated() {
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT"),
new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT")));
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(order(1L, "RK-1", "1", "WH-A")));
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(new SourceOrderSnapshot(2L, "100", "CONT-1", "RK-2",
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 1, null, null, null, 1, 0)));
assertThrows(ResponseStatusException.class, () -> service.convertContainer("BOOKING-1"));
verify(planMapper, never()).insert(any(InboundPlan.class));
verify(orderMapper, never()).markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any());
verify(inventoryBalanceMapper, never()).bindInboundPlan(any(), any(), any(), any(), any());
}
@Test
void completeContainerReplayReturnsStableExistingPlansWithoutWriting() {
var refs = List.of(
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED"),
new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "INBOUND_COMPLETED"));
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(refs);
when(planMapper.findBySourceOrder("100", 1L)).thenReturn(existingPlan("100", 1L, "LIP-1"));
when(planMapper.findBySourceOrder("200", 2L)).thenReturn(existingPlan("200", 2L, "LIP-2"));
var result = service.convertContainer("BOOKING-1");
assertEquals(List.of("LIP-1", "LIP-2"), result.stream().map(value -> value.planNo()).toList());
verify(orderMapper, never()).lockOrder(any(), any());
verify(planMapper, never()).insert(any(InboundPlan.class));
}
@Test
void partialExistingPlansAreRejectedBeforeAnyWrite() {
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED"),
new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT")));
when(planMapper.findBySourceOrder("100", 1L)).thenReturn(existingPlan("100", 1L, "LIP-1"));
assertThrows(ResponseStatusException.class, () -> service.convertContainer("BOOKING-1"));
verify(orderMapper, never()).lockOrder(any(), any());
verify(planMapper, never()).insert(any(InboundPlan.class));
}
private InboundPlan existingPlan(String customerId, long orderId, String planNo) {
InboundPlan plan = new InboundPlan();
plan.setId(orderId + 100);
plan.setPlanNo(planNo);
plan.setCustomerId(customerId);
plan.setSourceOrderId(orderId);
plan.setContainerNo("CONT-1");
plan.setStatus("PENDING_ASSIGNMENT");
plan.setLineCount(1);
plan.setPalletCount(1);
plan.setTotalVolumeCbm(BigDecimal.ONE);
return plan;
}
private SourceOrderSnapshot order(String customerId, Long id, String inboundCode, String warehouse) {
return new SourceOrderSnapshot(id, customerId, "CONT-1", inboundCode, "PENDING_ASSIGNMENT", null,
BigDecimal.ONE, 10, warehouse, warehouse, warehouse, 1, 0);
}
private SourceOrderSnapshot order(Long id, String inboundCode, String volume, String warehouse) {
return new SourceOrderSnapshot(id, "100", "CONT-1", inboundCode, "PENDING_ASSIGNMENT", null,
new BigDecimal(volume), new BigDecimal(volume).multiply(BigDecimal.TEN).intValue(),