feat(firstleg): enforce indivisible coload loading
This commit is contained in:
+2
@@ -23,6 +23,8 @@ class PlatformSmartLoadingControllerContractTest {
|
||||
@Test void everyMutationDeclaresDedicatedPermission() throws Exception {
|
||||
authority("assign","platform:firstleg:smart-loading:assign",AuthenticatedUser.class,long.class,AssignRequest.class);
|
||||
authority("assignInbound","platform:firstleg:smart-loading:assign",AuthenticatedUser.class,long.class,AssignInboundRequest.class);
|
||||
authority("assignGroup","platform:firstleg:smart-loading:assign",AuthenticatedUser.class,long.class,AssignGroupRequest.class);
|
||||
authority("removeGroup","platform:firstleg:smart-loading:delete",AuthenticatedUser.class,long.class,long.class,RemoveGroupRequest.class);
|
||||
authority("recommend","platform:firstleg:smart-loading:assign",AuthenticatedUser.class,long.class,RecommendRequest.class);
|
||||
authority("adjust","platform:firstleg:smart-loading:assign",AuthenticatedUser.class,long.class,long.class,AdjustRequest.class);
|
||||
authority("warehouseRemark","platform:firstleg:preload:edit",AuthenticatedUser.class,long.class,long.class,WarehouseRemarkRequest.class);
|
||||
|
||||
+3
-1
@@ -13,7 +13,7 @@ class SmartLoadingCandidateMapperContractTest {
|
||||
@Test
|
||||
void candidateHasOnlyTheCanonicalMybatisConstructor() {
|
||||
assertThat(Candidate.class.getDeclaredConstructors()).singleElement()
|
||||
.satisfies(constructor -> assertThat(constructor.getParameterCount()).isEqualTo(18));
|
||||
.satisfies(constructor -> assertThat(constructor.getParameterCount()).isEqualTo(24));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -33,6 +33,8 @@ class SmartLoadingCandidateMapperContractTest {
|
||||
.contains("o.process_status='WAIT_SPLIT'")
|
||||
.contains("unitGrossWeightKg")
|
||||
.contains("unitNetWeightKg")
|
||||
.contains("NULL coLoadGroupId", "NULL groupNo", "NULL memberCount",
|
||||
"NULL groupTotalBoxes", "NULL locked", "NULL blockReason")
|
||||
.contains("o.actual_gross_weight_kg")
|
||||
.contains("o.actual_net_weight_kg")
|
||||
.doesNotContain("i.unit_weight_kg")
|
||||
|
||||
@@ -25,6 +25,9 @@ import com.baiyue.firstleg.loading.mapper.LoadingInboundMapper;
|
||||
import com.baiyue.firstleg.loading.service.LoadingDeliveryService;
|
||||
import com.baiyue.firstleg.loading.bridge.dto.LoadingModelBridgeDtos.Link;
|
||||
import com.baiyue.firstleg.loading.bridge.service.LoadingModelBridgeService;
|
||||
import com.baiyue.firstleg.loading.coload.port.ColoadGroupAssignmentPort;
|
||||
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.GroupSnapshot;
|
||||
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.Member;
|
||||
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingPickItemMapper;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
@@ -43,6 +46,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -78,6 +82,8 @@ class LoadingPickServiceTest {
|
||||
@MockBean
|
||||
private LoadingDeliveryService loadingDeliveryService;
|
||||
@MockBean
|
||||
private ColoadGroupAssignmentPort coloadGroups;
|
||||
@MockBean
|
||||
private TruckAppointmentService truckAppointmentService;
|
||||
@MockBean
|
||||
private InboundStatusHistoryMapper inboundHistories;
|
||||
@@ -800,6 +806,40 @@ class LoadingPickServiceTest {
|
||||
verify(completionCommands, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shortPickedColoadMemberCannotCreateCompletionCommandOrRemainderChild() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
LoadingPickItemEntity shortPicked = item();
|
||||
shortPicked.setInboundId(50L);
|
||||
shortPicked.setPlannedPackageCount(5);
|
||||
shortPicked.setPickedPackageCount(4);
|
||||
shortPicked.setItemStatus("PARTIAL");
|
||||
LoadingPickItemEntity complete = item();
|
||||
complete.setId(31L);
|
||||
complete.setInboundId(60L);
|
||||
complete.setPlannedPackageCount(5);
|
||||
complete.setPickedPackageCount(5);
|
||||
complete.setItemStatus("PICKED");
|
||||
GroupSnapshot group = coloadGroup("LOCKED", 10L,
|
||||
new Member(101L, 301L, 50L, 5), new Member(202L, 302L, 60L, 5));
|
||||
when(repository.taskForUpdate(20L)).thenReturn(task);
|
||||
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
|
||||
when(repository.inboundOwners(20L)).thenReturn(List.of(
|
||||
new LoadingPickItemMapper.InboundOwner(101L, 50L),
|
||||
new LoadingPickItemMapper.InboundOwner(202L, 60L)));
|
||||
when(repository.items(20L)).thenReturn(List.of(shortPicked, complete));
|
||||
when(coloadGroups.findByInbound(101L, 50L)).thenReturn(group);
|
||||
when(coloadGroups.findByInbound(202L, 60L)).thenReturn(group);
|
||||
|
||||
assertThatThrownBy(() -> service.complete(20L,
|
||||
new CompleteLoadingRequest(0, "complete-short-coload", "短拣异常"), 9L, "operator"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("全组箱数守恒");
|
||||
|
||||
verify(completionCommands, never()).insert(any());
|
||||
verify(repository, never()).acceptCompletion(anyLong(), anyInt());
|
||||
verify(settlementService, never()).settleOwner(anyLong(), anyLong(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void workerProcessesFifteenInboundOwnersInStableOrderAndFinalizesOnce() {
|
||||
LoadingPickTaskEntity completing = task();
|
||||
@@ -841,6 +881,10 @@ class LoadingPickServiceTest {
|
||||
when(repository.approve(20L, 9L, "operator", "checked")).thenReturn(1);
|
||||
when(repository.completeAccepted(20L, 9L, "operator")).thenReturn(1);
|
||||
when(completionCommands.complete(903L, "worker-multi")).thenReturn(true);
|
||||
GroupSnapshot group = coloadGroup("LOCKED", 10L,
|
||||
new Member(101L, 301L, 50L, 1), new Member(101L, 302L, 60L, 1),
|
||||
new Member(202L, 303L, 70L, 1), new Member(202L, 304L, 80L, 1));
|
||||
when(coloadGroups.findByInbound(anyLong(), anyLong())).thenReturn(group);
|
||||
|
||||
service.processCompletion(completionCommand(903L, "async-multi"), "worker-multi");
|
||||
|
||||
@@ -855,6 +899,8 @@ class LoadingPickServiceTest {
|
||||
"COMPLETE_LOADING".equals(operation.getActionType())
|
||||
&& "async-multi".equals(operation.getOperationId())));
|
||||
verify(loadingListService, times(1)).publishCompletedEvent(10L);
|
||||
verify(coloadGroups).consumeWholeGroup(eq(700L), eq(10L),
|
||||
org.mockito.ArgumentMatchers.startsWith("PDA-CONSUME-"), eq(9L), eq("operator"));
|
||||
verify(completionCommands).complete(903L, "worker-multi");
|
||||
}
|
||||
|
||||
@@ -1200,4 +1246,9 @@ class LoadingPickServiceTest {
|
||||
return assignment;
|
||||
}
|
||||
|
||||
private static GroupSnapshot coloadGroup(String status, Long lockedPlanId, Member... members) {
|
||||
return new GroupSnapshot(700L, "CLG-700", status, 101L, "FBA", "FBA", 201L,
|
||||
"FBA1", lockedPlanId, 1, List.of(members), LocalDateTime.now(), LocalDateTime.now());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
||||
import com.baiyue.firstleg.loading.bridge.service.LoadingModelBridgeService;
|
||||
import com.baiyue.firstleg.loading.coload.port.ColoadGroupAssignmentPort;
|
||||
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.GroupSnapshot;
|
||||
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.Member;
|
||||
import com.baiyue.firstleg.split.service.SplitPreloadService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import feign.FeignException;
|
||||
@@ -40,6 +43,9 @@ import java.sql.SQLException;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
@@ -57,6 +63,7 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.times;
|
||||
|
||||
@SpringJUnitConfig(SmartLoadingServiceTest.Config.class)
|
||||
class SmartLoadingServiceTest {
|
||||
@@ -65,6 +72,7 @@ class SmartLoadingServiceTest {
|
||||
@MockBean private LoadingPickService loadingPickService;
|
||||
@MockBean private LoadingModelBridgeService loadingModelBridge;
|
||||
@MockBean private SplitPreloadService splitPreloadService;
|
||||
@MockBean private ColoadGroupAssignmentPort coloadGroups;
|
||||
@MockBean private FirstlegDocumentNumberGenerator documentNumbers;
|
||||
@Resource private SmartLoadingService service;
|
||||
|
||||
@@ -110,10 +118,12 @@ class SmartLoadingServiceTest {
|
||||
void groupsCandidateSpecificationsByInboundCodeForQuantityOnlyPlanning() {
|
||||
Candidate first = new Candidate(9L, 10L, 20L, 30L, "O", "I", 100L,
|
||||
"FBA", "FBA渠道", 200L, "FBA1", "FBA仓", "客户", "服装", 3,
|
||||
new BigDecimal("2"), new BigDecimal("1.5"), new BigDecimal("0.2"));
|
||||
new BigDecimal("2"), new BigDecimal("1.5"), new BigDecimal("0.2"),
|
||||
null, null, null, null, null, null);
|
||||
Candidate second = new Candidate(9L, 10L, 21L, 30L, "O", "I", 100L,
|
||||
"FBA", "FBA渠道", 200L, "FBA1", "FBA仓", "客户", "鞋类", 7,
|
||||
new BigDecimal("2"), new BigDecimal("1.5"), new BigDecimal("0.5"));
|
||||
new BigDecimal("2"), new BigDecimal("1.5"), new BigDecimal("0.5"),
|
||||
null, null, null, null, null, null);
|
||||
when(repository.booking(99L)).thenReturn(booking());
|
||||
when(repository.candidates(100L)).thenReturn(List.of(first, second));
|
||||
|
||||
@@ -129,17 +139,218 @@ class SmartLoadingServiceTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void exposesWholeColoadGroupFactsOnEveryMemberCandidate() {
|
||||
Candidate first = candidate(9L, 10L, 20L, 30L, 4);
|
||||
Candidate second = candidate(19L, 11L, 21L, 31L, 6);
|
||||
GroupSnapshot group = group("ACTIVE", null,
|
||||
new Member(9L, 30L, 10L, 4), new Member(19L, 31L, 11L, 6));
|
||||
when(repository.booking(99L)).thenReturn(booking());
|
||||
when(repository.candidates(100L)).thenReturn(List.of(first, second));
|
||||
when(coloadGroups.findByInbound(9L, 10L)).thenReturn(group);
|
||||
when(coloadGroups.findByInbound(19L, 11L)).thenReturn(group);
|
||||
|
||||
var result = service.inboundCandidates(99L, 100L);
|
||||
|
||||
assertThat(result).hasSize(2).allSatisfy(candidate -> {
|
||||
assertThat(candidate.coLoadGroupId()).isEqualTo(700L);
|
||||
assertThat(candidate.groupNo()).isEqualTo("CLG-700");
|
||||
assertThat(candidate.memberCount()).isEqualTo(2);
|
||||
assertThat(candidate.groupTotalBoxes()).isEqualTo(10);
|
||||
assertThat(candidate.locked()).isFalse();
|
||||
assertThat(candidate.blockReason()).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsIndividualAssignmentForColoadMemberWithoutWriting() {
|
||||
stubAssignment();
|
||||
when(coloadGroups.findByInbound(9L, 10L)).thenReturn(group("ACTIVE", null,
|
||||
new Member(9L, 30L, 10L, 10), new Member(19L, 31L, 11L, 5)));
|
||||
|
||||
assertThatThrownBy(() -> service.assign(1L,
|
||||
new AssignRequest(10L, 20L, 10, 0, "individual-group-member"), 7L, "u"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("整组分配");
|
||||
|
||||
verify(repository, never()).touch(anyLong(), anyInt(), anyLong(), anyString());
|
||||
verify(splitPreloadService, never()).ensurePendingForLoadingAllocation(
|
||||
anyLong(), anyLong(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void atomicallyAssignsEveryMemberWithItsFullAvailablePackageCount() {
|
||||
Candidate first = candidate(9L, 10L, 20L, 30L, 4);
|
||||
Candidate second = candidate(19L, 11L, 21L, 31L, 6);
|
||||
GroupSnapshot active = group("ACTIVE", null,
|
||||
new Member(9L, 30L, 10L, 4), new Member(19L, 31L, 11L, 6));
|
||||
GroupSnapshot locked = group("LOCKED", 1L,
|
||||
new Member(9L, 30L, 10L, 4), new Member(19L, 31L, 11L, 6));
|
||||
AtomicInteger version = new AtomicInteger();
|
||||
List<LoadingPlanItemEntity> saved = new ArrayList<>();
|
||||
when(repository.plan(1L)).thenAnswer(invocation -> plan("EDITING", version.get()));
|
||||
when(repository.planByBooking(99L)).thenAnswer(invocation -> plan("EDITING", version.get()));
|
||||
when(repository.booking(99L)).thenReturn(booking());
|
||||
when(repository.candidates(100L)).thenReturn(List.of(first, second));
|
||||
when(repository.candidate(1L, 100L, 10L, 20L)).thenReturn(first);
|
||||
when(repository.candidate(1L, 100L, 11L, 21L)).thenReturn(second);
|
||||
when(repository.items(1L)).thenAnswer(invocation -> List.copyOf(saved));
|
||||
when(repository.activeItemsByInbound(anyLong(), anyLong())).thenReturn(List.of());
|
||||
when(repository.activeItemsByInbound(anyLong(), anyLong(), anyLong())).thenReturn(List.of());
|
||||
when(repository.activeItem(anyLong(), anyLong(), anyLong())).thenReturn(null);
|
||||
when(repository.touch(eq(1L), anyInt(), eq(7L), eq("u"))).thenAnswer(invocation -> {
|
||||
int expected = invocation.getArgument(1);
|
||||
if (expected != version.get()) return 0;
|
||||
version.incrementAndGet();
|
||||
return 1;
|
||||
});
|
||||
doAnswer(invocation -> {
|
||||
LoadingPlanItemEntity item = invocation.getArgument(0);
|
||||
item.setId(800L + saved.size());
|
||||
saved.add(item);
|
||||
return null;
|
||||
}).when(repository).insert(any(LoadingPlanItemEntity.class));
|
||||
when(cargoClient.detail(anyString(), anyLong(), anyLong(), anyLong())).thenAnswer(invocation -> {
|
||||
long orderId = invocation.getArgument(2);
|
||||
long detailId = invocation.getArgument(3);
|
||||
return ApiResponse.success(new OrderCargoClient.CargoBinding(detailId, orderId, 1,
|
||||
"货物", orderId == 30L ? 4 : 6, BigDecimal.TEN, BigDecimal.ONE));
|
||||
});
|
||||
when(coloadGroups.findByInbound(9L, 10L)).thenReturn(active);
|
||||
when(coloadGroups.findByInbound(19L, 11L)).thenReturn(active);
|
||||
when(coloadGroups.lockWholeGroupForAssignment(eq(9L), eq(10L), eq(1L),
|
||||
anyString(), eq(7L), eq("u"))).thenReturn(locked);
|
||||
|
||||
Plan result = service.assignGroup(1L,
|
||||
new com.baiyue.firstleg.dto.SmartLoadingDtos.AssignGroupRequest(
|
||||
10L, 0, "assign-whole-group"), 7L, "u");
|
||||
|
||||
assertThat(result.version()).isEqualTo(2);
|
||||
assertThat(saved).extracting(LoadingPlanItemEntity::getInboundId,
|
||||
LoadingPlanItemEntity::getPackageCount).containsExactlyInAnyOrder(
|
||||
org.assertj.core.groups.Tuple.tuple(10L, 4),
|
||||
org.assertj.core.groups.Tuple.tuple(11L, 6));
|
||||
verify(coloadGroups).lockWholeGroupForAssignment(eq(9L), eq(10L), eq(1L),
|
||||
org.mockito.ArgumentMatchers.startsWith("LOCK-"), eq(7L), eq("u"));
|
||||
verify(loadingModelBridge, times(2)).bindIncrementalAllocation(eq(1L), anyLong(), anyLong(),
|
||||
eq(7L), eq("u"), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void recommendSkipsWholeThreeMemberGroupWhenCombinedCapacityDoesNotFit() {
|
||||
Candidate first = candidate(9L, 10L, 20L, 30L, 5);
|
||||
Candidate second = candidate(19L, 11L, 21L, 31L, 5);
|
||||
Candidate third = candidate(29L, 12L, 22L, 32L, 5);
|
||||
GroupSnapshot group = group("ACTIVE", null,
|
||||
new Member(9L, 30L, 10L, 5), new Member(19L, 31L, 11L, 5),
|
||||
new Member(29L, 32L, 12L, 5));
|
||||
ContainerBookingEntity small = booking();
|
||||
small.setCapacityCbm(new BigDecimal("5"));
|
||||
LoadingPlanEntity plan = plan("EDITING", 0);
|
||||
when(repository.plan(1L)).thenReturn(plan);
|
||||
when(repository.planByBooking(99L)).thenReturn(plan);
|
||||
when(repository.booking(99L)).thenReturn(small);
|
||||
when(repository.items(1L)).thenReturn(List.of());
|
||||
when(repository.candidates(100L)).thenReturn(List.of(first, second, third));
|
||||
when(coloadGroups.findByInbound(anyLong(), anyLong())).thenReturn(group);
|
||||
|
||||
Plan result = service.recommend(1L, new RecommendRequest(0, "recommend-group-too-large"), 7L, "u");
|
||||
|
||||
assertThat(result.items()).isEmpty();
|
||||
verify(coloadGroups, never()).lockWholeGroupForAssignment(anyLong(), anyLong(), anyLong(),
|
||||
anyString(), anyLong(), anyString());
|
||||
verify(repository, never()).touch(anyLong(), anyInt(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void adjustAndIndividualDeleteRejectColoadMembersBeforeAnyWrite() {
|
||||
LoadingPlanEntity plan = plan("EDITING", 0);
|
||||
LoadingPlanItemEntity item = item(8L, 9L, 20L, 4);
|
||||
when(repository.plan(1L)).thenReturn(plan);
|
||||
when(repository.item(1L, 8L)).thenReturn(item);
|
||||
when(coloadGroups.findByInbound(9L, 10L)).thenReturn(group("LOCKED", 1L,
|
||||
new Member(9L, 30L, 10L, 4), new Member(19L, 31L, 11L, 6)));
|
||||
|
||||
assertThatThrownBy(() -> service.adjust(1L, 8L,
|
||||
new AdjustRequest(4, 0, 0, "adjust-group-member"), 7L, "u"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("整组分配");
|
||||
assertThatThrownBy(() -> service.delete(1L, 8L,
|
||||
new DeleteRequest(0, 0, "delete-group-member"), 7L, "u"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("整组分配");
|
||||
|
||||
verify(repository, never()).touch(anyLong(), anyInt(), anyLong(), anyString());
|
||||
verify(repository, never()).remove(anyLong(), anyLong(), anyInt(), anyLong(), anyString());
|
||||
verify(loadingPickService, never()).remove(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pushPreloadRejectsIncompleteGroupBeforeAnyLifecycleOrPlanWrite() {
|
||||
LoadingPlanEntity plan = plan("EDITING", 0);
|
||||
LoadingPlanItemEntity first = item(8L, 9L, 20L, 4);
|
||||
GroupSnapshot group = group("LOCKED", 1L,
|
||||
new Member(9L, 30L, 10L, 4), new Member(19L, 31L, 11L, 6));
|
||||
when(repository.plan(1L)).thenReturn(plan);
|
||||
when(repository.booking(99L)).thenReturn(booking());
|
||||
when(repository.items(1L)).thenReturn(List.of(first));
|
||||
when(repository.activeItemsByInbound(9L, 10L)).thenReturn(List.of(first));
|
||||
when(coloadGroups.findByInbound(9L, 10L)).thenReturn(group);
|
||||
|
||||
assertThatThrownBy(() -> service.pushPreload(1L, "push-incomplete-group",
|
||||
new PushRequest(0), 7L, "u"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("全部在同一柜");
|
||||
|
||||
verify(repository, never()).push(anyLong(), anyString(), anyInt(), anyLong(), anyString());
|
||||
verify(splitPreloadService, never()).ensurePendingForLoadingAllocation(
|
||||
anyLong(), anyLong(), anyLong(), anyString());
|
||||
verify(loadingModelBridge, never()).bindAllocatedBeforeLoading(
|
||||
anyLong(), anyLong(), anyString(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void removesEveryGroupMemberAndUnlocksOnlyAfterAllRowsAreRemoved() {
|
||||
LoadingPlanEntity plan = plan("EDITING", 0);
|
||||
LoadingPlanEntity after = plan("EDITING", 1);
|
||||
LoadingPlanItemEntity first = item(8L, 9L, 20L, 4);
|
||||
LoadingPlanItemEntity second = item(9L, 19L, 21L, 6);
|
||||
second.setInboundId(11L);
|
||||
GroupSnapshot group = group("LOCKED", 1L,
|
||||
new Member(9L, 30L, 10L, 4), new Member(19L, 31L, 11L, 6));
|
||||
when(repository.plan(1L)).thenReturn(plan, after);
|
||||
when(repository.booking(99L)).thenReturn(booking());
|
||||
when(repository.items(1L)).thenReturn(List.of(first, second), List.of());
|
||||
when(repository.activeItemsByInbound(9L, 10L)).thenReturn(List.of(first));
|
||||
when(repository.activeItemsByInbound(19L, 11L)).thenReturn(List.of(second));
|
||||
when(repository.touch(1L, 0, 7L, "u")).thenReturn(1);
|
||||
when(repository.remove(eq(1L), anyLong(), eq(0), eq(7L), eq("u"))).thenReturn(1);
|
||||
when(coloadGroups.findByInbound(9L, 10L)).thenReturn(group);
|
||||
when(coloadGroups.findByInbound(19L, 11L)).thenReturn(group);
|
||||
when(coloadGroups.unlockWholeGroupAfterRemoval(eq(700L), eq(1L),
|
||||
anyString(), eq(7L), eq("u"))).thenReturn(group("ACTIVE", null,
|
||||
new Member(9L, 30L, 10L, 4), new Member(19L, 31L, 11L, 6)));
|
||||
|
||||
Plan result = service.removeGroup(1L, 700L,
|
||||
new com.baiyue.firstleg.dto.SmartLoadingDtos.RemoveGroupRequest(
|
||||
0, "remove-whole-group"), 7L, "u");
|
||||
|
||||
assertThat(result.items()).isEmpty();
|
||||
verify(repository, times(2)).remove(eq(1L), anyLong(), eq(0), eq(7L), eq("u"));
|
||||
verify(coloadGroups).unlockWholeGroupAfterRemoval(eq(700L), eq(1L),
|
||||
org.mockito.ArgumentMatchers.startsWith("UNLOCK-"), eq(7L), eq("u"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void proportionallyDistributesInboundQuantityWithoutLosingCartons() {
|
||||
Candidate first = new Candidate(9L, 10L, 20L, 30L, "O", "I", 100L,
|
||||
"FBA", "FBA渠道", 200L, "FBA1", "FBA仓", "客户", "A", 30,
|
||||
new BigDecimal("2"), new BigDecimal("1.5"), new BigDecimal("0.2"));
|
||||
new BigDecimal("2"), new BigDecimal("1.5"), new BigDecimal("0.2"),
|
||||
null, null, null, null, null, null);
|
||||
Candidate second = new Candidate(9L, 10L, 21L, 30L, "O", "I", 100L,
|
||||
"FBA", "FBA渠道", 200L, "FBA1", "FBA仓", "客户", "B", 40,
|
||||
new BigDecimal("2"), new BigDecimal("1.5"), new BigDecimal("0.5"));
|
||||
new BigDecimal("2"), new BigDecimal("1.5"), new BigDecimal("0.5"),
|
||||
null, null, null, null, null, null);
|
||||
Candidate third = new Candidate(9L, 10L, 22L, 30L, "O", "I", 100L,
|
||||
"FBA", "FBA渠道", 200L, "FBA1", "FBA仓", "客户", "C", 30,
|
||||
new BigDecimal("2"), new BigDecimal("1.5"), new BigDecimal("0.8"));
|
||||
new BigDecimal("2"), new BigDecimal("1.5"), new BigDecimal("0.8"),
|
||||
null, null, null, null, null, null);
|
||||
|
||||
var result = service.proportionalAllocations(List.of(first, second, third), 60);
|
||||
|
||||
@@ -987,8 +1198,19 @@ class SmartLoadingServiceTest {
|
||||
}
|
||||
|
||||
private static Candidate candidate(int boxes) {
|
||||
return new Candidate(9L, 10L, 20L, 30L, "O", "I", 100L, "FBA", "FBA渠道", 200L,
|
||||
"FBA1", "FBA仓", "客户", "货物", boxes, new BigDecimal("2"), new BigDecimal("1.5"), new BigDecimal("0.5"));
|
||||
return candidate(9L, 10L, 20L, 30L, boxes);
|
||||
}
|
||||
|
||||
private static Candidate candidate(long tenantId, long inboundId, long detailId, long orderId, int boxes) {
|
||||
return new Candidate(tenantId, inboundId, detailId, orderId, "O" + orderId, "I" + inboundId,
|
||||
100L, "FBA", "FBA渠道", 200L, "FBA1", "FBA仓", "客户", "货物", boxes,
|
||||
new BigDecimal("2"), new BigDecimal("1.5"),
|
||||
new BigDecimal("0.5"), null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
private static GroupSnapshot group(String status, Long lockedPlanId, Member... members) {
|
||||
return new GroupSnapshot(700L, "CLG-700", status, 100L, "FBA", "FBA", 200L,
|
||||
"FBA1", lockedPlanId, 0, List.of(members), LocalDateTime.now(), LocalDateTime.now());
|
||||
}
|
||||
|
||||
private static ContainerBookingEntity booking() {
|
||||
|
||||
@@ -16,6 +16,8 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
@@ -139,4 +141,12 @@ class SplitPreloadServiceTest {
|
||||
|
||||
verify(repository, never()).createPlan(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void splitPreloadNeverCreatesInboundChildrenOrFulfillmentSlices() throws Exception {
|
||||
String source = Files.readString(Path.of(
|
||||
"src/main/java/com/baiyue/firstleg/split/service/SplitPreloadService.java"));
|
||||
assertThat(source).doesNotContain("cloneChild", "OrderFulfillmentSplitClient",
|
||||
"PdaPickSettlementService", "REMAINDER");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user