优化
This commit is contained in:
@@ -441,6 +441,133 @@ components:
|
|||||||
cartonMark: { type: string, minLength: 1 }
|
cartonMark: { type: string, minLength: 1 }
|
||||||
palletCode: { type: string, minLength: 1 }
|
palletCode: { type: string, minLength: 1 }
|
||||||
idempotencyKey: { type: string, minLength: 1, maxLength: 255 }
|
idempotencyKey: { type: string, minLength: 1, maxLength: 255 }
|
||||||
|
ReceivingApiResponseBase:
|
||||||
|
type: object
|
||||||
|
required: [code, message, timestamp]
|
||||||
|
properties:
|
||||||
|
code: { type: string, example: '00000' }
|
||||||
|
message: { type: string }
|
||||||
|
requestId: { type: string, nullable: true }
|
||||||
|
traceId: { type: string, nullable: true }
|
||||||
|
timestamp: { type: string, format: date-time }
|
||||||
|
ReceivingInbound:
|
||||||
|
type: object
|
||||||
|
required: [inboundCode, destinationType, plannedPalletCount, totalPackages, receivedPackages, pendingPackages]
|
||||||
|
properties:
|
||||||
|
inboundCode: { type: string }
|
||||||
|
destinationType: { type: string, enum: [FBA, OVERSEAS] }
|
||||||
|
destinationWarehouseId: { type: string, nullable: true }
|
||||||
|
destinationWarehouseCode: { type: string, nullable: true }
|
||||||
|
destinationWarehouseName: { type: string, nullable: true }
|
||||||
|
fbaWarehouseCode: { type: string, nullable: true }
|
||||||
|
plannedPalletCount: { type: integer }
|
||||||
|
totalPackages: { type: integer }
|
||||||
|
receivedPackages: { type: integer }
|
||||||
|
pendingPackages: { type: integer }
|
||||||
|
ReceivingCandidate:
|
||||||
|
type: object
|
||||||
|
required: [containerNo, status, planCount, inboundCodeCount, totalPackages, receivedPackages, currentAccount]
|
||||||
|
properties:
|
||||||
|
containerNo: { type: string }
|
||||||
|
status: { type: string, enum: [WAIT_SIGN, SIGNED, RECEIVING, COMPLETED] }
|
||||||
|
planCount: { type: integer }
|
||||||
|
inboundCodeCount: { type: integer }
|
||||||
|
totalPackages: { type: integer }
|
||||||
|
receivedPackages: { type: integer }
|
||||||
|
currentAccount: { type: boolean }
|
||||||
|
ReceivingSession:
|
||||||
|
type: object
|
||||||
|
required: [containerNo, status, version, photoFileIds, totalPackages, receivedPackages, pendingPackages, inbounds]
|
||||||
|
properties:
|
||||||
|
containerNo: { type: string }
|
||||||
|
status: { type: string, enum: [WAIT_SIGN, SIGNED, RECEIVING, COMPLETED] }
|
||||||
|
operatorName: { type: string, nullable: true }
|
||||||
|
version: { type: integer }
|
||||||
|
photoFileIds: { type: array, items: { type: integer, format: int64 } }
|
||||||
|
preparedAt: { type: string, format: date-time, nullable: true }
|
||||||
|
signedAt: { type: string, format: date-time, nullable: true }
|
||||||
|
receivingAt: { type: string, format: date-time, nullable: true }
|
||||||
|
completedAt: { type: string, format: date-time, nullable: true }
|
||||||
|
totalPackages: { type: integer }
|
||||||
|
receivedPackages: { type: integer }
|
||||||
|
pendingPackages: { type: integer }
|
||||||
|
inbounds: { type: array, items: { $ref: '#/components/schemas/ReceivingInbound' } }
|
||||||
|
ReceivingPhoto:
|
||||||
|
type: object
|
||||||
|
required: [fileId, fileName, contentType, sizeBytes]
|
||||||
|
properties:
|
||||||
|
fileId: { type: integer, format: int64 }
|
||||||
|
fileName: { type: string }
|
||||||
|
contentType: { type: string }
|
||||||
|
sizeBytes: { type: integer, format: int64 }
|
||||||
|
ReceivingCartonResolved:
|
||||||
|
allOf:
|
||||||
|
- { $ref: '#/components/schemas/ReceivingInbound' }
|
||||||
|
- type: object
|
||||||
|
required: [cartonMark]
|
||||||
|
properties:
|
||||||
|
cartonMark: { type: string }
|
||||||
|
skuCode: { type: string, nullable: true }
|
||||||
|
ReceivingPalletResolved:
|
||||||
|
type: object
|
||||||
|
required: [palletCode, warehouseId, status, inboundCode, destinationType, compatible]
|
||||||
|
properties:
|
||||||
|
palletCode: { type: string }
|
||||||
|
warehouseId: { type: integer, format: int64 }
|
||||||
|
status: { type: string }
|
||||||
|
fbaWarehouseCode: { type: string, nullable: true }
|
||||||
|
inboundCode: { type: string }
|
||||||
|
destinationType: { type: string, enum: [FBA, OVERSEAS] }
|
||||||
|
compatible: { type: boolean }
|
||||||
|
ReceivingReceipt:
|
||||||
|
type: object
|
||||||
|
required: [result, containerNo, inboundCode, destinationType, palletCode, acceptedPackages, receivedPackages, pendingPackages, sessionStatus]
|
||||||
|
properties:
|
||||||
|
result: { type: string }
|
||||||
|
containerNo: { type: string }
|
||||||
|
inboundCode: { type: string }
|
||||||
|
destinationType: { type: string, enum: [FBA, OVERSEAS] }
|
||||||
|
palletCode: { type: string }
|
||||||
|
acceptedPackages: { type: integer }
|
||||||
|
receivedPackages: { type: integer }
|
||||||
|
pendingPackages: { type: integer }
|
||||||
|
sessionStatus: { type: string, enum: [SIGNED, RECEIVING] }
|
||||||
|
ReceivingCandidateListApiResponse:
|
||||||
|
allOf:
|
||||||
|
- { $ref: '#/components/schemas/ReceivingApiResponseBase' }
|
||||||
|
- type: object
|
||||||
|
required: [data]
|
||||||
|
properties: { data: { type: array, items: { $ref: '#/components/schemas/ReceivingCandidate' } } }
|
||||||
|
ReceivingSessionApiResponse:
|
||||||
|
allOf:
|
||||||
|
- { $ref: '#/components/schemas/ReceivingApiResponseBase' }
|
||||||
|
- type: object
|
||||||
|
required: [data]
|
||||||
|
properties: { data: { $ref: '#/components/schemas/ReceivingSession' } }
|
||||||
|
ReceivingPhotoApiResponse:
|
||||||
|
allOf:
|
||||||
|
- { $ref: '#/components/schemas/ReceivingApiResponseBase' }
|
||||||
|
- type: object
|
||||||
|
required: [data]
|
||||||
|
properties: { data: { $ref: '#/components/schemas/ReceivingPhoto' } }
|
||||||
|
ReceivingCartonResolvedApiResponse:
|
||||||
|
allOf:
|
||||||
|
- { $ref: '#/components/schemas/ReceivingApiResponseBase' }
|
||||||
|
- type: object
|
||||||
|
required: [data]
|
||||||
|
properties: { data: { $ref: '#/components/schemas/ReceivingCartonResolved' } }
|
||||||
|
ReceivingPalletResolvedApiResponse:
|
||||||
|
allOf:
|
||||||
|
- { $ref: '#/components/schemas/ReceivingApiResponseBase' }
|
||||||
|
- type: object
|
||||||
|
required: [data]
|
||||||
|
properties: { data: { $ref: '#/components/schemas/ReceivingPalletResolved' } }
|
||||||
|
ReceivingReceiptApiResponse:
|
||||||
|
allOf:
|
||||||
|
- { $ref: '#/components/schemas/ReceivingApiResponseBase' }
|
||||||
|
- type: object
|
||||||
|
required: [data]
|
||||||
|
properties: { data: { $ref: '#/components/schemas/ReceivingReceipt' } }
|
||||||
SmartDispatchCandidate:
|
SmartDispatchCandidate:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
package com.baiyue.lastleg.inboundplan.pda.service;
|
||||||
|
|
||||||
|
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
|
||||||
|
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
|
||||||
|
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPalletAllocation;
|
||||||
|
import com.baiyue.lastleg.inboundplan.pda.entity.InboundPdaTask;
|
||||||
|
import com.baiyue.lastleg.inboundplan.pda.repository.PdaWorkflowRepository;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class InboundPdaContainerCompletionTest {
|
||||||
|
@Mock PdaWorkflowRepository repository;
|
||||||
|
@Mock InboundStockReceiptService stockReceipts;
|
||||||
|
@Mock InboundUnloadCompletedOutboxService outbox;
|
||||||
|
private InboundPdaWorkflowService service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
service = new InboundPdaWorkflowService();
|
||||||
|
ReflectionTestUtils.setField(service, "repository", repository);
|
||||||
|
ReflectionTestUtils.setField(service, "stockReceipts", stockReceipts);
|
||||||
|
ReflectionTestUtils.setField(service, "unloadCompletedOutbox", outbox);
|
||||||
|
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().findAndRegisterModules());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void twoPlansOfOneContainerStockInTogetherAndPublishOneContainerEvent() {
|
||||||
|
InboundPlan first = plan(1L, "PLAN-1");
|
||||||
|
InboundPlan second = plan(2L, "PLAN-2");
|
||||||
|
InboundPdaTask firstTask = task(11L, 1L);
|
||||||
|
InboundPdaTask secondTask = task(22L, 2L);
|
||||||
|
when(repository.lockTask("100", 1L)).thenReturn(firstTask);
|
||||||
|
when(repository.lockTask("100", 2L)).thenReturn(secondTask);
|
||||||
|
when(repository.lines(1L)).thenReturn(List.of(line(101L)));
|
||||||
|
when(repository.lines(2L)).thenReturn(List.of(line(202L)));
|
||||||
|
when(repository.allocations(1L)).thenReturn(List.of(allocation(101L)));
|
||||||
|
when(repository.allocations(2L)).thenReturn(List.of(allocation(202L)));
|
||||||
|
when(repository.cartons(any())).thenReturn(List.of());
|
||||||
|
when(repository.pallets(any())).thenReturn(List.of());
|
||||||
|
|
||||||
|
service.completeContainer("100", List.of(second, first));
|
||||||
|
service.completeContainer("100", List.of(first, second));
|
||||||
|
|
||||||
|
verify(stockReceipts, times(2)).stockIn(eq("100"), any(), any(), any(), any(), any(), any());
|
||||||
|
verify(outbox, times(1)).publish(eq(first), eq(firstTask), eq(List.of(10L)));
|
||||||
|
verify(repository, times(2)).update(any(InboundPlan.class));
|
||||||
|
verify(repository, times(2)).update(any(InboundPdaTask.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InboundPlan plan(long id, String planNo) {
|
||||||
|
InboundPlan value = new InboundPlan();
|
||||||
|
value.setId(id); value.setPlanNo(planNo); value.setCustomerId("100"); value.setContainerNo("CONT-1");
|
||||||
|
value.setStatus("RECEIVING");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InboundPdaTask task(long id, long planId) {
|
||||||
|
InboundPdaTask value = new InboundPdaTask();
|
||||||
|
LocalDateTime now = LocalDateTime.of(2026, 8, 24, 10, 0);
|
||||||
|
value.setId(id); value.setPlanId(planId); value.setCustomerId("100"); value.setTaskNo("TASK-" + id);
|
||||||
|
value.setStatus("RECEIVING"); value.setUnloadPhotoFileIds("[10]"); value.setUnloadPhotoUploadedAt(now);
|
||||||
|
value.setEstimatedEmptyReturnAt(now.plusDays(2)); value.setVersion(1);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InboundPlanLine line(long id) {
|
||||||
|
InboundPlanLine value = new InboundPlanLine();
|
||||||
|
value.setId(id); value.setRequiredPackageCount(1); value.setDestinationType("FBA");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InboundPlanPalletAllocation allocation(long lineId) {
|
||||||
|
InboundPlanPalletAllocation value = new InboundPlanPalletAllocation();
|
||||||
|
value.setLineId(lineId); value.setPickedPackageCount(1); value.setCountedPackageCount(1);
|
||||||
|
value.setActualVolumeCbm(BigDecimal.ONE);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
+228
@@ -0,0 +1,228 @@
|
|||||||
|
package com.baiyue.lastleg.inboundplan.pda.service;
|
||||||
|
|
||||||
|
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||||
|
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
|
||||||
|
import com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton;
|
||||||
|
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
|
||||||
|
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPallet;
|
||||||
|
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPalletAllocation;
|
||||||
|
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper;
|
||||||
|
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanLineMapper;
|
||||||
|
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanMapper;
|
||||||
|
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletAllocationMapper;
|
||||||
|
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletMapper;
|
||||||
|
import com.baiyue.lastleg.inboundplan.pda.dto.ReceivingContainerRequests;
|
||||||
|
import com.baiyue.lastleg.inboundplan.pda.entity.ReceivingContainerOperation;
|
||||||
|
import com.baiyue.lastleg.inboundplan.pda.entity.ReceivingContainerSession;
|
||||||
|
import com.baiyue.lastleg.inboundplan.pda.gateway.UnloadPhotoFileGateway;
|
||||||
|
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||||
|
import com.baiyue.lastleg.inboundplan.pda.mapper.ReceivingContainerOperationMapper;
|
||||||
|
import com.baiyue.lastleg.inboundplan.pda.mapper.ReceivingContainerSessionMapper;
|
||||||
|
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class ReceivingContainerServiceBehaviorTest {
|
||||||
|
@Mock InboundPlanMapper plans;
|
||||||
|
@Mock InboundPlanLineMapper lines;
|
||||||
|
@Mock InboundPlanPalletMapper pallets;
|
||||||
|
@Mock InboundPlanPalletAllocationMapper allocations;
|
||||||
|
@Mock InboundPlanCartonMapper cartons;
|
||||||
|
@Mock LastlegOrderMapper orders;
|
||||||
|
@Mock ReceivingContainerSessionMapper sessions;
|
||||||
|
@Mock ReceivingContainerOperationMapper operations;
|
||||||
|
@Mock WarehouseResourceGateway warehouse;
|
||||||
|
@Mock UnloadPhotoFileGateway unloadPhotos;
|
||||||
|
@Mock InboundPdaWorkflowService workflow;
|
||||||
|
private ReceivingContainerService service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
service = new ReceivingContainerService();
|
||||||
|
ReflectionTestUtils.setField(service, "plans", plans);
|
||||||
|
ReflectionTestUtils.setField(service, "lines", lines);
|
||||||
|
ReflectionTestUtils.setField(service, "pallets", pallets);
|
||||||
|
ReflectionTestUtils.setField(service, "allocations", allocations);
|
||||||
|
ReflectionTestUtils.setField(service, "cartons", cartons);
|
||||||
|
ReflectionTestUtils.setField(service, "orders", orders);
|
||||||
|
ReflectionTestUtils.setField(service, "sessions", sessions);
|
||||||
|
ReflectionTestUtils.setField(service, "operations", operations);
|
||||||
|
ReflectionTestUtils.setField(service, "warehouse", warehouse);
|
||||||
|
ReflectionTestUtils.setField(service, "unloadPhotos", unloadPhotos);
|
||||||
|
ReflectionTestUtils.setField(service, "workflow", workflow);
|
||||||
|
ReflectionTestUtils.setField(service, "sourceObjectMapper", new ObjectMapper().findAndRegisterModules());
|
||||||
|
service.initializeObjectMapper();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void prepareIdempotencyHashIncludesContainerPath() {
|
||||||
|
when(plans.selectList(any())).thenReturn(List.of(plan("CONT-A")));
|
||||||
|
when(lines.selectList(any())).thenReturn(List.of());
|
||||||
|
when(sessions.insert(any())).thenReturn(1);
|
||||||
|
when(operations.insert(any())).thenReturn(1);
|
||||||
|
service.prepare("100", "7", "receiver", "CONT-A", new ReceivingContainerRequests.Action("same-key"));
|
||||||
|
ArgumentCaptor<ReceivingContainerOperation> saved = ArgumentCaptor.forClass(ReceivingContainerOperation.class);
|
||||||
|
verify(operations).insert(saved.capture());
|
||||||
|
when(operations.find("100", "7", "same-key")).thenReturn(saved.getValue());
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.prepare("100", "7", "receiver", "CONT-B",
|
||||||
|
new ReceivingContainerRequests.Action("same-key"))).isInstanceOf(ResponseStatusException.class)
|
||||||
|
.hasMessageContaining("409");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void waitSignSelectionAlreadyLocksAccountFromViewingAnotherContainer() {
|
||||||
|
when(plans.selectList(any())).thenReturn(List.of(plan("CONT-B")));
|
||||||
|
when(sessions.findActiveByOperator("100", "100:7")).thenReturn(session("CONT-A", "WAIT_SIGN"));
|
||||||
|
assertThatThrownBy(() -> service.detail("100", "7", "CONT-B"))
|
||||||
|
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("409");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void destinationModesAreHardRejectedBeforeWarehouseCalls() {
|
||||||
|
stubCurrent("OVERSEAS", "CONT-A");
|
||||||
|
assertThatThrownBy(() -> service.receiveBulk("100", "7", "receiver",
|
||||||
|
new ReceivingContainerRequests.BulkReceive("IN-1", "PAL-1", 1, "bulk-key")))
|
||||||
|
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("409");
|
||||||
|
verifyNoInteractions(warehouse);
|
||||||
|
|
||||||
|
org.mockito.Mockito.reset(plans, lines, orders, sessions, operations, cartons, warehouse);
|
||||||
|
stubCurrent("FBA", "CONT-A");
|
||||||
|
InboundPlanCarton carton = carton(1L, "BOX-1");
|
||||||
|
when(cartons.selectOne(any())).thenReturn(carton);
|
||||||
|
assertThatThrownBy(() -> service.receiveCarton("100", "7", "receiver",
|
||||||
|
new ReceivingContainerRequests.CartonReceive("BOX-1", "PAL-1", "carton-key")))
|
||||||
|
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("409");
|
||||||
|
verifyNoInteractions(warehouse);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void crossContainerCartonAndMismatchedPalletAreRejectedBeforeReservation() {
|
||||||
|
stubCurrent("OVERSEAS", "CONT-A");
|
||||||
|
when(cartons.selectOne(any())).thenReturn(carton(999L, "BOX-X"));
|
||||||
|
assertThatThrownBy(() -> service.receiveCarton("100", "7", "receiver",
|
||||||
|
new ReceivingContainerRequests.CartonReceive("BOX-X", "PAL-1", "cross-key")))
|
||||||
|
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("409");
|
||||||
|
verifyNoInteractions(warehouse);
|
||||||
|
|
||||||
|
org.mockito.Mockito.reset(plans, lines, orders, sessions, operations, cartons, warehouse, pallets, workflow);
|
||||||
|
stubCurrent("FBA", "CONT-A");
|
||||||
|
when(warehouse.resolvePallet("PAL-1")).thenReturn(resource(101L, "FBA-1"));
|
||||||
|
assertThatThrownBy(() -> service.receiveBulk("100", "7", "receiver",
|
||||||
|
new ReceivingContainerRequests.BulkReceive("IN-1", "PAL-1", 1, "pallet-key")))
|
||||||
|
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("409");
|
||||||
|
verify(workflow, never()).assignResolvedPallet(any(), any(), any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void overReceiptAndDuplicateCartonFailBeforeRemoteReservation() {
|
||||||
|
stubCurrent("FBA", "CONT-A");
|
||||||
|
when(warehouse.resolvePallet("PAL-1")).thenReturn(resource(100L, "FBA-1"));
|
||||||
|
when(pallets.selectList(any())).thenReturn(List.of(planPallet()));
|
||||||
|
doThrow(new ResponseStatusException(org.springframework.http.HttpStatus.CONFLICT, "over receipt"))
|
||||||
|
.when(workflow).validateBulkResolved(any(), any(), any(), any(Integer.class));
|
||||||
|
assertThatThrownBy(() -> service.receiveBulk("100", "7", "receiver",
|
||||||
|
new ReceivingContainerRequests.BulkReceive("IN-1", "PAL-1", 9, "over-key")))
|
||||||
|
.isInstanceOf(ResponseStatusException.class);
|
||||||
|
verify(workflow, never()).assignResolvedPallet(any(), any(), any(), any());
|
||||||
|
|
||||||
|
org.mockito.Mockito.reset(plans, lines, orders, sessions, operations, cartons, warehouse, pallets, workflow);
|
||||||
|
stubCurrent("OVERSEAS", "CONT-A");
|
||||||
|
when(cartons.selectOne(any())).thenReturn(carton(1L, "BOX-1"));
|
||||||
|
when(warehouse.resolvePallet("PAL-1")).thenReturn(resource(100L, null));
|
||||||
|
when(pallets.selectList(any())).thenReturn(List.of(planPallet()));
|
||||||
|
doThrow(new ResponseStatusException(org.springframework.http.HttpStatus.CONFLICT, "duplicate carton"))
|
||||||
|
.when(workflow).validateCartonResolved(any(), any(), any(), any(), any());
|
||||||
|
assertThatThrownBy(() -> service.receiveCarton("100", "7", "receiver",
|
||||||
|
new ReceivingContainerRequests.CartonReceive("BOX-1", "PAL-1", "duplicate-key")))
|
||||||
|
.isInstanceOf(ResponseStatusException.class);
|
||||||
|
verify(workflow, never()).assignResolvedPallet(any(), any(), any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void containerCompletionRequiresEveryInboundCodeToBalanceExactly() {
|
||||||
|
stubCurrent("FBA", "CONT-A");
|
||||||
|
InboundPlanPalletAllocation partial = new InboundPlanPalletAllocation();
|
||||||
|
partial.setLineId(10L); partial.setPickedPackageCount(1);
|
||||||
|
when(allocations.selectList(any())).thenReturn(List.of(partial));
|
||||||
|
assertThatThrownBy(() -> service.complete("100", "7", new ReceivingContainerRequests.Action("complete-key")))
|
||||||
|
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("409");
|
||||||
|
verify(workflow, never()).completeContainer(any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stubCurrent(String destinationType, String containerNo) {
|
||||||
|
InboundPlan plan = plan(containerNo);
|
||||||
|
InboundPlanLine line = line(destinationType);
|
||||||
|
LastlegOrder order = order(destinationType);
|
||||||
|
when(sessions.lockActiveByOperator("100", "100:7")).thenReturn(session(containerNo, "SIGNED"));
|
||||||
|
when(plans.selectList(any())).thenReturn(List.of(plan));
|
||||||
|
when(lines.selectList(any())).thenReturn(List.of(line));
|
||||||
|
when(orders.selectById(1L)).thenReturn(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InboundPlan plan(String containerNo) {
|
||||||
|
InboundPlan value = new InboundPlan();
|
||||||
|
value.setId(2L); value.setPlanNo("PLAN-1"); value.setCustomerId("100"); value.setContainerNo(containerNo);
|
||||||
|
value.setStatus("SIGNED");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InboundPlanLine line(String destinationType) {
|
||||||
|
InboundPlanLine value = new InboundPlanLine();
|
||||||
|
value.setId(10L); value.setCustomerId("100"); value.setPlanId(2L); value.setOrderId(1L);
|
||||||
|
value.setInboundCode("IN-1"); value.setDestinationType(destinationType); value.setRequiredPackageCount(2);
|
||||||
|
value.setDestinationWarehouseId("100"); value.setDestinationWarehouseCode("WH-100");
|
||||||
|
value.setFbaWarehouseCode("FBA".equals(destinationType) ? "FBA-1" : null);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LastlegOrder order(String destinationType) {
|
||||||
|
LastlegOrder value = new LastlegOrder();
|
||||||
|
value.setId(1L); value.setCustomerId("100"); value.setDestinationType(destinationType);
|
||||||
|
value.setOverseasWarehouseId("100"); value.setFbaWarehouseCode("FBA".equals(destinationType) ? "FBA-1" : null);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReceivingContainerSession session(String containerNo, String status) {
|
||||||
|
ReceivingContainerSession value = new ReceivingContainerSession();
|
||||||
|
value.setId(8L); value.setCustomerId("100"); value.setContainerNo(containerNo); value.setOperatorKey("7");
|
||||||
|
value.setStatus(status); value.setVersion(0);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InboundPlanCarton carton(long orderId, String cartonMark) {
|
||||||
|
InboundPlanCarton value = new InboundPlanCarton();
|
||||||
|
value.setOrderId(orderId); value.setCartonMark(cartonMark); value.setStatus("UNASSIGNED");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InboundPlanPallet planPallet() {
|
||||||
|
InboundPlanPallet value = new InboundPlanPallet();
|
||||||
|
value.setId(20L); value.setCustomerId("100"); value.setPlanId(2L);
|
||||||
|
value.setDestinationWarehouseId("100"); value.setDestinationWarehouseCode("WH-100");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WarehouseResourceGateway.WarehousePalletReference resource(long warehouseId, String fbaCode) {
|
||||||
|
return new WarehouseResourceGateway.WarehousePalletReference(30L, "PAL-1", warehouseId, 40L,
|
||||||
|
"AVAILABLE", fbaCode, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user