Merge remote-tracking branch 'origin/test' into dev
# Conflicts: # order/AGENTS.md
This commit is contained in:
+12
-2
@@ -24,7 +24,8 @@ class InboundMeasurementEvidenceOpenApiContractTest {
|
||||
Map<String, Object> schemas = stringMap(stringMap(document.get("components")).get("schemas"));
|
||||
Map<String, Object> detailProperties = properties(schemas, "InboundMeasurementDetail");
|
||||
assertThat(detailProperties).containsKeys(
|
||||
"actualGrossWeightKg", "actualNetWeightKg", "latestDeclaration", "items", "weightResults");
|
||||
"actualGrossWeightKg", "actualNetWeightKg", "latestDeclaration", "items", "weightResults",
|
||||
"palletLocationGuide");
|
||||
assertThat(stringMap(detailProperties.get("latestDeclaration")))
|
||||
.containsEntry("$ref", "#/components/schemas/InboundMeasurementLatestDeclaration");
|
||||
assertThat(properties(schemas, "InboundMeasurementLatestDeclaration"))
|
||||
@@ -52,7 +53,16 @@ class InboundMeasurementEvidenceOpenApiContractTest {
|
||||
assertThat(itemProperties).containsKeys("id", "intelligentImageFileId");
|
||||
Map<String, Object> weightProperties = properties(schemas, "InboundWeightResult");
|
||||
assertThat(weightProperties).containsKeys(
|
||||
"id", "fileId", "recognitionStatus", "grossWeightKg", "netWeightKg", "confirmedAt");
|
||||
"id", "fileId", "recognitionStatus", "grossWeightKg", "netWeightKg", "confirmedAt",
|
||||
"palletSequence", "blockId", "slotNo", "reservationId", "locationId", "locationCode",
|
||||
"sharedOverflow", "slotStatus");
|
||||
assertThat(stringMap(weightProperties.get("slotStatus"))).containsEntry(
|
||||
"enum", java.util.List.of("RESERVED_EMPTY", "BOUND", "TRANSFERRED", "RELEASED"));
|
||||
assertThat(properties(schemas, "InboundPalletLocationGuide")).containsKeys(
|
||||
"blockId", "reservedSlotCount", "actualPalletCount", "reservedEmptySlotCount", "slots");
|
||||
assertThat(properties(schemas, "InboundPalletLocationSlot")).containsKeys(
|
||||
"slotNo", "reservationId", "locationId", "locationCode", "sharedOverflow", "slotStatus",
|
||||
"boundWeightResultId", "palletSequence", "palletId", "palletCode");
|
||||
assertThat(stringMap(detailProperties.get("actualGrossWeightKg")).get("description").toString())
|
||||
.contains("CONFIRMED", "grossWeightKg", "含托盘毛重");
|
||||
assertThat(stringMap(detailProperties.get("actualNetWeightKg")).get("description").toString())
|
||||
|
||||
+5
@@ -49,6 +49,9 @@ class PdaConvergenceOpenApiContractTest {
|
||||
String batchRequest = yaml.substring(
|
||||
yaml.indexOf(" BatchRequest:"),
|
||||
yaml.indexOf(" BatchItemRequest:"));
|
||||
String batchItemRequest = yaml.substring(
|
||||
yaml.indexOf(" BatchItemRequest:"),
|
||||
yaml.indexOf(" ReviewRequest:"));
|
||||
assertThat(taskLookup)
|
||||
.contains("name: cabinetNo", "name: loadingPlanId")
|
||||
.doesNotContain("name: containerNo");
|
||||
@@ -59,5 +62,7 @@ class PdaConvergenceOpenApiContractTest {
|
||||
assertThat(batchRequest)
|
||||
.contains("loadingPlanId:", "containerBookingId:")
|
||||
.doesNotContain("containerNo:");
|
||||
assertThat(batchItemRequest)
|
||||
.doesNotContain("releaseMode:", "reservationIds:", "locationIds:");
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -56,6 +56,7 @@ class PlatformSmartLoadingControllerContractTest {
|
||||
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);
|
||||
authority("delete","platform:firstleg:smart-loading:delete",AuthenticatedUser.class,long.class,long.class,DeleteRequest.class);
|
||||
authority("removeUnpicked","platform:firstleg:smart-loading:delete",AuthenticatedUser.class,long.class,RemoveUnpickedRequest.class);
|
||||
}
|
||||
@Test void openApiHasNoManualCreateOrPushStep() throws Exception {
|
||||
String yaml=java.nio.file.Files.readString(java.nio.file.Path.of("src/main/resources/openapi/firstleg-v1.yaml"));
|
||||
@@ -75,6 +76,7 @@ class PlatformSmartLoadingControllerContractTest {
|
||||
.contains("SmartLoadingAssignRequest:")
|
||||
.contains("SmartLoadingAdjustRequest:")
|
||||
.contains("SmartLoadingDeleteRequest:")
|
||||
.contains("SmartLoadingRemoveUnpickedRequest:")
|
||||
.contains("SmartLoadingWarehouseRemarkRequest:")
|
||||
.contains("/items/{itemId}/warehouse-remark:")
|
||||
.contains("需要 platform:firstleg:preload:edit 权限")
|
||||
@@ -83,6 +85,8 @@ class PlatformSmartLoadingControllerContractTest {
|
||||
.contains("items[].version 是递增后的 itemVersion")
|
||||
.contains("数量调整或动态增量不会覆盖已有 warehouseRemark")
|
||||
.contains("柜内总容积和总重量分别允许达到柜型额定值的150%")
|
||||
.contains("人工分配不限制订单渠道与柜次渠道一致")
|
||||
.contains("渠道只约束智能推荐")
|
||||
.contains("canAssign: {type: boolean}")
|
||||
.contains("canEdit: {type: boolean}")
|
||||
.contains("pickedPackageCount: {type: integer, minimum: 0}")
|
||||
|
||||
+33
@@ -134,9 +134,42 @@ class CabinetLoadingEvidenceServiceTest {
|
||||
verify(files, times(1)).upload(anyString(), anyLong(), anyLong(), anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipartReplayRejectsChangedContentBeforeUploadingAnotherFile() {
|
||||
var first = new MockMultipartFile("file", "photo.png", "image/png", new byte[] {1, 2, 3});
|
||||
var changed = new MockMultipartFile("file", "photo.png", "image/png", new byte[] {1, 2, 4});
|
||||
CabinetLoadingEvidenceEntity existing = evidence("PHOTO_STEP_1");
|
||||
existing.setSourceTaskId(20L);
|
||||
existing.setOperationKey("same-key");
|
||||
existing.setRequestHash(uploadHashForTest(8L, 0, "PHOTO_STEP_1", null, "remark", first));
|
||||
when(mapper.byOperation(20L, "same-key")).thenReturn(existing);
|
||||
|
||||
assertThatThrownBy(() -> service.upload(20L, 8L, "same-key", 0, "PHOTO_STEP_1",
|
||||
null, "remark", changed, 9L, "operator"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("请求内容不一致");
|
||||
|
||||
verify(files, never()).upload(anyString(), anyLong(), anyLong(), anyString(), any());
|
||||
}
|
||||
|
||||
private static CabinetLoadingEvidenceEntity evidence(String type) {
|
||||
CabinetLoadingEvidenceEntity value = new CabinetLoadingEvidenceEntity();
|
||||
value.setEvidenceType(type); value.setStatus("ACTIVE"); value.setContainerBookingId(99L);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String uploadHashForTest(long warehouseId, int expectedVersion, String evidenceType,
|
||||
String sealNo, String remark, MockMultipartFile multipart) {
|
||||
try {
|
||||
var digest = java.security.MessageDigest.getInstance("SHA-256");
|
||||
String metadata = warehouseId + "|" + expectedVersion + "|" + evidenceType + "|"
|
||||
+ java.util.Objects.toString(sealNo, "") + "|" + java.util.Objects.toString(remark, "") + "|"
|
||||
+ java.util.Objects.toString(multipart.getOriginalFilename(), "") + "|"
|
||||
+ java.util.Objects.toString(multipart.getContentType(), "") + "|" + multipart.getSize() + "|";
|
||||
digest.update(metadata.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
digest.update(multipart.getBytes());
|
||||
return java.util.HexFormat.of().formatHex(digest.digest());
|
||||
} catch (java.io.IOException | java.security.NoSuchAlgorithmException exception) {
|
||||
throw new IllegalStateException(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -6,6 +6,7 @@ import com.baiyue.firstleg.loading.repository.LoadingRepository;
|
||||
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingPickItemMapper.InboundProgress;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import com.baiyue.firstleg.service.FbaPricingService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -27,6 +28,7 @@ class LoadingDeliveryServiceTest {
|
||||
@MockBean LoadingCompletionGateService completionGate;
|
||||
@MockBean InboundStatusHistoryMapper histories;
|
||||
@MockBean CabinetLoadingEvidenceService cabinetEvidence;
|
||||
@MockBean FbaPricingService pricingService;
|
||||
@MockBean ObjectMapper objectMapper;
|
||||
@Resource LoadingDeliveryService service;
|
||||
|
||||
@@ -46,6 +48,7 @@ class LoadingDeliveryServiceTest {
|
||||
new com.baiyue.firstleg.loading.dto.LoadingRequests.CompleteLoadingRequest(3, "done"));
|
||||
|
||||
verify(cabinetEvidence).requireComplete(99L);
|
||||
verify(pricingService).autoConfirmAfterLoading(10L, 20L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -86,6 +89,7 @@ class LoadingDeliveryServiceTest {
|
||||
|
||||
verify(repository).findInboundForUpdate(10L, 21L);
|
||||
verify(repository).completeLoadedChild(10L, 20L, 9L, 9);
|
||||
verify(pricingService).autoConfirmAfterLoading(10L, 21L);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+71
-51
@@ -1,84 +1,104 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baiyue.firstleg.entity.InboundPalletAllocationEntity;
|
||||
import com.baiyue.firstleg.mapper.InboundPalletAllocationMapper;
|
||||
import com.baiyue.firstleg.client.WarehousePalletLocationClient;
|
||||
import com.baiyue.firstleg.entity.InboundPalletAllocationEntity;
|
||||
import com.baiyue.firstleg.entity.InboundWeightResultEntity;
|
||||
import com.baiyue.firstleg.mapper.InboundPalletAllocationMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.List;
|
||||
import java.math.BigDecimal;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InboundPalletAllocationRepositoryTest {
|
||||
@Mock
|
||||
private InboundPalletAllocationMapper mapper;
|
||||
@InjectMocks
|
||||
private InboundPalletAllocationRepository repository;
|
||||
@Mock private InboundPalletAllocationMapper mapper;
|
||||
@InjectMocks private InboundPalletAllocationRepository repository;
|
||||
|
||||
@Test
|
||||
void acceptsReleasedSnapshotWhenRetryUpdateAffectsNoRows() {
|
||||
InboundPalletAllocationEntity current = allocation(1L, 91L, "RELEASED", 10, 10);
|
||||
when(mapper.markReleased(10001L, 31L, 91L)).thenReturn(0);
|
||||
when(mapper.selectOne(any())).thenReturn(current);
|
||||
void savesEveryBlockSlotIncludingReservedEmptySlots() {
|
||||
var result = new WarehousePalletLocationClient.BlockResult(700L, "OCCUPIED", List.of(
|
||||
resource(700L, 91L, 11L, "A01", 1, false),
|
||||
resource(700L, 92L, 12L, "A02", 2, false),
|
||||
resource(700L, 93L, 99L, "A0", 3, true)));
|
||||
|
||||
assertThatCode(() -> repository.markReleased(10001L, 31L, List.of(91L)))
|
||||
.doesNotThrowAnyException();
|
||||
repository.replaceBlock(10001L, 31L, 88L, 20, new BigDecimal("2.5000"), result);
|
||||
|
||||
ArgumentCaptor<InboundPalletAllocationEntity> inserted =
|
||||
ArgumentCaptor.forClass(InboundPalletAllocationEntity.class);
|
||||
verify(mapper, times(3)).insert(inserted.capture());
|
||||
assertThat(inserted.getAllValues()).extracting(InboundPalletAllocationEntity::getSlotNo)
|
||||
.containsExactly(1, 2, 3);
|
||||
assertThat(inserted.getAllValues()).extracting(InboundPalletAllocationEntity::getWeightResultId)
|
||||
.containsOnlyNulls();
|
||||
assertThat(inserted.getAllValues()).extracting(InboundPalletAllocationEntity::getSlotStatus)
|
||||
.containsOnly("RESERVED_EMPTY");
|
||||
assertThat(inserted.getAllValues().get(2).getSharedOverflow()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsTransferredSnapshotWhenRetryUpdateAffectsNoRows() {
|
||||
InboundPalletAllocationEntity source = allocation(1L, 91L, "CONFIRMED", 10, 2);
|
||||
InboundPalletAllocationEntity current = allocation(1L, 91L, "TRANSFERRED", 10, 2);
|
||||
when(mapper.markTransferred(1L, 10001L, 31L)).thenReturn(0);
|
||||
when(mapper.selectOne(any())).thenReturn(current);
|
||||
void bindsConfirmedWeightsByConfirmationTimeToTheNextEmptySlot() {
|
||||
InboundPalletAllocationEntity firstEmpty = allocation(1L, 1);
|
||||
InboundPalletAllocationEntity secondEmpty = allocation(2L, 2);
|
||||
when(mapper.selectNextEmptyForUpdate(10001L, 31L)).thenReturn(firstEmpty, secondEmpty);
|
||||
when(mapper.bindWeight(any(Long.class), any(Long.class), any(Long.class), any(Long.class),
|
||||
any(Integer.class), any(Long.class), any(String.class), any(BigDecimal.class))).thenReturn(1);
|
||||
InboundWeightResultEntity later = weight(102L, LocalDateTime.of(2026, 9, 4, 10, 2));
|
||||
InboundWeightResultEntity earlier = weight(101L, LocalDateTime.of(2026, 9, 4, 10, 1));
|
||||
|
||||
assertThatCode(() -> repository.markTransferred(10001L, 31L, List.of(source), List.of()))
|
||||
.doesNotThrowAnyException();
|
||||
repository.bindConfirmedWeights(10001L, 31L, List.of(later, earlier));
|
||||
|
||||
org.mockito.InOrder order = org.mockito.Mockito.inOrder(mapper);
|
||||
order.verify(mapper).bindWeight(1L, 10001L, 31L, 101L, 1, 501L, "P-501",
|
||||
new BigDecimal("1.2000"));
|
||||
order.verify(mapper).bindWeight(2L, 10001L, 31L, 102L, 2, 501L, "P-501",
|
||||
new BigDecimal("1.2000"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void restoresReservationNumbersByReservationIdWhenPalletIsReused() {
|
||||
InboundPalletAllocationEntity first = allocation(1L, 91L, "RESERVED", 1, 0);
|
||||
first.setPalletId(501L);
|
||||
first.setReservationNo("LR-91");
|
||||
InboundPalletAllocationEntity second = allocation(2L, 92L, "RESERVED", 1, 0);
|
||||
second.setPalletId(501L);
|
||||
second.setReservationNo("LR-92");
|
||||
when(mapper.selectList(any())).thenReturn(List.of(first, second));
|
||||
void unbindRestoresOnlyTheBoundSlotToReservedEmpty() {
|
||||
InboundPalletAllocationEntity bound = allocation(1L, 1);
|
||||
bound.setWeightResultId(101L);
|
||||
when(mapper.selectOne(any())).thenReturn(bound);
|
||||
when(mapper.unbindWeight(1L, 10001L, 31L, 101L)).thenReturn(1);
|
||||
|
||||
var result = new WarehousePalletLocationClient.BatchResult(2, List.of(
|
||||
new WarehousePalletLocationClient.AllocationResult(91L, null, 1, 501L, "P1", new BigDecimal("1.2"), 11L, "A1",
|
||||
new BigDecimal("1.2"), "OCCUPIED"),
|
||||
new WarehousePalletLocationClient.AllocationResult(92L, null, 2, 501L, "P1", new BigDecimal("1.2"), 12L, "A2",
|
||||
new BigDecimal("1.2"), "OCCUPIED")));
|
||||
repository.unbindWeight(10001L, 31L, 101L);
|
||||
|
||||
repository.replace(10001L, 31L, 88L, 0L, 2, result);
|
||||
|
||||
ArgumentCaptor<InboundPalletAllocationEntity> inserted = ArgumentCaptor.forClass(InboundPalletAllocationEntity.class);
|
||||
verify(mapper, org.mockito.Mockito.times(2)).insert(inserted.capture());
|
||||
assertThat(inserted.getAllValues()).extracting(InboundPalletAllocationEntity::getReservationNo)
|
||||
.containsExactly("LR-91", "LR-92");
|
||||
verify(mapper).unbindWeight(1L, 10001L, 31L, 101L);
|
||||
}
|
||||
|
||||
private static InboundPalletAllocationEntity allocation(long id, long reservationId, String status,
|
||||
int allocated, int picked) {
|
||||
InboundPalletAllocationEntity entity = new InboundPalletAllocationEntity();
|
||||
entity.setId(id);
|
||||
entity.setReservationId(reservationId);
|
||||
entity.setAllocationStatus(status);
|
||||
entity.setAllocatedPackageCount(allocated);
|
||||
entity.setPickedPackageCount(picked);
|
||||
return entity;
|
||||
private static WarehousePalletLocationClient.BlockResource resource(
|
||||
long blockId, long reservationId, long locationId, String code, int slotNo, boolean overflow) {
|
||||
return new WarehousePalletLocationClient.BlockResource(
|
||||
blockId, reservationId, locationId, code, slotNo, overflow, "OCCUPIED");
|
||||
}
|
||||
|
||||
private static InboundPalletAllocationEntity allocation(long id, int slotNo) {
|
||||
InboundPalletAllocationEntity value = new InboundPalletAllocationEntity();
|
||||
value.setId(id);
|
||||
value.setSlotNo(slotNo);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static InboundWeightResultEntity weight(long id, LocalDateTime confirmedAt) {
|
||||
InboundWeightResultEntity value = new InboundWeightResultEntity();
|
||||
value.setId(id);
|
||||
value.setRecognitionStatus("CONFIRMED");
|
||||
value.setConfirmedAt(confirmedAt);
|
||||
value.setPalletId(501L);
|
||||
value.setPalletCode("P-501");
|
||||
value.setPalletCapacityCbm(new BigDecimal("1.2000"));
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -90,6 +90,7 @@ class SmartLoadingCandidateMapperContractTest {
|
||||
.contains("status.name() == 'WAIT_MEASURE'")
|
||||
.contains("status.name() == 'WAIT_SPLIT'")
|
||||
.contains("status.name() == 'LOADING'")
|
||||
.contains("NOT EXISTS (", "active_item.inbound_id=o.id", "active_item.deleted=0")
|
||||
.contains("current_group.id groupId")
|
||||
.contains("group_member_count.group_id=current_group.id")
|
||||
.doesNotContain("fl_container_booking")
|
||||
|
||||
@@ -36,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.never;
|
||||
@@ -69,6 +70,48 @@ class FbaPricingServiceTest {
|
||||
verify(repository, never()).findLatestFee(TENANT, INBOUND);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("装柜完成后系统自动确认客户未确认的已发送费用")
|
||||
void automaticallyConfirmsSentFeeAfterLoading() {
|
||||
InboundFeeEntity sent = savedFee("SENT");
|
||||
when(repository.findLatestFeeForUpdate(TENANT, INBOUND)).thenReturn(sent);
|
||||
when(repository.confirmFeeByCustomer(TENANT, 501L, 1, 0L,
|
||||
"装柜完成自动确认", NOW)).thenReturn(1);
|
||||
|
||||
service.autoConfirmAfterLoading(TENANT, INBOUND);
|
||||
|
||||
verify(repository).confirmFeeByCustomer(TENANT, 501L, 1, 0L,
|
||||
"装柜完成自动确认", NOW);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("客户已经确认时装柜完成不覆盖原确认信息")
|
||||
void keepsExistingCustomerConfirmationAfterLoading() {
|
||||
InboundFeeEntity sent = savedFee("SENT");
|
||||
sent.setCustomerConfirmedBy(88L);
|
||||
sent.setCustomerConfirmedByName("客户成员");
|
||||
sent.setCustomerConfirmedAt(NOW.minusHours(1));
|
||||
when(repository.findLatestFeeForUpdate(TENANT, INBOUND)).thenReturn(sent);
|
||||
|
||||
service.autoConfirmAfterLoading(TENANT, INBOUND);
|
||||
|
||||
verify(repository, never()).confirmFeeByCustomer(anyLong(), anyLong(), anyInt(),
|
||||
anyLong(), anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("没有已发送费用时装柜完成不受影响")
|
||||
void ignoresMissingOrUnsentFeeAfterLoading() {
|
||||
when(repository.findLatestFeeForUpdate(TENANT, INBOUND))
|
||||
.thenReturn(null, savedFee("CONFIRMED"));
|
||||
|
||||
service.autoConfirmAfterLoading(TENANT, INBOUND);
|
||||
service.autoConfirmAfterLoading(TENANT, INBOUND);
|
||||
|
||||
verify(repository, never()).confirmFeeByCustomer(anyLong(), anyLong(), anyInt(),
|
||||
anyLong(), anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("包税KG按生效价格表计费且不依赖客户规则或承运商")
|
||||
void calculatesTaxIncludedKgWithoutCustomerRuleOrCarrier() {
|
||||
@@ -98,7 +141,7 @@ class FbaPricingServiceTest {
|
||||
assertThat(fee.getValue().getPricingTaxMode()).isEqualTo("TAX_INCLUDED");
|
||||
assertThat(fee.getValue().getChargeRuleId()).isNull();
|
||||
assertThat(fee.getValue().getCarrierId()).isNull();
|
||||
assertThat(fee.getValue().getCustomerId()).isEqualTo(11L);
|
||||
assertThat(fee.getValue().getCustomerId()).isEqualTo(TENANT);
|
||||
assertThat(fee.getValue().getConversionFactor()).isEqualByComparingTo("167");
|
||||
assertThat(fee.getValue().getRoundingScale()).isEqualTo(6);
|
||||
assertThat(fee.getValue().getRoundingMode()).isEqualTo("HALF_UP");
|
||||
@@ -134,19 +177,37 @@ class FbaPricingServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("缺少订单客户归属时拒绝生成对客费用")
|
||||
void rejectsMissingOrderCustomerOwnership() {
|
||||
@DisplayName("指定实际测量来源时必须存在测量批次和明细")
|
||||
void requiresActualMeasurementDetailsForActualSource() {
|
||||
readyRepository();
|
||||
InboundPricingEntity inbound = inbound("100.000", "0.7880");
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
|
||||
assertThatThrownBy(() -> service.calculate(TENANT, OPERATOR, INBOUND, NOW,
|
||||
new BigDecimal("100.000"), new BigDecimal("0.7880"),
|
||||
"ACTUAL", "ACTUAL", "calc-actual-missing"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("实际测量数据不存在");
|
||||
|
||||
verify(priceBookEstimator, never()).estimate(any());
|
||||
verify(repository, never()).insertFee(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("订单客户字段为空时使用租户归属生成对客费用")
|
||||
void usesTenantOwnershipWhenOrderCustomerIsMissing() {
|
||||
readyRepository();
|
||||
InboundPricingEntity inbound = inbound("200.000", "0.2000");
|
||||
inbound.setCustomerId(null);
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
when(priceBookEstimator.estimate(any())).thenReturn(
|
||||
estimate("KG", "200.000", "2.000000", "400.00", "0.00"));
|
||||
|
||||
assertThatThrownBy(() -> service.calculate(
|
||||
TENANT, OPERATOR, INBOUND, NOW, "calc-no-customer"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("缺少订单客户归属");
|
||||
verify(priceBookEstimator, never()).estimate(any());
|
||||
verify(repository, never()).insertFee(any());
|
||||
assertThat(service.calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-no-customer"))
|
||||
.isNotNull();
|
||||
ArgumentCaptor<InboundFeeEntity> fee = ArgumentCaptor.forClass(InboundFeeEntity.class);
|
||||
verify(repository).insertFee(fee.capture());
|
||||
assertThat(fee.getValue().getCustomerId()).isEqualTo(TENANT);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -12,9 +12,11 @@ import com.baiyue.firstleg.dto.InboundMeasurementResponses;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.entity.InboundPalletAllocationEntity;
|
||||
import com.baiyue.firstleg.entity.InboundWeightResultEntity;
|
||||
import com.baiyue.firstleg.entity.InboundWeightRevisionEntity;
|
||||
import com.baiyue.firstleg.repository.InboundMeasurementRepository;
|
||||
import com.baiyue.firstleg.repository.InboundPalletAllocationRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
@@ -58,6 +60,7 @@ class InboundMeasurementServiceTest {
|
||||
@MockBean private OrderCargoClient orderCargoClient;
|
||||
@MockBean private MeasurementImageClient imageClient;
|
||||
@MockBean private WarehousePalletLocationClient warehousePallets;
|
||||
@MockBean private InboundPalletAllocationRepository palletAllocations;
|
||||
@Resource private InboundMeasurementService service;
|
||||
|
||||
@BeforeEach
|
||||
@@ -205,6 +208,10 @@ class InboundMeasurementServiceTest {
|
||||
when(repository.findLatestMeasurement(1L, 10L)).thenReturn(measurement);
|
||||
when(repository.listItems(1L, 30L)).thenReturn(List.of(item));
|
||||
when(repository.listWeights(1L, 10L)).thenReturn(List.of(weight));
|
||||
InboundPalletAllocationEntity bound = locationSlot(1, 801L, "A01", "BOUND", 50L);
|
||||
InboundPalletAllocationEntity secondBound = locationSlot(2, 802L, "A02", "BOUND", 51L);
|
||||
InboundPalletAllocationEntity empty = locationSlot(3, 803L, "A03", "RESERVED_EMPTY", null);
|
||||
when(palletAllocations.list(1L, 10L)).thenReturn(List.of(empty, secondBound, bound));
|
||||
|
||||
InboundMeasurementResponses.Detail result = service.detail(1L, 10L);
|
||||
|
||||
@@ -224,6 +231,16 @@ class InboundMeasurementServiceTest {
|
||||
assertThat(confirmedWeight.grossWeightKg()).isEqualByComparingTo("30.000");
|
||||
assertThat(confirmedWeight.netWeightKg()).isEqualByComparingTo("11.500");
|
||||
assertThat(confirmedWeight.confirmedAt()).isNotNull();
|
||||
assertThat(confirmedWeight.locationCode()).isEqualTo("A01");
|
||||
});
|
||||
assertThat(result.palletLocationGuide()).satisfies(guide -> {
|
||||
assertThat(guide.blockId()).isEqualTo(700L);
|
||||
assertThat(guide.reservedSlotCount()).isEqualTo(3);
|
||||
assertThat(guide.actualPalletCount()).isEqualTo(2);
|
||||
assertThat(guide.reservedEmptySlotCount()).isEqualTo(1);
|
||||
assertThat(guide.slots()).extracting(InboundMeasurementResponses.PalletLocationSlot::locationCode)
|
||||
.containsExactly("A01", "A02", "A03");
|
||||
assertThat(guide.slots().get(2).boundWeightResultId()).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -537,6 +554,23 @@ class InboundMeasurementServiceTest {
|
||||
assertThat(revision.getValue().getNetWeightKg()).isEqualByComparingTo("11.500");
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmingAWeightBindsItToTheNextEmptySlotWhenTheTicketAlreadyHasABlock() {
|
||||
InboundOrderEntity inbound = inbound("MEASURE_REJECTED");
|
||||
InboundWeightResultEntity weight = weight(20L, "30.000", "KG", "PENDING_CONFIRM", 0);
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.findWeight(1L, 10L, 20L)).thenReturn(weight);
|
||||
when(repository.updateWeight(weight)).thenReturn(true);
|
||||
when(repository.listWeights(1L, 10L)).thenReturn(List.of(weight));
|
||||
when(palletAllocations.list(1L, 10L)).thenReturn(List.of(
|
||||
locationSlot(1, 801L, "A01", "RESERVED_EMPTY", null)));
|
||||
|
||||
service.confirmWeightResult(1L, 10L, 20L, 9L, "操作员",
|
||||
new InboundMeasurementRequests.ConfirmWeightResultRequest(0, null, null, null));
|
||||
|
||||
verify(palletAllocations).bindNextEmpty(1L, 10L, weight);
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmingFifthPalletSeparatelyAggregatesGrossAndCargoNetWeight() {
|
||||
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
|
||||
@@ -834,6 +868,26 @@ class InboundMeasurementServiceTest {
|
||||
verify(repository).updateInboundWeightSummary(inbound);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deletingBoundWeightUnbindsTheSlotBeforeDeletingTheResultAndFailsOnFileError() {
|
||||
InboundOrderEntity inbound = inbound("MEASURING");
|
||||
InboundWeightResultEntity weight = weight(20L, "20.000", "KG", "CONFIRMED", 1);
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.findWeight(1L, 10L, 20L)).thenReturn(weight);
|
||||
when(repository.deleteWeight(1L, 10L, 20L, 1)).thenReturn(true);
|
||||
when(imageClient.delete("internal-token", 9L, 1L, 10L, 120L))
|
||||
.thenReturn(ApiResponse.failure(com.baiyue.common.core.api.ResultCode.SERVICE_UNAVAILABLE,
|
||||
"file unavailable", null, null));
|
||||
|
||||
assertThatThrownBy(() -> service.deleteWeightResult(1L, 10L, 20L, 1, 9L))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("file unavailable");
|
||||
|
||||
org.mockito.InOrder order = org.mockito.Mockito.inOrder(palletAllocations, repository, imageClient);
|
||||
order.verify(palletAllocations).unbindWeight(1L, 10L, 20L);
|
||||
order.verify(repository).deleteWeight(1L, 10L, 20L, 1);
|
||||
order.verify(imageClient).delete("internal-token", 9L, 1L, 10L, 120L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deletingLastConfirmedWeightClearsInboundTotal() {
|
||||
InboundOrderEntity inbound = inbound("MEASURING");
|
||||
@@ -1051,6 +1105,21 @@ class InboundMeasurementServiceTest {
|
||||
verify(batchService, never()).saveBatch(any());
|
||||
}
|
||||
|
||||
private static InboundPalletAllocationEntity locationSlot(
|
||||
int slotNo, long locationId, String locationCode, String slotStatus, Long weightResultId) {
|
||||
InboundPalletAllocationEntity value = new InboundPalletAllocationEntity();
|
||||
value.setBlockId(700L);
|
||||
value.setSlotNo(slotNo);
|
||||
value.setReservationId(900L + slotNo);
|
||||
value.setLocationId(locationId);
|
||||
value.setLocationCode(locationCode);
|
||||
value.setSharedOverflow(false);
|
||||
value.setSlotStatus(slotStatus);
|
||||
value.setWeightResultId(weightResultId);
|
||||
value.setPalletSequence(weightResultId == null ? null : slotNo);
|
||||
return value;
|
||||
}
|
||||
|
||||
private InboundOrderEntity inbound(String status) {
|
||||
InboundOrderEntity inbound = new InboundOrderEntity();
|
||||
inbound.setId(10L);
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.baiyue.firstleg.dto.LoadingPickDtos.SkipRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.BatchRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.BatchItemRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.ClaimRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.AssignRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.CompleteLoadingRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.Task;
|
||||
import com.baiyue.firstleg.client.WarehousePalletLocationClient;
|
||||
@@ -341,7 +342,7 @@ class LoadingPickServiceTest {
|
||||
preparePick(task, item, "pick-1");
|
||||
when(repository.allTaskItemsResolved(20L)).thenReturn(false);
|
||||
|
||||
service.pick(20L, 30L, new PickRequest(101L, 2, List.of(), 0, "pick-1"), 9L, "operator");
|
||||
service.pick(20L, 30L, new PickRequest(101L, 2, 0, "pick-1"), 9L, "operator");
|
||||
|
||||
verify(repository).touchTask(20L, 0);
|
||||
verify(repository).syncInboundProjections(20L, 9L, "operator");
|
||||
@@ -357,7 +358,7 @@ class LoadingPickServiceTest {
|
||||
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
|
||||
when(repository.allPlanItemsPicked(10L)).thenReturn(true);
|
||||
|
||||
service.pick(20L, 30L, new PickRequest(101L, 2, List.of(), 0, "pick-2"), 9L, "operator");
|
||||
service.pick(20L, 30L, new PickRequest(101L, 2, 0, "pick-2"), 9L, "operator");
|
||||
|
||||
verify(repository).touchTask(20L, 0);
|
||||
verify(settlementService, never()).settle(20L, 9L, "operator");
|
||||
@@ -373,7 +374,7 @@ class LoadingPickServiceTest {
|
||||
preparePick(task, item, "pick-start-loading");
|
||||
when(smartLoadingRepository.startPicking(10L, 9L, "operator")).thenReturn(1);
|
||||
|
||||
service.pick(20L, 30L, new PickRequest(101L, 2, List.of(), 0, "pick-start-loading"),
|
||||
service.pick(20L, 30L, new PickRequest(101L, 2, 0, "pick-start-loading"),
|
||||
9L, "operator");
|
||||
|
||||
verify(smartLoadingRepository).startPicking(10L, 9L, "operator");
|
||||
@@ -392,7 +393,7 @@ class LoadingPickServiceTest {
|
||||
new LoadingInboundMapper.Snapshot(50L, 101L, "WAIT_SPLIT", 4));
|
||||
when(repository.beginInboundLoading(101L, 50L, 9L, 4)).thenReturn(1);
|
||||
|
||||
service.pick(20L, 30L, new PickRequest(101L, 2, List.of(), 0, "pick-start-inbound"),
|
||||
service.pick(20L, 30L, new PickRequest(101L, 2, 0, "pick-start-inbound"),
|
||||
9L, "operator");
|
||||
|
||||
verify(repository).beginInboundLoading(101L, 50L, 9L, 4);
|
||||
@@ -418,7 +419,7 @@ class LoadingPickServiceTest {
|
||||
new LoadingInboundMapper.Snapshot(50L, 101L, "LOADING", 5));
|
||||
when(repository.beginInboundLoading(101L, 50L, 9L, 4)).thenReturn(0);
|
||||
|
||||
service.pick(20L, 30L, new PickRequest(101L, 2, List.of(), 0, "pick-concurrent-inbound"),
|
||||
service.pick(20L, 30L, new PickRequest(101L, 2, 0, "pick-concurrent-inbound"),
|
||||
9L, "operator");
|
||||
|
||||
verify(repository).beginInboundLoading(101L, 50L, 9L, 4);
|
||||
@@ -513,6 +514,104 @@ class LoadingPickServiceTest {
|
||||
verify(repository, times(1)).createAssignment(20L, 9L, "operator", 9L, "operator", "CLAIMED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void claimRechecksSameOperationAfterWaitingForTaskLock() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
LoadingPickOperationEntity completed = taskOperation("claim-race", "CLAIM");
|
||||
when(repository.operation("claim-race")).thenReturn(null);
|
||||
when(repository.operationForUpdate("claim-race")).thenReturn(completed);
|
||||
when(repository.taskForUpdate(20L)).thenReturn(task);
|
||||
when(repository.task(20L)).thenReturn(task);
|
||||
when(repository.items(20L)).thenReturn(List.of());
|
||||
|
||||
Task replay = service.claim(20L, new ClaimRequest(0, "claim-race"), 9L, "operator");
|
||||
|
||||
assertThat(replay.id()).isEqualTo(20L);
|
||||
verify(repository, never()).createAssignment(anyLong(), anyLong(), anyString(), anyLong(),
|
||||
anyString(), anyString());
|
||||
verify(smartLoadingRepository, never()).startPicking(anyLong(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void assignRechecksSameOperationAfterWaitingForTaskLock() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
LoadingPickOperationEntity completed = taskOperation("assign-race", "ASSIGN");
|
||||
when(repository.operation("assign-race")).thenReturn(null);
|
||||
when(repository.operationForUpdate("assign-race")).thenReturn(completed);
|
||||
when(repository.taskForUpdate(20L)).thenReturn(task);
|
||||
when(repository.task(20L)).thenReturn(task);
|
||||
when(repository.items(20L)).thenReturn(List.of());
|
||||
|
||||
Task replay = service.assign(20L, new AssignRequest(11L, "picker", 0, "assign-race"),
|
||||
9L, "operator");
|
||||
|
||||
assertThat(replay.id()).isEqualTo(20L);
|
||||
verify(repository, never()).createAssignment(anyLong(), anyLong(), anyString(), anyLong(),
|
||||
anyString(), anyString());
|
||||
verify(repository, never()).reassign(anyLong(), anyLong(), anyString(), anyLong(),
|
||||
anyString(), anyString(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pickRechecksSameHistoryAfterWaitingForTaskLock() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
LoadingPickHistoryEntity completed = itemHistory("pick-race", "PICK",
|
||||
"20|30|101|2");
|
||||
when(repository.history("pick-race")).thenReturn(null);
|
||||
when(repository.historyForUpdate("pick-race")).thenReturn(completed);
|
||||
when(repository.taskForUpdate(20L)).thenReturn(task);
|
||||
when(repository.assignment(20L)).thenReturn(assignment());
|
||||
when(repository.items(20L, 101L)).thenReturn(List.of(item()));
|
||||
|
||||
Task replay = service.pick(20L, 30L,
|
||||
new PickRequest(101L, 2, 0, "pick-race"), 9L, "operator");
|
||||
|
||||
assertThat(replay.id()).isEqualTo(20L);
|
||||
verify(repository, never()).pick(anyLong(), anyInt(), anyInt(), anyLong());
|
||||
verify(repository, never()).insert(any(LoadingPickHistoryEntity.class));
|
||||
verify(repository, never()).touchTask(anyLong(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void partialRechecksSameHistoryAfterWaitingForTaskLock() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
LoadingPickHistoryEntity completed = itemHistory("partial-race", "PARTIAL",
|
||||
"PARTIAL|20|30|101|1");
|
||||
when(repository.history("partial-race")).thenReturn(null);
|
||||
when(repository.historyForUpdate("partial-race")).thenReturn(completed);
|
||||
when(repository.taskForUpdate(20L)).thenReturn(task);
|
||||
when(repository.assignment(20L)).thenReturn(assignment());
|
||||
when(repository.items(20L, 101L)).thenReturn(List.of(item()));
|
||||
|
||||
Task replay = service.partial(20L, 30L,
|
||||
new PartialPickRequest(101L, 1, 0, "partial-race"), 9L, "operator");
|
||||
|
||||
assertThat(replay.id()).isEqualTo(20L);
|
||||
verify(repository, never()).partial(anyLong(), anyInt(), anyInt(), anyLong());
|
||||
verify(repository, never()).insert(any(LoadingPickHistoryEntity.class));
|
||||
verify(repository, never()).touchTask(anyLong(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void skipRechecksSameHistoryAfterWaitingForTaskLock() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
LoadingPickHistoryEntity completed = itemHistory("skip-race", "SKIP",
|
||||
"SKIP|20|30|101|damaged");
|
||||
when(repository.history("skip-race")).thenReturn(null);
|
||||
when(repository.historyForUpdate("skip-race")).thenReturn(completed);
|
||||
when(repository.taskForUpdate(20L)).thenReturn(task);
|
||||
when(repository.assignment(20L)).thenReturn(assignment());
|
||||
when(repository.items(20L, 101L)).thenReturn(List.of(item()));
|
||||
|
||||
Task replay = service.skip(20L, 30L,
|
||||
new SkipRequest(101L, 0, "skip-race", "damaged"), 9L, "operator");
|
||||
|
||||
assertThat(replay.id()).isEqualTo(20L);
|
||||
verify(repository, never()).skip(anyLong(), anyInt(), anyLong());
|
||||
verify(repository, never()).insert(any(LoadingPickHistoryEntity.class));
|
||||
verify(repository, never()).touchTask(anyLong(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsPartialPickLockedUntilManualCompletion() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
@@ -521,7 +620,7 @@ class LoadingPickServiceTest {
|
||||
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
|
||||
when(repository.allPlanItemsPicked(10L)).thenReturn(true);
|
||||
|
||||
service.partial(20L, 30L, new PartialPickRequest(101L, 1, List.of(), 0, "partial-1"),
|
||||
service.partial(20L, 30L, new PartialPickRequest(101L, 1, 0, "partial-1"),
|
||||
9L, "operator");
|
||||
|
||||
verify(repository).touchTask(20L, 0);
|
||||
@@ -1046,7 +1145,7 @@ class LoadingPickServiceTest {
|
||||
when(repository.assignment(20L)).thenReturn(assignment());
|
||||
when(repository.items(20L)).thenReturn(items);
|
||||
when(repository.items(20L, 101L)).thenReturn(items);
|
||||
when(repository.pick(30L, 2, "MANUAL", "[]", 0, 101L)).thenReturn(1);
|
||||
when(repository.pick(30L, 2, 0, 101L)).thenReturn(1);
|
||||
when(repository.skip(31L, 0, 101L)).thenReturn(1);
|
||||
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
|
||||
when(repository.touchTask(20L, 0)).thenReturn(1);
|
||||
@@ -1056,13 +1155,13 @@ class LoadingPickServiceTest {
|
||||
71L, 72L, 0, 101L, 73L, 74L, "A-01", false, 2))));
|
||||
String snapshotToken = service.inventorySnapshot(20L, 101L).snapshotToken();
|
||||
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, snapshotToken, List.of(
|
||||
new BatchItemRequest(30L, "PICK", 2, null, "MANUAL", List.of(), 0, null),
|
||||
new BatchItemRequest(31L, "SKIP", null, null, null, null, 0, "damaged carton")));
|
||||
new BatchItemRequest(30L, "PICK", 2, null, 0, null),
|
||||
new BatchItemRequest(31L, "SKIP", null, null, 0, "damaged carton")));
|
||||
|
||||
service.batch(20L, "batch-1", request, 9L, "operator");
|
||||
service.batch(20L, "batch-1", request, 9L, "operator");
|
||||
|
||||
verify(repository, times(1)).pick(30L, 2, "MANUAL", "[]", 0, 101L);
|
||||
verify(repository, times(1)).pick(30L, 2, 0, 101L);
|
||||
verify(repository, times(1)).skip(31L, 0, 101L);
|
||||
verify(repository, times(1)).touchTask(20L, 0);
|
||||
verify(settlementService, never()).settle(20L, 9L, "operator");
|
||||
@@ -1087,7 +1186,7 @@ class LoadingPickServiceTest {
|
||||
when(repository.assignment(20L)).thenReturn(assignment());
|
||||
when(repository.items(20L)).thenReturn(List.of(item));
|
||||
when(repository.items(20L, 101L)).thenReturn(List.of(item));
|
||||
when(repository.pick(30L, 100, "AUTO", "[]", 0, 101L)).thenReturn(1);
|
||||
when(repository.pick(30L, 100, 0, 101L)).thenReturn(1);
|
||||
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
|
||||
when(repository.touchTask(20L, 0)).thenReturn(1);
|
||||
when(smartLoadingRepository.startPicking(10L, 9L, "operator")).thenReturn(1);
|
||||
@@ -1097,11 +1196,11 @@ class LoadingPickServiceTest {
|
||||
71L, 72L, 0, 101L, 73L, 74L, "A-01", false, 100))));
|
||||
String snapshotToken = service.inventorySnapshot(20L, 101L).snapshotToken();
|
||||
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, snapshotToken, List.of(
|
||||
new BatchItemRequest(30L, "PICK", 100, null, "AUTO", List.of(), 0, null)));
|
||||
new BatchItemRequest(30L, "PICK", 100, null, 0, null)));
|
||||
|
||||
service.batch(20L, "batch-reset-plan", request, 9L, "operator");
|
||||
|
||||
verify(repository).pick(30L, 100, "AUTO", "[]", 0, 101L);
|
||||
verify(repository).pick(30L, 100, 0, 101L);
|
||||
verify(smartLoadingRepository).startPicking(10L, 9L, "operator");
|
||||
verify(smartLoadingRepository, never()).insert(any(LoadingPlanHistoryEntity.class));
|
||||
verify(repository).touchTask(20L, 0);
|
||||
@@ -1123,14 +1222,14 @@ class LoadingPickServiceTest {
|
||||
when(repository.items(20L)).thenReturn(items);
|
||||
when(repository.items(20L, 101L)).thenReturn(items);
|
||||
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, "stale-token", List.of(
|
||||
new BatchItemRequest(30L, "PICK", 2, null, "MANUAL", List.of(), 0, null),
|
||||
new BatchItemRequest(31L, "PARTIAL", null, 4, "AUTO", null, 0, null)));
|
||||
new BatchItemRequest(30L, "PICK", 2, null, 0, null),
|
||||
new BatchItemRequest(31L, "PARTIAL", null, 4, 0, null)));
|
||||
|
||||
assertThatThrownBy(() -> service.batch(20L, "batch-invalid", request, 9L, "operator"))
|
||||
.isInstanceOf(BizException.class);
|
||||
|
||||
verify(repository, never()).pick(any(Long.class), any(Integer.class), any(), any(), any(Integer.class), any(Long.class));
|
||||
verify(repository, never()).partial(any(Long.class), any(Integer.class), any(), any(), any(Integer.class), any(Long.class));
|
||||
verify(repository, never()).pick(any(Long.class), any(Integer.class), any(Integer.class), any(Long.class));
|
||||
verify(repository, never()).partial(any(Long.class), any(Integer.class), any(Integer.class), any(Long.class));
|
||||
verify(repository, never()).touchTask(any(Long.class), any(Integer.class));
|
||||
verify(settlementService, never()).settle(20L, 9L, "operator");
|
||||
}
|
||||
@@ -1147,13 +1246,13 @@ class LoadingPickServiceTest {
|
||||
ApiResponse.success(List.of(new WarehousePalletLocationClient.OccupiedResource(
|
||||
71L, 72L, 0, 101L, 73L, 74L, "A-01", false, 2))));
|
||||
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, "0".repeat(64), List.of(
|
||||
new BatchItemRequest(30L, "PICK", 2, null, "MANUAL", List.of(), 0, null)));
|
||||
new BatchItemRequest(30L, "PICK", 2, null, 0, null)));
|
||||
|
||||
assertThatThrownBy(() -> service.batch(20L, "batch-inventory-stale", request, 9L, "operator"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("库存已变化");
|
||||
|
||||
verify(repository, never()).pick(any(Long.class), any(Integer.class), any(), any(),
|
||||
verify(repository, never()).pick(any(Long.class), any(Integer.class),
|
||||
any(Integer.class), any(Long.class));
|
||||
verify(repository, never()).touchTask(any(Long.class), any(Integer.class));
|
||||
verify(settlementService, never()).settle(20L, 9L, "operator");
|
||||
@@ -1165,7 +1264,7 @@ class LoadingPickServiceTest {
|
||||
task.setVersion(3);
|
||||
when(repository.taskForUpdate(20L)).thenReturn(task);
|
||||
BatchRequest request = new BatchRequest(101L, 10L, 99L, 2, "stale-token", List.of(
|
||||
new BatchItemRequest(30L, "PICK", 2, null, "AUTO", null, 0, null)));
|
||||
new BatchItemRequest(30L, "PICK", 2, null, 0, null)));
|
||||
|
||||
assertThatThrownBy(() -> service.batch(20L, "batch-stale", request, 9L, "operator"))
|
||||
.isInstanceOf(BizException.class);
|
||||
@@ -1183,15 +1282,14 @@ class LoadingPickServiceTest {
|
||||
when(repository.assignment(20L)).thenReturn(assignment());
|
||||
when(repository.items(20L)).thenReturn(List.of(item));
|
||||
when(repository.items(20L, 101L)).thenReturn(List.of(item));
|
||||
when(repository.pick(30L, 2, "MANUAL", "[]", 0, 101L)).thenReturn(1);
|
||||
when(repository.pick(30L, 2, 0, 101L)).thenReturn(1);
|
||||
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
|
||||
when(repository.touchTask(20L, 0)).thenReturn(1);
|
||||
when(warehouse.occupiedResources(any(), any(Long.class), any(), any(Long.class))).thenReturn(
|
||||
ApiResponse.success(List.of(new WarehousePalletLocationClient.OccupiedResource(
|
||||
71L, 72L, 0, 101L, 73L, 74L, "A-01", false, 2))));
|
||||
String snapshotToken = service.inventorySnapshot(20L, 101L).snapshotToken();
|
||||
BatchItemRequest line = new BatchItemRequest(
|
||||
30L, "PICK", 2, null, "MANUAL", List.of(), 0, null);
|
||||
BatchItemRequest line = new BatchItemRequest(30L, "PICK", 2, null, 0, null);
|
||||
|
||||
assertThatThrownBy(() -> service.batch(20L, "batch-stale", new BatchRequest(
|
||||
101L, 10L, 99L, 1, snapshotToken, List.of(line)), 9L, "operator"))
|
||||
@@ -1203,7 +1301,7 @@ class LoadingPickServiceTest {
|
||||
service.batch(20L, "batch-valid", new BatchRequest(
|
||||
101L, 10L, 99L, 0, snapshotToken, List.of(line)), 9L, "operator");
|
||||
|
||||
verify(repository, times(1)).pick(30L, 2, "MANUAL", "[]", 0, 101L);
|
||||
verify(repository, times(1)).pick(30L, 2, 0, 101L);
|
||||
verify(repository, times(1)).touchTask(20L, 0);
|
||||
}
|
||||
|
||||
@@ -1224,7 +1322,7 @@ class LoadingPickServiceTest {
|
||||
|
||||
BatchRequest staleRequest = new BatchRequest(101L, 10L, 99L, 1,
|
||||
"stale-token", List.of(
|
||||
new BatchItemRequest(30L, "PICK", 2, null, "AUTO", null, 0, null)));
|
||||
new BatchItemRequest(30L, "PICK", 2, null, 0, null)));
|
||||
assertThatThrownBy(() -> service.batch(20L, "batch-stale-after-resync", staleRequest,
|
||||
9L, "operator")).isInstanceOf(BizException.class);
|
||||
|
||||
@@ -1232,7 +1330,7 @@ class LoadingPickServiceTest {
|
||||
assertThat(pending.getPickedPackageCount()).isZero();
|
||||
assertThat(pending.getItemStatus()).isEqualTo("PENDING");
|
||||
verify(repository, never()).update(any(LoadingPickItemEntity.class));
|
||||
verify(repository, never()).pick(any(Long.class), any(Integer.class), any(), any(),
|
||||
verify(repository, never()).pick(any(Long.class), any(Integer.class),
|
||||
any(Integer.class), any(Long.class));
|
||||
verify(repository, never()).touchTask(any(Long.class), any(Integer.class));
|
||||
}
|
||||
@@ -1251,7 +1349,7 @@ class LoadingPickServiceTest {
|
||||
when(repository.taskForUpdate(20L)).thenReturn(task);
|
||||
when(repository.assignment(20L)).thenReturn(assignment());
|
||||
when(repository.item(20L, 30L, 101L)).thenReturn(item);
|
||||
when(repository.pick(30L, 2, "MANUAL", "[]", 0, 101L)).thenReturn(1);
|
||||
when(repository.pick(30L, 2, 0, 101L)).thenReturn(1);
|
||||
when(repository.touchTask(20L, 0)).thenReturn(1);
|
||||
when(repository.items(20L, 101L)).thenReturn(List.of(item));
|
||||
}
|
||||
@@ -1262,7 +1360,7 @@ class LoadingPickServiceTest {
|
||||
when(repository.taskForUpdate(20L)).thenReturn(task);
|
||||
when(repository.assignment(20L)).thenReturn(assignment());
|
||||
when(repository.item(20L, 30L, 101L)).thenReturn(item);
|
||||
when(repository.partial(30L, 1, "MANUAL", "[]", 0, 101L)).thenReturn(1);
|
||||
when(repository.partial(30L, 1, 0, 101L)).thenReturn(1);
|
||||
when(repository.touchTask(20L, 0)).thenReturn(1);
|
||||
when(repository.items(20L, 101L)).thenReturn(List.of(item));
|
||||
}
|
||||
@@ -1316,6 +1414,41 @@ class LoadingPickServiceTest {
|
||||
return assignment;
|
||||
}
|
||||
|
||||
private static LoadingPickOperationEntity taskOperation(String operationId, String action) {
|
||||
LoadingPickOperationEntity operation = new LoadingPickOperationEntity();
|
||||
operation.setPickTaskId(20L);
|
||||
operation.setOperationId(operationId);
|
||||
operation.setRequestHash(hashForTest(action + "|20|0"));
|
||||
if ("ASSIGN".equals(action)) {
|
||||
operation.setRequestHash(hashForTest("ASSIGN|20|11|picker|0"));
|
||||
}
|
||||
operation.setActionType(action);
|
||||
operation.setOperatorId(9L);
|
||||
operation.setOperatorName("operator");
|
||||
return operation;
|
||||
}
|
||||
|
||||
private static LoadingPickHistoryEntity itemHistory(String operationId, String action, String canonical) {
|
||||
LoadingPickHistoryEntity history = new LoadingPickHistoryEntity();
|
||||
history.setPickTaskId(20L);
|
||||
history.setPickItemId(30L);
|
||||
history.setOperationId(operationId);
|
||||
history.setRequestHash(hashForTest(canonical));
|
||||
history.setActionType(action);
|
||||
history.setOperatorId(9L);
|
||||
history.setOperatorName("operator");
|
||||
return history;
|
||||
}
|
||||
|
||||
private static String hashForTest(String value) {
|
||||
try {
|
||||
return java.util.HexFormat.of().formatHex(java.security.MessageDigest.getInstance("SHA-256")
|
||||
.digest(value.getBytes(java.nio.charset.StandardCharsets.UTF_8)));
|
||||
} catch (java.security.NoSuchAlgorithmException exception) {
|
||||
throw new IllegalStateException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static GroupSnapshot coloadGroup(String status, Long lockedPlanId, Member... members) {
|
||||
GroupSnapshot group = mock(GroupSnapshot.class);
|
||||
when(group.groupId()).thenReturn(700L);
|
||||
|
||||
@@ -462,7 +462,7 @@ class MeasurementBatchServiceTest {
|
||||
InboundOrderEntity inbound = new InboundOrderEntity();
|
||||
inbound.setId(10L);
|
||||
inbound.setTenantId(1L);
|
||||
inbound.setCustomerId(1L);
|
||||
inbound.setCustomerId(null);
|
||||
inbound.setProcessStatus("WAIT_MEASURE");
|
||||
inbound.setVersion(0);
|
||||
return inbound;
|
||||
|
||||
+72
-54
@@ -52,37 +52,38 @@ class MeasurementLocationOccupationServiceTest {
|
||||
properties.setWarehouseResourceTenantId(90001L);
|
||||
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound());
|
||||
when(measurements.listWeights(BUSINESS_TENANT, INBOUND)).thenReturn(confirmedWeights());
|
||||
when(warehouse.reserve(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(batch("RESERVED")));
|
||||
when(warehouse.confirm(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(batch("OCCUPIED")));
|
||||
when(warehouse.reserveBlock(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(block("RESERVED", 3)));
|
||||
when(warehouse.confirmBlock(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(block("OCCUPIED", 3)));
|
||||
|
||||
service.occupy(BUSINESS_TENANT, INBOUND, OPERATOR, MEASUREMENT, 3);
|
||||
|
||||
ArgumentCaptor<WarehousePalletLocationClient.ReserveRequest> reserve =
|
||||
ArgumentCaptor.forClass(WarehousePalletLocationClient.ReserveRequest.class);
|
||||
verify(warehouse).reserve(org.mockito.ArgumentMatchers.eq("internal-token"), reserve.capture());
|
||||
assertThat(reserve.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT);
|
||||
ArgumentCaptor<WarehousePalletLocationClient.BlockReserveRequest> reserve =
|
||||
ArgumentCaptor.forClass(WarehousePalletLocationClient.BlockReserveRequest.class);
|
||||
verify(warehouse).reserveBlock(org.mockito.ArgumentMatchers.eq("internal-token"), reserve.capture());
|
||||
assertThat(reserve.getValue().businessId()).isEqualTo(INBOUND);
|
||||
assertThat(reserve.getValue().idempotencyKey()).startsWith("measure-88-30-3-");
|
||||
assertThat(reserve.getValue().pallets())
|
||||
.extracting(WarehousePalletLocationClient.PalletReference::palletId)
|
||||
.containsExactly(601L, 601L, 601L);
|
||||
assertThat(reserve.getValue().idempotencyKey()).startsWith("measure-block-88-30-3-");
|
||||
assertThat(reserve.getValue().requiredSlotCount()).isEqualTo(3);
|
||||
|
||||
ArgumentCaptor<WarehousePalletLocationClient.ConfirmRequest> confirm =
|
||||
ArgumentCaptor.forClass(WarehousePalletLocationClient.ConfirmRequest.class);
|
||||
verify(warehouse).confirm(org.mockito.ArgumentMatchers.eq("internal-token"), confirm.capture());
|
||||
assertThat(confirm.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT);
|
||||
ArgumentCaptor<WarehousePalletLocationClient.BlockConfirmRequest> confirm =
|
||||
ArgumentCaptor.forClass(WarehousePalletLocationClient.BlockConfirmRequest.class);
|
||||
verify(warehouse).confirmBlock(org.mockito.ArgumentMatchers.eq("internal-token"), confirm.capture());
|
||||
assertThat(confirm.getValue().businessId()).isEqualTo(INBOUND);
|
||||
assertThat(confirm.getValue().packageCount()).isEqualTo(20);
|
||||
assertThat(confirm.getValue().blockId()).isEqualTo(500L);
|
||||
assertThat(confirm.getValue().idempotencyKey())
|
||||
.isEqualTo(reserve.getValue().idempotencyKey() + "-confirm");
|
||||
|
||||
verify(allocations, times(2)).replace(
|
||||
verify(allocations, times(2)).replaceBlock(
|
||||
org.mockito.ArgumentMatchers.eq(BUSINESS_TENANT),
|
||||
org.mockito.ArgumentMatchers.eq(INBOUND),
|
||||
org.mockito.ArgumentMatchers.eq(WAREHOUSE),
|
||||
org.mockito.ArgumentMatchers.eq(PLATFORM_RESOURCE_TENANT),
|
||||
org.mockito.ArgumentMatchers.eq(20),
|
||||
org.mockito.ArgumentMatchers.eq(new BigDecimal("1.0000")),
|
||||
org.mockito.ArgumentMatchers.any());
|
||||
verify(allocations, times(2)).bindConfirmedWeights(
|
||||
org.mockito.ArgumentMatchers.eq(BUSINESS_TENANT),
|
||||
org.mockito.ArgumentMatchers.eq(INBOUND), org.mockito.ArgumentMatchers.anyList());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -90,33 +91,51 @@ class MeasurementLocationOccupationServiceTest {
|
||||
properties.setWarehouseResourceTenantId(0L);
|
||||
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound());
|
||||
when(measurements.listWeights(BUSINESS_TENANT, INBOUND)).thenReturn(confirmedWeights());
|
||||
when(warehouse.reserve(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(batch("RESERVED")));
|
||||
when(warehouse.confirm(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(batch("OCCUPIED")));
|
||||
when(warehouse.reserveBlock(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(block("RESERVED", 3)));
|
||||
when(warehouse.confirmBlock(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(block("OCCUPIED", 3)));
|
||||
|
||||
service.occupy(BUSINESS_TENANT, INBOUND, OPERATOR, MEASUREMENT, 3);
|
||||
|
||||
ArgumentCaptor<WarehousePalletLocationClient.ReserveRequest> reserve =
|
||||
ArgumentCaptor.forClass(WarehousePalletLocationClient.ReserveRequest.class);
|
||||
verify(warehouse).reserve(org.mockito.ArgumentMatchers.eq("internal-token"), reserve.capture());
|
||||
assertThat(reserve.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT);
|
||||
ArgumentCaptor<WarehousePalletLocationClient.BlockReserveRequest> reserve =
|
||||
ArgumentCaptor.forClass(WarehousePalletLocationClient.BlockReserveRequest.class);
|
||||
verify(warehouse).reserveBlock(org.mockito.ArgumentMatchers.eq("internal-token"), reserve.capture());
|
||||
assertThat(reserve.getValue().warehouseId()).isEqualTo(WAREHOUSE);
|
||||
|
||||
ArgumentCaptor<WarehousePalletLocationClient.ConfirmRequest> confirm =
|
||||
ArgumentCaptor.forClass(WarehousePalletLocationClient.ConfirmRequest.class);
|
||||
verify(warehouse).confirm(org.mockito.ArgumentMatchers.eq("internal-token"), confirm.capture());
|
||||
assertThat(confirm.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT);
|
||||
assertThat(confirm.getValue().packageCount()).isEqualTo(20);
|
||||
ArgumentCaptor<WarehousePalletLocationClient.BlockConfirmRequest> confirm =
|
||||
ArgumentCaptor.forClass(WarehousePalletLocationClient.BlockConfirmRequest.class);
|
||||
verify(warehouse).confirmBlock(org.mockito.ArgumentMatchers.eq("internal-token"), confirm.capture());
|
||||
assertThat(confirm.getValue().businessId()).isEqualTo(INBOUND);
|
||||
|
||||
verify(allocations, times(2)).replace(
|
||||
verify(allocations, times(2)).replaceBlock(
|
||||
org.mockito.ArgumentMatchers.eq(BUSINESS_TENANT),
|
||||
org.mockito.ArgumentMatchers.eq(INBOUND),
|
||||
org.mockito.ArgumentMatchers.eq(WAREHOUSE),
|
||||
org.mockito.ArgumentMatchers.eq(PLATFORM_RESOURCE_TENANT),
|
||||
org.mockito.ArgumentMatchers.eq(20),
|
||||
org.mockito.ArgumentMatchers.eq(new BigDecimal("1.0000")),
|
||||
org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void reservesThreeSlotsForVolumeWhileBindingOnlyTwoConfirmedWeightImages() {
|
||||
InboundOrderEntity inbound = inbound();
|
||||
inbound.setActualVolumeCbm(new BigDecimal("2.5000"));
|
||||
List<InboundWeightResultEntity> twoWeights = confirmedWeights().subList(0, 2);
|
||||
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound);
|
||||
when(measurements.listWeights(BUSINESS_TENANT, INBOUND)).thenReturn(twoWeights);
|
||||
when(warehouse.reserveBlock(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(block("RESERVED", 3)));
|
||||
when(warehouse.confirmBlock(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(block("OCCUPIED", 3)));
|
||||
|
||||
service.occupy(BUSINESS_TENANT, INBOUND, OPERATOR, MEASUREMENT, 3);
|
||||
|
||||
verify(warehouse).reserveBlock(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.argThat(request -> request.requiredSlotCount() == 3));
|
||||
verify(allocations, times(2)).bindConfirmedWeights(BUSINESS_TENANT, INBOUND, twoWeights);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsConfirmedWeightWithoutConfirmationTimeInsteadOfDroppingIt() {
|
||||
InboundWeightResultEntity weight = confirmedWeights().get(0);
|
||||
@@ -128,7 +147,7 @@ class MeasurementLocationOccupationServiceTest {
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("缺少确认时间");
|
||||
|
||||
verify(warehouse, never()).reserve(org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.any());
|
||||
verify(warehouse, never()).reserveBlock(org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -142,7 +161,7 @@ class MeasurementLocationOccupationServiceTest {
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("缺少有效净重");
|
||||
|
||||
verify(warehouse, never()).reserve(org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.any());
|
||||
verify(warehouse, never()).reserveBlock(org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -156,7 +175,7 @@ class MeasurementLocationOccupationServiceTest {
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("缺少有效托盘");
|
||||
|
||||
verify(warehouse, never()).reserve(org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.any());
|
||||
verify(warehouse, never()).reserveBlock(org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -164,24 +183,23 @@ class MeasurementLocationOccupationServiceTest {
|
||||
properties.setWarehouseResourceTenantId(90001L);
|
||||
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound());
|
||||
when(measurements.listWeights(BUSINESS_TENANT, INBOUND)).thenReturn(confirmedWeights());
|
||||
when(warehouse.reserve(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(batch("RESERVED")));
|
||||
when(warehouse.reserveBlock(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(block("RESERVED", 3)));
|
||||
IllegalStateException failure = new IllegalStateException("confirm failed");
|
||||
when(warehouse.confirm(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
when(warehouse.confirmBlock(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenThrow(failure);
|
||||
when(warehouse.release(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(List.of()));
|
||||
when(warehouse.releaseBlock(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(block("RELEASED", 3)));
|
||||
|
||||
assertThatThrownBy(() -> service.occupy(BUSINESS_TENANT, INBOUND, OPERATOR, MEASUREMENT, 3))
|
||||
.isSameAs(failure);
|
||||
|
||||
ArgumentCaptor<WarehousePalletLocationClient.ReleaseRequest> release =
|
||||
ArgumentCaptor.forClass(WarehousePalletLocationClient.ReleaseRequest.class);
|
||||
verify(warehouse).release(org.mockito.ArgumentMatchers.eq("internal-token"), release.capture());
|
||||
assertThat(release.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT);
|
||||
ArgumentCaptor<WarehousePalletLocationClient.BlockReleaseRequest> release =
|
||||
ArgumentCaptor.forClass(WarehousePalletLocationClient.BlockReleaseRequest.class);
|
||||
verify(warehouse).releaseBlock(org.mockito.ArgumentMatchers.eq("internal-token"), release.capture());
|
||||
assertThat(release.getValue().businessId()).isEqualTo(INBOUND);
|
||||
verify(allocations).replace(BUSINESS_TENANT, INBOUND, WAREHOUSE, PLATFORM_RESOURCE_TENANT,
|
||||
20, batch("RESERVED"));
|
||||
verify(allocations).replaceBlock(BUSINESS_TENANT, INBOUND, WAREHOUSE,
|
||||
20, new BigDecimal("1.0000"), block("RESERVED", 3));
|
||||
}
|
||||
|
||||
private static InboundProperties properties() {
|
||||
@@ -209,12 +227,12 @@ class MeasurementLocationOccupationServiceTest {
|
||||
return value;
|
||||
}
|
||||
|
||||
private static WarehousePalletLocationClient.BatchResult batch(String state) {
|
||||
return new WarehousePalletLocationClient.BatchResult(3, java.util.stream.IntStream.rangeClosed(1, 3)
|
||||
.mapToObj(index -> new WarehousePalletLocationClient.AllocationResult(
|
||||
500L + index, "RES-00" + index, index, 601L, "PL-001", new BigDecimal("1.200000"),
|
||||
700L + index, "A-00" + index, new BigDecimal("0.333333"), state))
|
||||
.toList());
|
||||
private static WarehousePalletLocationClient.BlockResult block(String state, int count) {
|
||||
return new WarehousePalletLocationClient.BlockResult(500L, state,
|
||||
java.util.stream.IntStream.rangeClosed(1, count)
|
||||
.mapToObj(index -> new WarehousePalletLocationClient.BlockResource(
|
||||
500L, 600L + index, 700L + index, "A-00" + index, index, false, state))
|
||||
.toList());
|
||||
}
|
||||
|
||||
private static List<InboundWeightResultEntity> confirmedWeights() {
|
||||
|
||||
+31
-58
@@ -56,9 +56,6 @@ class PdaPickSettlementServiceTest {
|
||||
.thenReturn(List.of(new MeasurementItemRow(51L, 61L, 41L, 20)));
|
||||
when(allocations.lockConfirmed(10001L, 31L)).thenAnswer(invocation ->
|
||||
List.of(allocation(1L, 1, 501L, 10), allocation(2L, 2, 502L, 10)));
|
||||
when(warehouse.settle(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||
new WarehousePalletLocationClient.SettlementResult(List.of(), List.of(
|
||||
transfer(1, 501L, 2), transfer(2, 502L, 10)))));
|
||||
writes(12, 8);
|
||||
|
||||
service.settle(1L, 9L, "operator");
|
||||
@@ -71,14 +68,13 @@ class PdaPickSettlementServiceTest {
|
||||
verify(splits).normalizeLoadedCartonLabels(10001L, 31L, 8);
|
||||
verify(splits).markSourceLoaded(10001L, 31L, 8, 9L, 3);
|
||||
verify(allocations).consume(10001L, 31L, 1L, 8);
|
||||
verify(warehouse).settle(eq("internal-token"), org.mockito.ArgumentMatchers.argThat(request ->
|
||||
request.warehouseId() == 701L && request.sourceBusinessId() == 31L
|
||||
&& request.idempotencyKey().equals("PDA-SETTLE-1-31")
|
||||
&& request.releasedReservationIds().isEmpty()
|
||||
&& request.remainingItems().equals(List.of(new WarehousePalletLocationClient.SettlementTransferItem(91L, 2),
|
||||
new WarehousePalletLocationClient.SettlementTransferItem(92L, 10)))));
|
||||
verify(allocations).copyRemainingToChild(eq(10001L), anyLong(), any());
|
||||
verify(allocations).markTransferred(eq(10001L), eq(31L), any(), eq(List.of()));
|
||||
verify(warehouse).transferBlock(eq("internal-token"), org.mockito.ArgumentMatchers.argThat(request ->
|
||||
request.blockId() == 800L && request.sourceBusinessId() == 31L
|
||||
&& request.targetBusinessId() > 0
|
||||
&& request.idempotencyKey().equals("PDA-BLOCK-1-31")));
|
||||
verify(warehouse, never()).releaseBlock(anyString(), any());
|
||||
verify(allocations).copyBlockToChild(eq(10001L), anyLong(), any());
|
||||
verify(allocations).markBlockTransferred(10001L, 31L, 800L);
|
||||
verify(splits).convergePartialPlanItem(21L, 9L, "operator");
|
||||
}
|
||||
|
||||
@@ -92,8 +88,6 @@ class PdaPickSettlementServiceTest {
|
||||
new MeasurementItemRow(52L, 61L, 41L, 1)));
|
||||
when(allocations.lockConfirmed(10001L, 31L)).thenAnswer(invocation ->
|
||||
List.of(allocation(1L, 1, 501L, 2)));
|
||||
when(warehouse.settle(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||
new WarehousePalletLocationClient.SettlementResult(List.of(), List.of(transfer(1, 501L, 1)))));
|
||||
writes(1, 1);
|
||||
when(splits.insertSplitItem(anyLong(), eq(10001L), anyLong(), eq(31L), anyLong(), anyInt(),
|
||||
eq(41L), eq(61L), eq(52L), eq(1))).thenReturn(1);
|
||||
@@ -114,7 +108,7 @@ class PdaPickSettlementServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void reusesWarehouseSettlementTargetWhenRetryingAfterLocalRollback() {
|
||||
void transfersTheWholeBlockToTheGeneratedRemainderChild() {
|
||||
LoadingPickItemEntity item = item(11L, 21L, 31L, 41L, 3, 1, "PARTIAL");
|
||||
resolved(List.of(item));
|
||||
when(splits.lockParent(31L)).thenReturn(parent(3));
|
||||
@@ -122,19 +116,16 @@ class PdaPickSettlementServiceTest {
|
||||
.thenReturn(List.of(new MeasurementItemRow(51L, 61L, 41L, 3)));
|
||||
when(allocations.lockConfirmed(10001L, 31L)).thenAnswer(invocation ->
|
||||
List.of(allocation(1L, 1, 501L, 3)));
|
||||
when(warehouse.settlementTarget("internal-token", 31L, "PDA-SETTLE-1-31"))
|
||||
.thenReturn(ApiResponse.success(999L));
|
||||
when(warehouse.settle(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||
new WarehousePalletLocationClient.SettlementResult(List.of(), List.of(transfer(1, 501L, 2)))));
|
||||
writes(2, 1);
|
||||
|
||||
service.settle(1L, 9L, "operator");
|
||||
|
||||
verify(splits).cloneChildWithPurpose(eq(10001L), eq(31L), eq(31L), eq(999L),
|
||||
verify(splits).cloneChildWithPurpose(eq(10001L), eq(31L), eq(31L), anyLong(),
|
||||
eq(1002L), eq("ORDER-REMAINDER"), eq("IN-001-1"), eq(2),
|
||||
eq(1), eq("REMAINDER"), eq(9L), eq("operator"));
|
||||
verify(warehouse).settle(eq("internal-token"), org.mockito.ArgumentMatchers.argThat(request ->
|
||||
request.targetBusinessId() == 999L && request.targetPackageCount() == 2));
|
||||
verify(warehouse).transferBlock(eq("internal-token"), org.mockito.ArgumentMatchers.argThat(request ->
|
||||
request.blockId() == 800L && request.sourceBusinessId() == 31L
|
||||
&& request.targetBusinessId() > 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -146,12 +137,13 @@ class PdaPickSettlementServiceTest {
|
||||
|
||||
service.settle(1L, 9L, "operator");
|
||||
|
||||
verify(warehouse, never()).settle(anyString(), any());
|
||||
verify(warehouse, never()).transferBlock(anyString(), any());
|
||||
verify(warehouse, never()).releaseBlock(anyString(), any());
|
||||
verify(splits, never()).convergePartialPlanItem(anyLong(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void releasesFullyPickedAllocationBeforeTransferringChildResources() {
|
||||
void partialPickTransfersTheWholeBlockWithoutReleasingPickedEmptySlots() {
|
||||
LoadingPickItemEntity item = item(11L, 21L, 31L, 41L, 10, 10, "PICKED");
|
||||
resolved(List.of(item));
|
||||
when(splits.lockParent(31L)).thenReturn(parent(20));
|
||||
@@ -159,17 +151,13 @@ class PdaPickSettlementServiceTest {
|
||||
.thenReturn(List.of(new MeasurementItemRow(51L, 61L, 41L, 20)));
|
||||
when(allocations.lockConfirmed(10001L, 31L)).thenAnswer(invocation ->
|
||||
List.of(allocation(1L, 1, 501L, 10), allocation(2L, 2, 502L, 10)));
|
||||
when(warehouse.settle(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||
new WarehousePalletLocationClient.SettlementResult(List.of(released(91L, 501L)),
|
||||
List.of(transfer(2, 502L, 10)))));
|
||||
writes(10, 10);
|
||||
|
||||
service.settle(1L, 9L, "operator");
|
||||
|
||||
verify(warehouse).settle(eq("internal-token"), org.mockito.ArgumentMatchers.argThat(request ->
|
||||
request.idempotencyKey().equals("PDA-SETTLE-1-31")
|
||||
&& request.releasedReservationIds().equals(List.of(91L))
|
||||
&& request.remainingItems().equals(List.of(new WarehousePalletLocationClient.SettlementTransferItem(92L, 10)))));
|
||||
verify(warehouse).transferBlock(eq("internal-token"), org.mockito.ArgumentMatchers.argThat(request ->
|
||||
request.blockId() == 800L && request.sourceBusinessId() == 31L));
|
||||
verify(warehouse, never()).releaseBlock(anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -184,9 +172,6 @@ class PdaPickSettlementServiceTest {
|
||||
first.setPalletId(801L);
|
||||
second.setPalletId(801L);
|
||||
when(allocations.lockConfirmed(10001L, 31L)).thenReturn(List.of(first, second));
|
||||
when(warehouse.settle(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||
new WarehousePalletLocationClient.SettlementResult(List.of(released(91L, 501L), released(92L, 501L)),
|
||||
List.of())));
|
||||
writes(0, 20);
|
||||
|
||||
service.settle(1L, 9L, "operator");
|
||||
@@ -196,10 +181,10 @@ class PdaPickSettlementServiceTest {
|
||||
verify(splits, never()).cloneChildWithPurpose(anyLong(), anyLong(), anyLong(), anyLong(),
|
||||
anyLong(), anyString(), anyString(), anyInt(), anyInt(), anyString(), anyLong(), anyString());
|
||||
verify(splits).markSourceLoaded(10001L, 31L, 20, 9L, 3);
|
||||
verify(warehouse).settle(eq("internal-token"), org.mockito.ArgumentMatchers.argThat(request ->
|
||||
request.targetBusinessId() == 0L && request.targetPackageCount() == 0
|
||||
&& request.releasedReservationIds().equals(List.of(91L, 92L))
|
||||
&& request.remainingItems().isEmpty()));
|
||||
verify(warehouse).releaseBlock(eq("internal-token"), org.mockito.ArgumentMatchers.argThat(request ->
|
||||
request.blockId() == 800L && request.businessId() == 31L
|
||||
&& request.reason().equals("ALL_PACKAGES_PICKED")));
|
||||
verify(warehouse, never()).transferBlock(anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -251,9 +236,6 @@ class PdaPickSettlementServiceTest {
|
||||
.thenReturn(List.of(new MeasurementItemRow(51L, 61L, 41L, 20)));
|
||||
when(allocations.lockConfirmed(10001L, 31L))
|
||||
.thenReturn(List.of(allocation(1L, 1, 501L, 10), allocation(2L, 2, 502L, 10)));
|
||||
when(warehouse.settle(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||
new WarehousePalletLocationClient.SettlementResult(
|
||||
List.of(released(91L, 501L), released(92L, 502L)), List.of())));
|
||||
writes(0, 20);
|
||||
|
||||
service.settle(1L, 9L, "operator");
|
||||
@@ -272,9 +254,6 @@ class PdaPickSettlementServiceTest {
|
||||
.thenReturn(List.of(new MeasurementItemRow(51L, 61L, 41L, 20)));
|
||||
when(allocations.lockConfirmed(10001L, 31L))
|
||||
.thenReturn(List.of(allocation(1L, 1, 501L, 10), allocation(2L, 2, 502L, 10)));
|
||||
when(warehouse.settle(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||
new WarehousePalletLocationClient.SettlementResult(
|
||||
List.of(released(91L, 501L), released(92L, 502L)), List.of())));
|
||||
writes(0, 20);
|
||||
|
||||
service.settle(1L, 9L, "operator");
|
||||
@@ -323,9 +302,6 @@ class PdaPickSettlementServiceTest {
|
||||
|
||||
stubOrderSplitSuccess();
|
||||
writes(0, 20);
|
||||
when(warehouse.settle(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||
new WarehousePalletLocationClient.SettlementResult(
|
||||
List.of(released(91L, 501L), released(92L, 502L)), List.of())));
|
||||
|
||||
service.settle(1L, 9L, "operator");
|
||||
|
||||
@@ -341,8 +317,6 @@ class PdaPickSettlementServiceTest {
|
||||
resolvedChild(item, 32L, 42L, 62L, 52L, 12, 2);
|
||||
when(splits.nextSplitSequence(10001L, 31L)).thenReturn(2);
|
||||
childWrites(32L, 42L, 62L, 52L, 5, 2);
|
||||
when(warehouse.settle(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||
new WarehousePalletLocationClient.SettlementResult(List.of(), List.of(transfer(1, 501L, 7)))));
|
||||
|
||||
service.settle(1L, 9L, "operator");
|
||||
|
||||
@@ -365,8 +339,6 @@ class PdaPickSettlementServiceTest {
|
||||
when(splits.nextSplitSequence(10001L, 31L)).thenReturn(3);
|
||||
childWrites(33L, 43L, 63L, 53L, 4, 3);
|
||||
when(splits.markSourceLoaded(10001L, 33L, 4, 9L, 3)).thenReturn(0, 1);
|
||||
when(warehouse.settle(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||
new WarehousePalletLocationClient.SettlementResult(List.of(), List.of(transfer(1, 501L, 5)))));
|
||||
|
||||
assertThatThrownBy(() -> service.settle(1L, 9L, "operator"))
|
||||
.hasMessageContaining("来源分片状态已被并发修改");
|
||||
@@ -389,6 +361,10 @@ class PdaPickSettlementServiceTest {
|
||||
task.setId(1L); task.setLoadingPlanId(701L);
|
||||
when(picks.task(1L)).thenReturn(task);
|
||||
when(splits.containerBookingId(701L)).thenReturn(801L);
|
||||
when(warehouse.transferBlock(eq("internal-token"), any()))
|
||||
.thenReturn(ApiResponse.success(block("OCCUPIED")));
|
||||
when(warehouse.releaseBlock(eq("internal-token"), any()))
|
||||
.thenReturn(ApiResponse.success(block("RELEASED")));
|
||||
stubOrderSplitSuccess();
|
||||
}
|
||||
|
||||
@@ -485,19 +461,16 @@ class PdaPickSettlementServiceTest {
|
||||
private static InboundPalletAllocationEntity allocation(long id, int no, long locationId, int allocated) {
|
||||
InboundPalletAllocationEntity value = new InboundPalletAllocationEntity();
|
||||
value.setId(id); value.setAllocationNo(no); value.setLocationId(locationId); value.setWarehouseId(701L);
|
||||
value.setBlockId(800L); value.setSlotNo(no); value.setAllocationStatus("CONFIRMED");
|
||||
value.setReservationId(90L + no);
|
||||
value.setAllocatedPackageCount(allocated); value.setPickedPackageCount(0);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static WarehousePalletLocationClient.TransferResult transfer(int no, long locationId, int remaining) {
|
||||
return new WarehousePalletLocationClient.TransferResult(90L + no, "RES-" + no, 92L + no, no,
|
||||
0L, 701L, 801L + no, "PALLET-" + no, null, locationId, "L-" + no, null, remaining, "OCCUPIED");
|
||||
}
|
||||
|
||||
private static WarehousePalletLocationClient.SelectiveReleaseResult released(long reservationId, long locationId) {
|
||||
return new WarehousePalletLocationClient.SelectiveReleaseResult(reservationId, 92L, 1, 801L,
|
||||
locationId, "L-1", 10);
|
||||
private static WarehousePalletLocationClient.BlockResult block(String status) {
|
||||
return new WarehousePalletLocationClient.BlockResult(800L, status, List.of(
|
||||
new WarehousePalletLocationClient.BlockResource(
|
||||
800L, 91L, 501L, "L-1", 1, false, status)));
|
||||
}
|
||||
|
||||
private static LoadingPickItemEntity item(long id, long planItemId, long inboundId, long detailId,
|
||||
|
||||
@@ -81,7 +81,6 @@ import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
|
||||
@SpringJUnitConfig(SmartLoadingServiceTest.Config.class)
|
||||
class SmartLoadingServiceTest {
|
||||
@@ -108,6 +107,7 @@ class SmartLoadingServiceTest {
|
||||
when(documentNumbers.next("LP")).thenReturn("LP-260818-0001");
|
||||
when(loadingPickService.mutationSnapshot(anyLong())).thenReturn(
|
||||
new LoadingPickService.MutationSnapshot(null, null, null, Map.of()));
|
||||
when(repository.occupiedOrderIds(any(), anySet())).thenReturn(List.of());
|
||||
when(cargoClient.detail(anyString(), anyLong(), anyLong(), anyLong())).thenReturn(
|
||||
ApiResponse.success(new OrderCargoClient.CargoBinding(20L, 30L, 1, "货物", 5,
|
||||
new BigDecimal("10"), new BigDecimal("2"))));
|
||||
@@ -224,9 +224,9 @@ class SmartLoadingServiceTest {
|
||||
100L, "EXX_TRUCK", "EXX海卡", 3, LocalDateTime.parse("2026-09-01T09:00:00"),
|
||||
"PENDING_REVIEW");
|
||||
when(pendingReviewOrders.page(anyString(), anyList(), isNull(), isNull(), isNull(),
|
||||
isNull(), isNull(), anyLong(), anyLong())).thenAnswer(invocation -> {
|
||||
long page = invocation.getArgument(7);
|
||||
long size = invocation.getArgument(8);
|
||||
isNull(), isNull(), anyList(), anyLong(), anyLong())).thenAnswer(invocation -> {
|
||||
long page = invocation.getArgument(8);
|
||||
long size = invocation.getArgument(9);
|
||||
return ApiResponse.success(new PageResult<>(page == 1 ? List.of(pending) : List.of(),
|
||||
page, size, 1, 1));
|
||||
});
|
||||
@@ -248,11 +248,15 @@ class SmartLoadingServiceTest {
|
||||
.containsExactly(true, true);
|
||||
ArgumentCaptor<InboundCandidateQuery> firstlegQueries =
|
||||
ArgumentCaptor.forClass(InboundCandidateQuery.class);
|
||||
verify(repository, atLeastOnce()).globalCandidateUnits(firstlegQueries.capture(), anySet());
|
||||
verify(repository).globalCandidateUnits(firstlegQueries.capture(), anySet());
|
||||
assertThat(firstlegQueries.getValue().page()).isEqualTo(1);
|
||||
assertThat(firstlegQueries.getValue().size()).isEqualTo(19);
|
||||
assertThat(firstlegQueries.getAllValues()).allSatisfy(requested ->
|
||||
assertThat(requested.orderStatuses()).containsExactly(
|
||||
CandidateOrderStatus.WAIT_MEASURE, CandidateOrderStatus.WAIT_SPLIT,
|
||||
CandidateOrderStatus.LOADING));
|
||||
verify(pendingReviewOrders).page(anyString(), anyList(), isNull(), isNull(), isNull(),
|
||||
isNull(), isNull(), anyList(), eq(1L), eq(20L));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -311,9 +315,10 @@ class SmartLoadingServiceTest {
|
||||
"EXXHK2609020361", 2092797167699816450L, "客户", 193000000000200002L,
|
||||
"EXX_TRUCK", "EXX海卡", 100, LocalDateTime.parse("2026-09-02T15:31:36"),
|
||||
"PENDING_REVIEW");
|
||||
when(repository.occupiedOrderIds(eq(query), anySet())).thenReturn(List.of(998L));
|
||||
when(pendingReviewOrders.page(anyString(), anyList(), eq(193000000000200002L),
|
||||
isNull(), eq("EXXHK2609020361"), eq("2026-09-02"),
|
||||
eq("2026-09-02"), eq(1L), eq(20L)))
|
||||
eq("2026-09-02"), eq(List.of(998L)), eq(1L), eq(20L)))
|
||||
.thenReturn(ApiResponse.success(new PageResult<>(List.of(row), 1, 20, 1, 1)));
|
||||
|
||||
var result = service.inboundCandidates(query);
|
||||
@@ -329,26 +334,27 @@ class SmartLoadingServiceTest {
|
||||
});
|
||||
verify(repository, never()).globalCandidateUnits(any(), anySet());
|
||||
verify(repository, never()).globalCandidatesByUnits(anyList());
|
||||
verify(repository).occupiedOrderIds(eq(query), anySet());
|
||||
}
|
||||
|
||||
@Test
|
||||
void assignsPendingReviewOrderAsAProvisionalContainerAllocation() {
|
||||
void manuallyAssignsCrossChannelPendingReviewOrderAsAProvisionalContainerAllocation() {
|
||||
long customerId = 2092797167699816450L;
|
||||
long orderId = 2095052002489335810L;
|
||||
String inboundCode = "EXXHK2609020361";
|
||||
var pending = new PendingReviewOrderClient.Row(orderId, "FL-361", inboundCode,
|
||||
customerId, "客户", 100L, "FBA", "FBA渠道", 5,
|
||||
customerId, "客户", 100L, "SLOW", "慢船", 5,
|
||||
LocalDateTime.parse("2026-09-02T15:31:36"), "PENDING_REVIEW");
|
||||
var snapshot = new com.baiyue.firstleg.dto.OrderApprovedInboundCommand(
|
||||
customerId, orderId, "FL-361", inboundCode, customerId, "客户", 100L, "义乌仓",
|
||||
null, null, null, "FIRST_LEG", "FBA", "FBA渠道", null, null, null,
|
||||
null, null, null, "FIRST_LEG", "SLOW", "慢船", null, null, null,
|
||||
null, null, false, "FBA", 200L, "FBA1", "FBA仓", null, null, null, 5);
|
||||
var label = new OrderCargoClient.CargoLabelBinding(20L, orderId, 1, "货物", 5,
|
||||
null, null, null);
|
||||
var cargo = new OrderCargoClient.CargoBinding(20L, orderId, 1, "货物", 5,
|
||||
new BigDecimal("50"), new BigDecimal("2.5"));
|
||||
when(pendingReviewOrders.page(anyString(), isNull(), isNull(), eq(customerId),
|
||||
eq(inboundCode), isNull(), isNull(), eq(1L), eq(2L)))
|
||||
eq(inboundCode), isNull(), isNull(), isNull(), eq(1L), eq(2L)))
|
||||
.thenReturn(ApiResponse.success(new PageResult<>(List.of(pending), 1, 2, 1, 1)));
|
||||
when(cargoClient.inboundSnapshot(anyString(), eq(customerId), eq(orderId)))
|
||||
.thenReturn(ApiResponse.success(snapshot));
|
||||
@@ -381,7 +387,7 @@ class SmartLoadingServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void assignsWaitMeasureInboundUsingDeclaredFactsWithoutTreatingStatusAsAGate() {
|
||||
void manuallyAssignsCrossChannelWaitMeasureInboundUsingDeclaredFacts() {
|
||||
long customerId = 9L;
|
||||
long inboundId = 10L;
|
||||
long orderId = 30L;
|
||||
@@ -391,11 +397,11 @@ class SmartLoadingServiceTest {
|
||||
projection.setOrderId(orderId);
|
||||
projection.setInboundCode("I10");
|
||||
projection.setWarehouseId(100L);
|
||||
projection.setChannelCode("FBA");
|
||||
projection.setChannelCode("SLOW");
|
||||
projection.setProcessStatus("WAIT_MEASURE");
|
||||
var snapshot = new com.baiyue.firstleg.dto.OrderApprovedInboundCommand(
|
||||
customerId, orderId, "O30", "I10", customerId, "客户", 100L, "义乌仓",
|
||||
null, null, null, "FIRST_LEG", "FBA", "FBA渠道", null, null, null,
|
||||
null, null, null, "FIRST_LEG", "SLOW", "慢船", null, null, null,
|
||||
null, null, false, "FBA", 200L, "FBA1", "FBA仓", null, null, null, 5);
|
||||
var label = new OrderCargoClient.CargoLabelBinding(20L, orderId, 1, "货物", 5,
|
||||
null, null, null);
|
||||
@@ -430,7 +436,7 @@ class SmartLoadingServiceTest {
|
||||
List.of("EXX"), null, null, null, null, null,
|
||||
List.of(CandidateOrderStatus.PENDING_REVIEW), 1, 20);
|
||||
when(pendingReviewOrders.page(anyString(), anyList(), isNull(), isNull(), isNull(),
|
||||
isNull(), isNull(), eq(1L), eq(20L))).thenThrow(new RuntimeException("offline"));
|
||||
isNull(), isNull(), anyList(), eq(1L), eq(20L))).thenThrow(new RuntimeException("offline"));
|
||||
|
||||
assertThatThrownBy(() -> service.inboundCandidates(query))
|
||||
.isInstanceOf(BizException.class)
|
||||
@@ -575,8 +581,8 @@ class SmartLoadingServiceTest {
|
||||
|
||||
@Test
|
||||
void atomicallyAssignsEveryInboundFragmentOfSameOrderAcrossDifferentDestinations() {
|
||||
Candidate first = candidate(9L, 10L, 20L, 30L, 4);
|
||||
Candidate second = candidate(9L, 11L, 21L, 30L, 6, "FBA2");
|
||||
Candidate first = withChannel(candidate(9L, 10L, 20L, 30L, 4), "SLOW", "慢船");
|
||||
Candidate second = withChannel(candidate(9L, 11L, 21L, 30L, 6, "FBA2"), "SLOW", "慢船");
|
||||
GroupSnapshot active = group("ACTIVE", null,
|
||||
new Member(9L, 30L, 10L, "IN-10", 4), new Member(9L, 30L, 11L, "IN-11", 6));
|
||||
GroupSnapshot locked = group("LOCKED", 1L,
|
||||
@@ -796,6 +802,94 @@ class SmartLoadingServiceTest {
|
||||
org.mockito.ArgumentMatchers.startsWith("UNLOCK-"), eq(7L), eq("u"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void removesAllUnpickedItemsWithOnePlanVersionChange() {
|
||||
LoadingPlanEntity initial = plan("EDITING", 2);
|
||||
LoadingPlanEntity after = plan("EDITING", 3);
|
||||
LoadingPlanItemEntity first = item(8L, 9L, 20L, 4);
|
||||
LoadingPlanItemEntity second = item(9L, 19L, 21L, 6);
|
||||
second.setInboundId(11L);
|
||||
when(repository.plan(1L)).thenReturn(initial, after);
|
||||
when(repository.booking(99L)).thenReturn(booking());
|
||||
when(repository.items(1L)).thenReturn(List.of(first, second), List.of());
|
||||
when(repository.touch(1L, 2, 7L, "u")).thenReturn(1);
|
||||
when(repository.remove(eq(1L), anyLong(), eq(0), eq(7L), eq("u"))).thenReturn(1);
|
||||
|
||||
Plan result = service.removeUnpicked(1L,
|
||||
new com.baiyue.firstleg.dto.SmartLoadingDtos.RemoveUnpickedRequest(2, "remove-unpicked"),
|
||||
7L, "u");
|
||||
|
||||
assertThat(result.items()).isEmpty();
|
||||
verify(repository).touch(1L, 2, 7L, "u");
|
||||
verify(repository, times(2)).remove(eq(1L), anyLong(), eq(0), eq(7L), eq("u"));
|
||||
verify(loadingPickService, times(2)).remove(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void bulkRemovalKeepsWholeGroupWhenAnyMemberWasPicked() {
|
||||
LoadingPlanEntity initial = plan("LOADING", 2);
|
||||
LoadingPlanEntity after = plan("LOADING", 3);
|
||||
LoadingPlanItemEntity first = item(8L, 9L, 20L, 4);
|
||||
LoadingPlanItemEntity second = item(9L, 19L, 21L, 6);
|
||||
second.setInboundId(11L);
|
||||
LoadingPlanItemEntity standalone = item(10L, 29L, 22L, 3);
|
||||
standalone.setInboundId(12L);
|
||||
GroupSnapshot group = group("LOCKED", 1L,
|
||||
new Member(9L, 30L, 10L, "IN-10", 4), new Member(19L, 31L, 11L, "IN-11", 6));
|
||||
when(repository.plan(1L)).thenReturn(initial, after);
|
||||
when(repository.booking(99L)).thenReturn(booking());
|
||||
when(repository.items(1L)).thenReturn(List.of(first, second, standalone), List.of(first, second));
|
||||
when(repository.touch(1L, 2, 7L, "u")).thenReturn(1);
|
||||
when(repository.remove(1L, 10L, 0, 7L, "u")).thenReturn(1);
|
||||
when(coloadGroups.findByInbound(9L, 10L)).thenReturn(group);
|
||||
when(coloadGroups.findByInbound(19L, 11L)).thenReturn(group);
|
||||
when(loadingPickService.mutationSnapshot(1L)).thenReturn(
|
||||
new LoadingPickService.MutationSnapshot(77L, "LOADING", 5, Map.of(8L, 1)));
|
||||
|
||||
Plan result = service.removeUnpicked(1L,
|
||||
new com.baiyue.firstleg.dto.SmartLoadingDtos.RemoveUnpickedRequest(2, "keep-picked-group"),
|
||||
7L, "u");
|
||||
|
||||
assertThat(result.items()).extracting(LoadingPlanItemEntity::getId).containsExactly(8L, 9L);
|
||||
verify(repository).remove(1L, 10L, 0, 7L, "u");
|
||||
verify(repository, never()).remove(eq(1L), eq(8L), anyInt(), anyLong(), anyString());
|
||||
verify(repository, never()).remove(eq(1L), eq(9L), anyInt(), anyLong(), anyString());
|
||||
verify(coloadGroups, never()).unlockWholeGroupAfterRemoval(anyLong(), anyLong(), anyString(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void bulkRemovalRemovesAndUnlocksACompletelyUnpickedGroupOnce() {
|
||||
LoadingPlanEntity initial = plan("EDITING", 2);
|
||||
LoadingPlanEntity after = plan("EDITING", 3);
|
||||
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, "IN-10", 4), new Member(19L, 31L, 11L, "IN-11", 6));
|
||||
when(repository.plan(1L)).thenReturn(initial, 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, 2, 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);
|
||||
GroupSnapshot unlocked = group("ACTIVE", null,
|
||||
new Member(9L, 30L, 10L, "IN-10", 4), new Member(19L, 31L, 11L, "IN-11", 6));
|
||||
when(coloadGroups.unlockWholeGroupAfterRemoval(eq(700L), eq(1L), anyString(), eq(7L), eq("u")))
|
||||
.thenReturn(unlocked);
|
||||
|
||||
Plan result = service.removeUnpicked(1L,
|
||||
new com.baiyue.firstleg.dto.SmartLoadingDtos.RemoveUnpickedRequest(2, "remove-unpicked-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,
|
||||
@@ -1319,6 +1413,44 @@ class SmartLoadingServiceTest {
|
||||
assertThat(updated.getValue().getWarehouseRemark()).isEqualTo("装柜时注意外箱");
|
||||
}
|
||||
|
||||
@Test
|
||||
void quantityAdjustmentCanDecreaseWhenCalculatedRemainingQuantityIsNegative() {
|
||||
LoadingPlanEntity plan = plan("LOADING", 93);
|
||||
LoadingPlanItemEntity line = item(8, 9, 20, 50);
|
||||
when(repository.plan(1L)).thenReturn(plan, plan("LOADING", 94));
|
||||
when(repository.item(1L, 8L)).thenReturn(line);
|
||||
when(repository.booking(99L)).thenReturn(booking());
|
||||
when(repository.candidate(1L, 100L, 10L, 20L)).thenReturn(candidate(-25));
|
||||
when(repository.items(1L)).thenReturn(List.of(line));
|
||||
when(repository.touch(1L, 93, 7L, "u")).thenReturn(1);
|
||||
when(repository.update(line)).thenReturn(1);
|
||||
|
||||
service.adjust(1L, 8L, new AdjustRequest(30, 93, 0, "adjust-down"), 7L, "u");
|
||||
|
||||
ArgumentCaptor<LoadingPlanItemEntity> updated = ArgumentCaptor.forClass(LoadingPlanItemEntity.class);
|
||||
verify(repository).update(updated.capture());
|
||||
assertThat(updated.getValue().getPackageCount()).isEqualTo(30);
|
||||
}
|
||||
|
||||
@Test
|
||||
void quantityAdjustmentOnlyAppliesRemainingQuantityLimitToTheIncrease() {
|
||||
LoadingPlanEntity plan = plan("LOADING", 2);
|
||||
LoadingPlanItemEntity line = item(8, 9, 20, 50);
|
||||
when(repository.plan(1L)).thenReturn(plan, plan("LOADING", 3));
|
||||
when(repository.item(1L, 8L)).thenReturn(line);
|
||||
when(repository.booking(99L)).thenReturn(booking());
|
||||
when(repository.candidate(1L, 100L, 10L, 20L)).thenReturn(candidate(10));
|
||||
when(repository.items(1L)).thenReturn(List.of(line));
|
||||
when(repository.touch(1L, 2, 7L, "u")).thenReturn(1);
|
||||
when(repository.update(line)).thenReturn(1);
|
||||
|
||||
service.adjust(1L, 8L, new AdjustRequest(60, 2, 0, "adjust-up"), 7L, "u");
|
||||
|
||||
ArgumentCaptor<LoadingPlanItemEntity> updated = ArgumentCaptor.forClass(LoadingPlanItemEntity.class);
|
||||
verify(repository).update(updated.capture());
|
||||
assertThat(updated.getValue().getPackageCount()).isEqualTo(60);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unchangedQuantityAndCargoSnapshotDoNotWriteAdjustmentHistory() {
|
||||
LoadingPlanEntity plan = plan("LOADING", 2);
|
||||
|
||||
+6
-4
@@ -97,11 +97,13 @@ class TaillegRealProducerAcceptanceTest {
|
||||
private void seedAllocation(long id, long inboundId, long palletId, long locationId,
|
||||
int packages, String suffix) {
|
||||
jdbc.update("INSERT IGNORE INTO fl_inbound_pallet_allocation(id,tenant_id,inbound_id,allocation_no," +
|
||||
"warehouse_id,warehouse_resource_tenant_id,pallet_id,pallet_code,pallet_capacity_cbm," +
|
||||
"block_id,slot_no,weight_result_id,pallet_sequence,warehouse_id,warehouse_resource_tenant_id,pallet_id,pallet_code,pallet_capacity_cbm," +
|
||||
"allocated_volume_cbm,allocated_package_count,picked_package_count,location_id,location_code," +
|
||||
"reservation_id,reservation_no,allocation_status) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'CONFIRMED')",
|
||||
id, CUSTOMER, inboundId, 1, 92L, 0L, palletId, MARKER + "-PALLET-" + suffix,
|
||||
1.2, 0.4, packages, packages, locationId, "A-01", id + 1000, MARKER + "-RES-" + suffix);
|
||||
"shared_overflow,reservation_id,slot_status,allocation_status) " +
|
||||
"VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'BOUND','CONFIRMED')",
|
||||
id, CUSTOMER, inboundId, 1, id + 3000, 1, id + 2000, 1, 92L, 0L,
|
||||
palletId, MARKER + "-PALLET-" + suffix, 1.2, 0.4, packages, packages,
|
||||
locationId, "A-01", false, id + 1000);
|
||||
}
|
||||
|
||||
private void seedPickItem(long id, long planItemId, long inboundId, long detailId, int packages) {
|
||||
|
||||
Reference in New Issue
Block a user