fix(lastleg): complete generated pallet receiving flow

This commit is contained in:
2026-09-09 02:54:09 +08:00
parent 86646ccd69
commit c5862db98d
20 changed files with 367 additions and 225 deletions
@@ -49,7 +49,23 @@ class HttpWarehouseResourceGatewayTest {
}
@Test
void resolveRejectsPalletWhoseLocationIsNotReservable() {
void resolveAllowsOccupiedPalletForInventoryQueryAndOperations() {
server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallets/resolve/PAL-OCCUPIED"))
.andRespond(withSuccess("""
{"code":"200","data":{"palletId":31,"palletCode":"PAL-OCCUPIED",
"warehouseId":21,"locationId":41,"status":"OCCUPIED","version":4,
"reservable":false,"reservationBlockReason":"PALLET_NOT_IDLE"}}
""", MediaType.APPLICATION_JSON));
var result = gateway.resolvePallet("PAL-OCCUPIED");
assertThat(result.palletId()).isEqualTo(31L);
assertThat(result.status()).isEqualTo("OCCUPIED");
server.verify();
}
@Test
void availableResolveRejectsPalletWhoseLocationIsNotReservable() {
server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallets/resolve/PAL-BLOCKED"))
.andRespond(withSuccess("""
{"code":"200","data":{"palletId":2092040891261427713,"palletCode":"PAL-BLOCKED",
@@ -59,7 +75,7 @@ class HttpWarehouseResourceGatewayTest {
"reservationBlockReason":"LOCATION_NOT_IDLE"}}
""", MediaType.APPLICATION_JSON));
assertThatThrownBy(() -> gateway.resolvePallet("PAL-BLOCKED"))
assertThatThrownBy(() -> gateway.resolveAvailablePallet("PAL-BLOCKED"))
.isInstanceOf(ResponseStatusException.class)
.hasMessageContaining("409")
.hasMessageContaining("Pallet location is not idle");
@@ -99,7 +115,7 @@ class HttpWarehouseResourceGatewayTest {
}
@Test
void resolveFailsClosedWhenWarehouseOmitsReservationFacts() {
void availableResolveFailsClosedWhenWarehouseOmitsReservationFacts() {
server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallets/resolve/PAL-UNKNOWN"))
.andRespond(withSuccess("""
{"code":"200","data":{"palletId":2092040891261427713,"palletCode":"PAL-UNKNOWN",
@@ -107,7 +123,7 @@ class HttpWarehouseResourceGatewayTest {
"status":"IDLE","fbaWarehouseCode":"ABE3","version":3}}
""", MediaType.APPLICATION_JSON));
assertThatThrownBy(() -> gateway.resolvePallet("PAL-UNKNOWN"))
assertThatThrownBy(() -> gateway.resolveAvailablePallet("PAL-UNKNOWN"))
.isInstanceOf(ResponseStatusException.class)
.hasMessageContaining("409")
.hasMessageContaining("availability could not be verified");
@@ -376,7 +376,7 @@ class ReceivingContainerServiceBehaviorTest {
org.mockito.Mockito.reset(plans, lines, orders, sessions, operations, cartons, warehouse, pallets, workflow);
stubCurrent("FBA", "CONT-A");
stubSku();
when(warehouse.resolvePallet("PAL-1")).thenReturn(resource(101L, "FBA-1"));
when(warehouse.resolveAvailablePallet("PAL-1")).thenReturn(resource(101L, "FBA-1"));
assertThatThrownBy(() -> service.receiveBulk("7", "receiver",
new ReceivingContainerRequests.BulkReceive("IN-1", "PAL-1", 1, "pallet-key")))
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("409");
@@ -387,7 +387,6 @@ class ReceivingContainerServiceBehaviorTest {
void overReceiptAndDuplicateCartonFailBeforeRemoteReservation() {
stubCurrent("FBA", "CONT-A");
stubSku();
when(warehouse.resolvePallet("PAL-1")).thenReturn(resource(100L, "FBA-1"));
when(pallets.selectList(any())).thenReturn(List.of(planPallet()));
doThrow(new ResponseStatusException(org.springframework.http.HttpStatus.CONFLICT, "over receipt"))
.when(workflow).validateBulkResolved(any(), any(), any(), anyInt());
@@ -399,10 +398,7 @@ class ReceivingContainerServiceBehaviorTest {
org.mockito.Mockito.reset(plans, lines, orders, sessions, operations, cartons, warehouse, pallets, workflow);
stubCurrent("OVERSEAS", "CONT-A");
when(cartons.selectOne(any())).thenReturn(carton(1L, "BOX-1"));
when(warehouse.resolvePallet("PAL-1")).thenReturn(resource(100L, null));
when(pallets.selectList(any())).thenReturn(List.of(planPallet()));
doThrow(new ResponseStatusException(org.springframework.http.HttpStatus.CONFLICT, "duplicate carton"))
.when(workflow).validateCartonResolved(any(), any(), any(), any(), any());
assertThatThrownBy(() -> service.receiveCarton("7", "receiver",
new ReceivingContainerRequests.CartonReceive("BOX-1", "PAL-1", "duplicate-key")))
.isInstanceOf(ResponseStatusException.class);
@@ -415,7 +411,7 @@ class ReceivingContainerServiceBehaviorTest {
InboundPlanLine overseasLine = line("OVERSEAS");
overseasLine.setDestinationWarehouseId("900");
when(lines.selectList(any())).thenReturn(List.of(overseasLine));
when(warehouse.resolvePallet("PAL-1")).thenReturn(resource(100L, null));
when(warehouse.resolveAvailablePallet("PAL-1")).thenReturn(resource(100L, null));
var resolved = service.resolvePallet("7",
new ReceivingContainerRequests.PalletResolve("PAL-1", "IN-1"));
@@ -453,7 +449,7 @@ class ReceivingContainerServiceBehaviorTest {
void localFailureAfterRemoteReservationTriggersReleaseCompensation() {
stubCurrent("FBA", "CONT-A");
stubSku();
when(warehouse.resolvePallet("PAL-1")).thenReturn(resource(100L, "FBA-1"));
when(warehouse.resolveAvailablePallet("PAL-1")).thenReturn(resource(100L, "FBA-1"));
when(pallets.selectList(any())).thenReturn(List.of(planPallet()));
when(workflow.assignResolvedPallet(any(), any(), any(), any())).thenReturn(55L);
ResponseStatusException failure = new ResponseStatusException(org.springframework.http.HttpStatus.CONFLICT, "race");
@@ -490,7 +486,7 @@ class ReceivingContainerServiceBehaviorTest {
new ReceivingContainerRequests.BulkReceive("IN-1", "PAL-RESERVED", 1, "reserved-key"));
verify(warehouse).resolveReservedPallet("PAL-RESERVED", "100", 100L, 30L, 40L, 50L);
verify(warehouse, never()).resolvePallet("PAL-RESERVED");
verify(warehouse, never()).resolveAvailablePallet("PAL-RESERVED");
verify(workflow).assignResolvedPallet(org.mockito.ArgumentMatchers.eq("100"),
any(InboundPlan.class), org.mockito.ArgumentMatchers.same(reserved),
org.mockito.ArgumentMatchers.same(occupied));
@@ -623,17 +619,21 @@ class ReceivingContainerServiceBehaviorTest {
}
@Test
void cartonResolutionReturnsInboundAndPalletMergeReminderFromTailLegOrder() {
void cartonResolutionReturnsAssignmentOptionalMergeAndUrgencyFromInboundPlan() {
stubCurrent("FBA", "CONT-A");
LastlegOrder marked = order("FBA");
marked.setPalletMergeFlag("REQUIRED");
when(orders.selectById(1L)).thenReturn(marked);
InboundPlan flagged = plan("CONT-A");
flagged.setAssignedFlag(true);
flagged.setMergeRequired(true);
flagged.setUrgentFlag(true);
when(plans.selectList(any())).thenReturn(List.of(flagged));
var resolved = service.resolveCarton("7", new ReceivingContainerRequests.CartonResolve("IN-1#2-1"));
assertThat(resolved.inboundCode()).isEqualTo("IN-1");
assertThat(resolved.palletMergeRequired()).isTrue();
assertThat(resolved.palletMergeHint()).contains("合托");
assertThat(resolved.assigned()).isTrue();
assertThat(resolved.mergeAllowed()).isTrue();
assertThat(resolved.urgent()).isTrue();
assertThat(resolved.mergeHint()).contains("可选择", "同目的仓", "新托盘码", "不强制");
}
@Test
@@ -700,7 +700,7 @@ class ReceivingContainerServiceBehaviorTest {
when(orders.selectById(2L)).thenReturn(secondOrder);
when(orderDetails.selectList(any())).thenReturn(List.of(detail("SKU-A", 1), detail("SKU-B", 1)),
List.of(detail("SKU-C", 2)));
when(warehouse.resolvePallet("PAL-1")).thenReturn(resource(100L, "FBA-1"));
when(warehouse.resolveAvailablePallet("PAL-1")).thenReturn(resource(100L, "FBA-1"));
when(pallets.selectList(any())).thenReturn(List.of(planPallet()));
when(workflow.receiveBulkResolved(any(), any(), any(), any(), anyInt(), any(), any()))
.thenAnswer(invocation -> invocation.getArgument(4));
@@ -49,7 +49,7 @@ class InboundPlanPaginationTest {
LocalDateTime.of(2026, 8, 1, 0, 0), LocalDateTime.of(2026, 8, 31, 23, 59));
InboundPlanListResponse row = new InboundPlanListResponse(9007199254740993L, 2L, "LIP2", "100",
3L, "IN-1", null, "FIRST_LEG", "US-LAX-01", "洛杉矶仓", "W1", "ONT", "安大略仓",
"ONT8", "客户A", 10, 0, BigDecimal.ONE, 1, "PENDING_ASSIGNMENT", null, null, null,
"ONT8", "客户A", 10, 0, BigDecimal.ONE, 1, true, true, true, "PENDING_ASSIGNMENT", null, null, null,
null, "UNSCHEDULED", LocalDateTime.of(2026, 8, 20, 10, 0), 0);
when(queryMapper.count("100", query)).thenReturn(11L);
when(queryMapper.rows("100", query, 10, 10)).thenReturn(List.of(row));
@@ -61,6 +61,9 @@ class InboundPlanPaginationTest {
assertThat(result.records().get(0).appointmentStatus()).isNull();
assertThat(result.records().get(0).overseasWarehouseCode()).isEqualTo("US-LAX-01");
assertThat(result.records().get(0).overseasWarehouseName()).isEqualTo("洛杉矶仓");
assertThat(result.records().get(0).assigned()).isTrue();
assertThat(result.records().get(0).mergeAllowed()).isTrue();
assertThat(result.records().get(0).urgent()).isTrue();
assertThat(result.pages()).isEqualTo(2);
verify(queryMapper).count("100", query);
verify(queryMapper).rows("100", query, 10, 10);
@@ -76,6 +79,8 @@ class InboundPlanPaginationTest {
assertThat(sql).contains("o.overseas_warehouse_code overseasWarehouseCode",
"o.overseas_warehouse_name overseasWarehouseName",
"o.overseas_warehouse_id=#{q.overseasWarehouseId}");
assertThat(sql).contains("p.assigned_flag assigned", "p.merge_required mergeAllowed",
"p.urgent_flag urgent");
assertThat(sql).doesNotContain("l.destination_warehouse_id=#{q.overseasWarehouseId}");
assertThat(sql).contains("q.planNo", "q.inboundCode", "q.status", "q.overseasWarehouseId",
"q.destinationWarehouseCode", "q.warehouse",