feat: complete finance and overseas receiving workflow

This commit is contained in:
2026-09-10 22:43:11 +08:00
parent e531ae9407
commit 56635ad4e4
66 changed files with 1601 additions and 417 deletions
@@ -69,6 +69,6 @@ class DropshipOrderControllerContractTest {
return new DropshipRequests.Receive("100", 10L, 20L, "event-1",
List.of(new DropshipRequests.Pallet("PALLET-1",
List.of(new DropshipRequests.SkuPackages("SKU-1", "Product", 1, 2,
List.of("CARTON-1"))))));
List.of("CARTON-1"))), 1, 1, 1, "0".repeat(64))));
}
}
@@ -52,10 +52,10 @@ class DropshipOrderServiceTest {
List.of(
new DropshipRequests.Pallet("SOURCE-P-1", List.of(
new DropshipRequests.SkuPackages("SKU-A", "Product A", 2, 20,
List.of("MARK-1", "MARK-2")))),
List.of("MARK-1", "MARK-2"))), 1, 2, 1, "0".repeat(64)),
new DropshipRequests.Pallet("SOURCE-P-2", List.of(
new DropshipRequests.SkuPackages("SKU-A", "Product A", 1, 10,
List.of("MARK-3"))))));
List.of("MARK-3"))), 1, 1, 1, "1".repeat(64))));
assertThat(service.receive(request)).isEqualTo(3);
verifyNoInteractions(warehouse);
@@ -38,8 +38,9 @@ class ReceivingContainerContractTest {
.map(RecordComponent::getName).collect(Collectors.toSet());
assertTrue(java.util.Collections.disjoint(fields, FORBIDDEN_CALLER_SCOPE), type.getSimpleName());
}
for (Class<?> type : Set.of(ReceivingContainerRequests.Action.class, ReceivingContainerRequests.Sign.class,
ReceivingContainerRequests.BulkReceive.class, ReceivingContainerRequests.CartonReceive.class)) {
for (Class<?> type : Set.of(ReceivingContainerRequests.Action.class, ReceivingContainerRequests.Complete.class,
ReceivingContainerRequests.Sign.class, ReceivingContainerRequests.BulkReceive.class,
ReceivingContainerRequests.CartonReceive.class, ReceivingContainerRequests.WholePalletReceive.class)) {
RecordComponent key = Arrays.stream(type.getRecordComponents())
.filter(component -> "idempotencyKey".equals(component.getName())).findFirst().orElseThrow();
Size size = key.getAccessor().getAnnotation(Size.class);
@@ -67,7 +68,7 @@ class ReceivingContainerContractTest {
@Test
void inventoryCreatingEntryTransactionsAvoidMissingDimensionGapLockDeadlocks() throws NoSuchMethodException {
Transactional pda = ReceivingContainerService.class
.getMethod("complete", String.class, ReceivingContainerRequests.Action.class)
.getMethod("complete", String.class, ReceivingContainerRequests.Complete.class)
.getAnnotation(Transactional.class);
Transactional bridge = LoadingCompletedConsumer.class.getMethod("consume", EventEnvelope.class)
.getAnnotation(Transactional.class);
@@ -103,6 +104,10 @@ class ReceivingContainerContractTest {
assertTrue(unified.contains("alter table ll_container_shipping_gate drop column customer_id"));
assertTrue(schema.contains("create table if not exists ll_receiving_sku_receipt"));
assertTrue(schema.contains("unique key uk_ll_receiving_sku_pallet (customer_id,line_id,pallet_id,sku_code)"));
assertTrue(schema.contains("create table if not exists ll_receiving_fba_pallet_count"));
assertTrue(schema.contains("unique key uk_ll_receiving_fba_count(session_id,group_key,pallet_code)"));
assertTrue(tableBlock(schema, "ll_inbound_plan_pallet").contains("key idx_ll_inbound_pallet_no (pallet_no)"));
assertFalse(tableBlock(schema, "ll_inbound_plan_pallet").contains("unique key uk_ll_inbound_pallet_no"));
String order = tableBlock(schema, "ll_order");
assertTrue(order.contains("overseas_warehouse_id varchar(128) null"));
assertTrue(order.contains("overseas_warehouse_code varchar(128) null"));
@@ -113,6 +118,7 @@ class ReceivingContainerContractTest {
assertTrue(receivingSession.contains("overseas_warehouse_name varchar(255) not null"));
assertFalse(receivingSession.contains("photo_file_ids json"));
assertTrue(unified.contains("create table if not exists ll_receiving_sku_receipt"));
assertTrue(unified.contains("create table if not exists ll_receiving_fba_pallet_count"));
assertTrue(unified.contains("column_name='sign_photos'"));
assertTrue(schema.contains("wait_sign、signed、receiving、completed"));
assertTrue(unified.contains("customer_id is null or l.customer_id=''"));
@@ -120,7 +126,7 @@ class ReceivingContainerContractTest {
assertTrue(unified.contains("customer_id is null or a.customer_id=''"));
assertTrue(unified.contains("signal sqlstate '45000'"));
assertTrue(unified.contains("assert_receiving_global_locks_migratable"));
assertTrue(unified.contains("where status='completed' and (active_operator_key is not null or active_container_key is not null)"));
assertTrue(unified.contains("where status in ('completed','completed_with_difference') and (active_operator_key is not null or active_container_key is not null)"));
assertTrue(unified.contains("concat('operator:',operator_key)"));
assertTrue(unified.contains("concat('container:',upper(trim(container_no)))"));
assertTrue(unified.contains("assert_receiving_operation_global_key_migratable"));
@@ -30,6 +30,7 @@ import org.springframework.mock.web.MockMultipartFile;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -596,6 +597,24 @@ class InboundPdaWorkflowServiceTest {
any(), any(), anyLong());
}
@Test
void completionLoadsSharedFbaPalletOwnedByAnotherCustomerPlan() {
activeTask();
target.setPickedPackageCount(5);
InboundPlanPallet sharedPallet = pallet(40L, "WH-A", 5, "1.0000", 400L);
sharedPallet.setPlanId(99L);
target.setPalletId(sharedPallet.getId());
when(repository.allocations(plan.getId())).thenReturn(List.of(target));
when(repository.lines(plan.getId())).thenReturn(List.of(targetLine));
when(repository.pallets(plan.getId())).thenReturn(List.of());
when(repository.palletsByIds(Set.of(sharedPallet.getId()))).thenReturn(List.of(sharedPallet));
service.complete("100", "LIP1");
verify(stockReceipts).stockIn(eq("100"), eq(plan), eq(task), eq(List.of(targetLine)),
eq(List.of(target)), eq(List.of(sharedPallet)), any());
}
@Test
void completedTaskReplayDoesNotRunStockInAgain() {
task.setStatus("COMPLETED");
@@ -16,11 +16,17 @@ import com.baiyue.lastleg.inboundplan.pda.dto.ReceivingContainerRequests;
import com.baiyue.lastleg.inboundplan.pda.dto.ReceivingPhotoRole;
import com.baiyue.lastleg.inboundplan.pda.entity.ReceivingContainerOperation;
import com.baiyue.lastleg.inboundplan.pda.entity.ReceivingContainerSession;
import com.baiyue.lastleg.inboundplan.pda.entity.ReceivingFbaPalletCount;
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.inboundplan.pda.mapper.ReceivingFbaPalletCountMapper;
import com.baiyue.lastleg.inboundplan.pda.mapper.ReceivingSkuReceiptMapper;
import com.baiyue.lastleg.dropship.mapper.DropshipCartonMapper;
import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper;
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
import com.baiyue.lastleg.dropship.entity.DropshipOrderProfile;
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.shipping.mapper.ContainerShippingGateMapper;
@@ -58,8 +64,11 @@ class ReceivingContainerServiceBehaviorTest {
@Mock LastlegOrderDetailMapper orderDetails;
@Mock ReceivingContainerSessionMapper sessions;
@Mock ReceivingContainerOperationMapper operations;
@Mock ReceivingFbaPalletCountMapper fbaPalletCounts;
@Mock ReceivingSkuReceiptMapper skuReceipts;
@Mock ContainerShippingGateMapper shippingGates;
@Mock DropshipCartonMapper dropshipCartons;
@Mock DropshipOrderProfileMapper dropshipProfiles;
@Mock WarehouseResourceGateway warehouse;
@Mock UnloadPhotoFileGateway unloadPhotos;
@Mock InboundPdaWorkflowService workflow;
@@ -77,8 +86,11 @@ class ReceivingContainerServiceBehaviorTest {
ReflectionTestUtils.setField(service, "orderDetails", orderDetails);
ReflectionTestUtils.setField(service, "sessions", sessions);
ReflectionTestUtils.setField(service, "operations", operations);
ReflectionTestUtils.setField(service, "fbaPalletCounts", fbaPalletCounts);
ReflectionTestUtils.setField(service, "skuReceipts", skuReceipts);
ReflectionTestUtils.setField(service, "shippingGates", shippingGates);
ReflectionTestUtils.setField(service, "dropshipCartons", dropshipCartons);
ReflectionTestUtils.setField(service, "dropshipProfiles", dropshipProfiles);
ReflectionTestUtils.setField(service, "warehouse", warehouse);
ReflectionTestUtils.setField(service, "unloadPhotos", unloadPhotos);
ReflectionTestUtils.setField(service, "workflow", workflow);
@@ -414,11 +426,10 @@ class ReceivingContainerServiceBehaviorTest {
org.mockito.Mockito.reset(plans, lines, orders, sessions, operations, cartons, warehouse, pallets, workflow);
stubCurrent("FBA", "CONT-A");
stubSku();
when(warehouse.resolveAvailablePallet("PAL-1")).thenReturn(resource(101L, "FBA-1"));
assertThatThrownBy(() -> service.receiveBulk("7", "receiver",
new ReceivingContainerRequests.BulkReceive("IN-1", "PAL-1", 1, "pallet-key")))
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("409");
.isInstanceOfSatisfying(ResponseStatusException.class,
error -> assertThat(error.getStatusCode().value()).isEqualTo(404));
verify(workflow, never()).assignResolvedPallet(any(), any(), any(), any());
}
@@ -426,7 +437,7 @@ class ReceivingContainerServiceBehaviorTest {
void overReceiptAndDuplicateCartonFailBeforeRemoteReservation() {
stubCurrent("FBA", "CONT-A");
stubSku();
when(pallets.selectList(any())).thenReturn(List.of(planPallet()));
stubReservedFbaPallet();
doThrow(new ResponseStatusException(org.springframework.http.HttpStatus.CONFLICT, "over receipt"))
.when(workflow).validateBulkResolved(any(), any(), any(), anyInt());
assertThatThrownBy(() -> service.receiveBulk("7", "receiver",
@@ -442,6 +453,30 @@ class ReceivingContainerServiceBehaviorTest {
verify(workflow, never()).assignResolvedPallet(any(), any(), any(), any());
}
@Test
void fbaOverReceiptAllocatesOnlyPlannedBalanceAndRecordsPhysicalDifference() {
stubCurrent("FBA", "CONT-A");
stubSku();
stubReservedFbaPallet();
when(workflow.receiveBulkResolved(any(), any(), any(), any(), anyInt(), any(), any()))
.thenAnswer(invocation -> invocation.getArgument(4));
when(fbaPalletCounts.insert(any(ReceivingFbaPalletCount.class))).thenReturn(1);
when(fbaPalletCounts.sumActual(8L, "FBA:CONT-A:FBA-1")).thenReturn(3);
when(sessions.updateById(any(ReceivingContainerSession.class))).thenReturn(1);
when(operations.insert(any(ReceivingContainerOperation.class))).thenReturn(1);
var result = service.receiveBulk("7", "receiver",
new ReceivingContainerRequests.BulkReceive("IN-1", "PAL-1", 3, "over-mark-key"));
assertThat(result.result()).isEqualTo("DIFFERENCE_RECORDED");
assertThat(result.acceptedPackages()).isEqualTo(3);
verify(workflow).receiveBulkResolved(any(), any(), any(), any(),
org.mockito.ArgumentMatchers.eq(2), any(), any());
ArgumentCaptor<ReceivingFbaPalletCount> count = ArgumentCaptor.forClass(ReceivingFbaPalletCount.class);
verify(fbaPalletCounts).insert(count.capture());
assertThat(count.getValue().getActualPackages()).isEqualTo(3);
}
@Test
void overseasPalletUsesActualReceivingWarehouseInsteadOfBusinessDestinationId() {
stubCurrent("OVERSEAS", "CONT-A");
@@ -464,7 +499,7 @@ class ReceivingContainerServiceBehaviorTest {
InboundPlanPalletAllocation partial = new InboundPlanPalletAllocation();
partial.setLineId(10L); partial.setPickedPackageCount(1);
when(allocations.selectList(any())).thenReturn(List.of(partial));
assertThatThrownBy(() -> service.complete("7", new ReceivingContainerRequests.Action("complete-key")))
assertThatThrownBy(() -> service.complete("7", new ReceivingContainerRequests.Complete(false, "complete-key")))
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("409");
verify(workflow, never()).completeContainer(any());
}
@@ -480,7 +515,7 @@ class ReceivingContainerServiceBehaviorTest {
when(allocations.selectList(any())).thenReturn(List.of(received));
when(sessions.updateById(any(ReceivingContainerSession.class))).thenReturn(1);
var completed = service.complete("7", new ReceivingContainerRequests.Action("dropship-complete"));
var completed = service.complete("7", new ReceivingContainerRequests.Complete(false, "dropship-complete"));
assertThat(completed.status()).isEqualTo("COMPLETED");
verifyNoInteractions(skuReceipts);
@@ -491,11 +526,11 @@ class ReceivingContainerServiceBehaviorTest {
void completedRequestReplaysWithoutAnActiveCustomerSession() {
ReceivingContainerOperation operation = new ReceivingContainerOperation();
operation.setOperatorKey("7"); operation.setAction("COMPLETE"); operation.setIdempotencyKey("complete-replay");
operation.setRequestHash(hash(new ReceivingContainerRequests.Action("complete-replay")));
operation.setRequestHash(hash(new ReceivingContainerRequests.Complete(false, "complete-replay")));
operation.setResultJson("{\"containerNo\":\"CONT-A\",\"status\":\"COMPLETED\",\"operatorName\":\"receiver\",\"version\":1,\"photos\":[],\"totalPackages\":2,\"receivedPackages\":2,\"pendingPackages\":0,\"inbounds\":[]}");
when(operations.find("7", "complete-replay")).thenReturn(operation);
assertThat(service.complete("7", new ReceivingContainerRequests.Action("complete-replay")).status())
assertThat(service.complete("7", new ReceivingContainerRequests.Complete(false, "complete-replay")).status())
.isEqualTo("COMPLETED");
verifyNoInteractions(sessions);
}
@@ -504,8 +539,7 @@ class ReceivingContainerServiceBehaviorTest {
void localFailureAfterRemoteReservationTriggersReleaseCompensation() {
stubCurrent("FBA", "CONT-A");
stubSku();
when(warehouse.resolveAvailablePallet("PAL-1")).thenReturn(resource(100L, "FBA-1"));
when(pallets.selectList(any())).thenReturn(List.of(planPallet()));
stubReservedFbaPallet();
when(workflow.assignResolvedPallet(any(), any(), any(), any())).thenReturn(55L);
ResponseStatusException failure = new ResponseStatusException(org.springframework.http.HttpStatus.CONFLICT, "race");
doThrow(failure).when(workflow).receiveBulkResolved(any(), any(), any(), any(), anyInt(), any(), any());
@@ -675,9 +709,10 @@ class ReceivingContainerServiceBehaviorTest {
}
@Test
void bulkRejectsAggregateSkuOverReceiptBeforeWarehouseLookup() {
void bulkRejectsInconsistentFrozenSkuProgressBeforeAnyReceiptWrite() {
stubCurrent("FBA", "CONT-A");
stubSku();
stubReservedFbaPallet();
com.baiyue.lastleg.inboundplan.pda.entity.ReceivingSkuReceipt received =
new com.baiyue.lastleg.inboundplan.pda.entity.ReceivingSkuReceipt();
received.setReceivedPackages(2);
@@ -686,7 +721,7 @@ class ReceivingContainerServiceBehaviorTest {
assertThatThrownBy(() -> service.receiveBulk("7", "receiver",
new ReceivingContainerRequests.BulkReceive("IN-1", "PAL-1", 1, "sku-over")))
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("409");
verifyNoInteractions(warehouse);
verify(workflow, never()).assignResolvedPallet(any(), any(), any(), any());
}
@Test
@@ -707,7 +742,7 @@ class ReceivingContainerServiceBehaviorTest {
}
@Test
void cartonResolutionReturnsAssignmentOptionalMergeAndUrgencyFromInboundPlan() {
void fbaGroupResolutionKeepsUrgencyWithoutPerPlanMergeChoices() {
stubCurrent("FBA", "CONT-A");
InboundPlan flagged = plan("CONT-A");
flagged.setAssignedFlag(true);
@@ -719,9 +754,9 @@ class ReceivingContainerServiceBehaviorTest {
assertThat(resolved.inboundCode()).isEqualTo("IN-1");
assertThat(resolved.assigned()).isTrue();
assertThat(resolved.mergeAllowed()).isTrue();
assertThat(resolved.mergeAllowed()).isFalse();
assertThat(resolved.urgent()).isTrue();
assertThat(resolved.mergeHint()).contains("可选择", "同目的仓", "新托盘码", "不强制");
assertThat(resolved.mergeHint()).isNull();
}
@Test
@@ -788,8 +823,7 @@ class ReceivingContainerServiceBehaviorTest {
when(orders.selectById(2L)).thenReturn(secondOrder);
when(orderDetails.selectList(any())).thenReturn(List.of(detail("SKU-A", 1), detail("SKU-B", 1)),
List.of(detail("SKU-C", 2)));
when(warehouse.resolveAvailablePallet("PAL-1")).thenReturn(resource(100L, "FBA-1"));
when(pallets.selectList(any())).thenReturn(List.of(planPallet()));
stubReservedFbaPallet();
when(workflow.receiveBulkResolved(any(), any(), any(), any(), anyInt(), any(), any()))
.thenAnswer(invocation -> invocation.getArgument(4));
when(sessions.updateById(any(ReceivingContainerSession.class))).thenReturn(1);
@@ -813,22 +847,27 @@ class ReceivingContainerServiceBehaviorTest {
}
@Test
void overseasDropshipResolvesOnlyFrozenCartonsAndNeverCreatesLabelsWhileResolving() {
void dropshipCartonResolvesItsCompleteDpInsteadOfAPlanOrCustomer() {
stubCurrent("OVERSEAS", "CONT-A");
LastlegOrder dropship = order("OVERSEAS"); dropship.setOrderType("ONE_CLICK_DROPSHIP");
when(orders.selectById(1L)).thenReturn(dropship);
InboundPlanCarton frozen = carton(1L, "IN-1#2-1");
frozen.setCustomerId("100"); frozen.setSourceInboundDetailId("d-1"); frozen.setSkuCode("SKU-1");
when(cartons.selectOne(any())).thenReturn(frozen);
InboundPlanPallet pallet = planPallet(); pallet.setSourcePalletNo("DP-260909-0001");
pallet.setWarehousePalletId(30L);
InboundPlanPalletAllocation allocation = new InboundPlanPalletAllocation();
allocation.setId(30L); allocation.setPlanId(2L); allocation.setLineId(10L);
allocation.setPalletId(20L); allocation.setPlannedPackageCount(2); allocation.setPickedPackageCount(0);
when(dropshipCartons.sourcePalletsInContainer("CONT-A", "IN-1#2-1"))
.thenReturn(List.of("DP-260909-0001"));
when(pallets.selectList(any())).thenReturn(List.of(pallet));
when(allocations.selectList(any())).thenReturn(List.of(allocation));
stubCompleteManifest("DP-260909-0001", 2);
var resolved = service.resolveCarton("7", new ReceivingContainerRequests.CartonResolve("IN-1#2-1"));
assertThat(resolved.inboundCode()).isEqualTo("IN-1");
assertThat(resolved.destinationType()).isEqualTo("OVERSEAS");
assertThat(resolved.operationMode()).isEqualTo("DROPSHIP_PALLET");
assertThat(resolved.sourcePalletNo()).isEqualTo("DP-260909-0001");
assertThat(resolved.totalPackages()).isEqualTo(2);
verify(cartons, never()).insert(any(InboundPlanCarton.class));
verifyNoInteractions(warehouse);
when(cartons.selectOne(any())).thenReturn(null);
assertThatThrownBy(() -> service.resolveCarton("7", new ReceivingContainerRequests.CartonResolve("IN-1#2-9")))
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("404");
verify(cartons, never()).insert(any(InboundPlanCarton.class));
}
@Test
@@ -837,7 +876,8 @@ class ReceivingContainerServiceBehaviorTest {
LastlegOrder dropship = order("OVERSEAS"); dropship.setOrderType("ONE_CLICK_DROPSHIP");
when(orders.selectById(1L)).thenReturn(dropship);
InboundPlanPallet pallet = planPallet();
pallet.setPalletNo("PAL-PHYSICAL-1"); pallet.setSourcePalletNo("DP-260909-0001");
pallet.setPalletNo("DP-260909-0001"); pallet.setSourcePalletNo("DP-260909-0001");
pallet.setWarehousePalletId(30L);
InboundPlanPalletAllocation pending = new InboundPlanPalletAllocation();
pending.setId(30L); pending.setCustomerId("100"); pending.setPlanId(2L);
pending.setLineId(10L); pending.setPalletId(20L); pending.setPlannedPackageCount(2);
@@ -847,6 +887,7 @@ class ReceivingContainerServiceBehaviorTest {
received.setPickedPackageCount(2);
when(pallets.selectList(any())).thenReturn(List.of(pallet));
when(allocations.selectList(any())).thenReturn(List.of(pending), List.of(received));
stubCompleteManifest("DP-260909-0001", 2);
when(operations.insert(any(ReceivingContainerOperation.class))).thenReturn(1);
when(sessions.updateById(any(ReceivingContainerSession.class))).thenReturn(1);
@@ -856,7 +897,7 @@ class ReceivingContainerServiceBehaviorTest {
assertThat(result.acceptedPackages()).isEqualTo(2);
assertThat(result.receivedPackages()).isEqualTo(2);
assertThat(result.pendingPackages()).isZero();
assertThat(result.palletCode()).isEqualTo("PAL-PHYSICAL-1");
assertThat(result.palletCode()).isEqualTo("DP-260909-0001");
verify(workflow).pickWholePallet(org.mockito.ArgumentMatchers.eq("100"),
org.mockito.ArgumentMatchers.eq("PLAN-1"), org.mockito.ArgumentMatchers.eq(30L),
any(com.baiyue.lastleg.inboundplan.pda.dto.PdaRequests.WholePalletOperation.class));
@@ -940,6 +981,28 @@ class ReceivingContainerServiceBehaviorTest {
return value;
}
private InboundPlanPallet stubReservedFbaPallet() {
InboundPlanPallet value = planPallet();
value.setPalletNo("PAL-1"); value.setWarehousePalletId(30L); value.setWarehouseLocationId(40L);
value.setWarehouseResourceLockId(50L); value.setAssignmentStatus("RESERVED"); value.setVersion(0);
when(pallets.selectList(any())).thenReturn(List.of(value));
when(warehouse.resolveReservedPallet("PAL-1", "100", 100L, 30L, 40L, 50L))
.thenReturn(new WarehouseResourceGateway.WarehousePalletReference(
30L, "PAL-1", 100L, 40L, "OCCUPIED", "FBA-1", 0));
return value;
}
private void stubCompleteManifest(String sourcePalletNo, int packages) {
DropshipOrderProfile profile = new DropshipOrderProfile();
profile.setId(60L); profile.setCustomerId("100"); profile.setSourcePalletNo(sourcePalletNo);
profile.setManifestVersion(1); profile.setManifestPackageCount(packages);
profile.setManifestFragmentCount(1); profile.setManifestHash("manifest-hash");
when(dropshipProfiles.listContainerPallet("CONT-A", sourcePalletNo)).thenReturn(List.of(profile));
List<DropshipCarton> values = new java.util.ArrayList<>();
for (int index = 0; index < packages; index++) values.add(new DropshipCarton());
when(dropshipCartons.listProfile("100", 60L)).thenReturn(values);
}
private static WarehouseResourceGateway.WarehousePalletReference resource(long warehouseId, String fbaCode) {
return new WarehouseResourceGateway.WarehousePalletReference(30L, "PAL-1", warehouseId, 40L,
"AVAILABLE", fbaCode, 0);
@@ -304,7 +304,7 @@ class InventoryCommandServiceTest {
position.setPickedPackages(5L);
when(commands.lockPosition("100", 201L)).thenReturn(position);
when(commands.lockBalance("100", 200L)).thenReturn(balance);
when(commands.palletPackages("100", 300L)).thenReturn(0L);
when(commands.palletPackages(300L)).thenReturn(0L);
var result = service.ship("100", "7", new InventoryCommandRequests.Move(
201L, 5, "OUT-1", "OUT-1", null, "ship-1", null));
@@ -32,7 +32,7 @@ class Phase5OverseasUpgradeSqlContractTest {
assertThat(overseas.indexOf(table)).as(table + " first reference")
.isEqualTo(overseas.indexOf(create) + "CREATE TABLE IF NOT EXISTS ".length());
}
assertThat(overseas).contains("<> 24", "五期完整业务表集合未部署");
assertThat(overseas).contains("<> 23", "五期完整业务表集合未部署");
}
@Test
@@ -26,7 +26,7 @@ class LoadingCompletedConsumerTest {
assertTrue(table.contains("customer_id")); assertFalse(table.contains("tenant_id"));
}
@Test void rejectsEventFromWrongRegion(){ EventEnvelope source=event(); EventEnvelope invalid=new EventEnvelope(source.eventId(),source.eventType(),source.eventVersion(),source.occurredAt(),RegionCode.OVERSEAS,RegionCode.CN,source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),source.payload(),source.payloadHash()); assertThrows(IllegalArgumentException.class,()->service.consume(invalid)); }
@Test void consumesFormalDropshipPalletSnapshots(){ var sku=new FirstLegLoadingPlanCompletedV1.SkuPackageSnapshot("SKU-A","Product A",2,20,List.of("BOX-1","BOX-2")); var pallet=new FirstLegLoadingPlanCompletedV1.PalletSnapshot("PLT-1",List.of(sku)); var item=new FirstLegLoadingPlanCompletedV1.Item("d-1","SKU-A","Product A","Product A",2,20,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,"wh-1","ONT8","ONT8",null,null,"MARK",null,List.of()); var order=new FirstLegLoadingPlanCompletedV1.Order("in-d","root-d","ord-d","FL-D","root-od","RK-D","wh-1","ONT8","ONT8",2,20,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,List.of(item),"100","Customer 100","ONE_CLICK_DROPSHIP",List.of(pallet),"FBA"); var payload=new FirstLegLoadingPlanCompletedV1("plan-d","LP-D","book-d","MSCU6639870","CAB-D","92","Warehouse",List.of(order)); JsonNode node=json.valueToTree(payload); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); var event=new EventEnvelope("123e4567-e89b-12d3-a456-426614174010",EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED,1,Instant.now(),RegionCode.CN,RegionCode.OVERSEAS,"100","LOADING_PLAN","plan-d",2,1L,"123e4567-e89b-12d3-a456-426614174011","FIRSTLEG_LOADING_COMPLETED:plan-d:100:1",null,node,hash); assertEquals(1,service.consume(event)); ArgumentCaptor<DropshipRequests.Receive> captor=ArgumentCaptor.forClass(DropshipRequests.Receive.class); verify(dropshipOrders).receive(captor.capture()); verify(inventory,never()).registerInTransit(anyString(),anyString(),any()); assertEquals("100",captor.getValue().customerId()); assertEquals("123e4567-e89b-12d3-a456-426614174010:DROPSHIP:in-d",captor.getValue().idempotencyKey()); assertEquals(1,captor.getValue().pallets().size()); assertEquals("PLT-1",captor.getValue().pallets().get(0).palletNo()); assertEquals(List.of("BOX-1","BOX-2"),captor.getValue().pallets().get(0).skuPackages().get(0).cartonMarks()); }
@Test void consumesFormalDropshipPalletSnapshots(){ var sku=new FirstLegLoadingPlanCompletedV1.SkuPackageSnapshot("SKU-A","Product A",2,20,List.of("BOX-1","BOX-2")); var pallet=new FirstLegLoadingPlanCompletedV1.PalletSnapshot("PLT-1",List.of(sku)); var item=new FirstLegLoadingPlanCompletedV1.Item("d-1","SKU-A","Product A","Product A",2,20,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,"wh-1","ONT8","ONT8",null,null,"MARK",null,List.of()); var order=new FirstLegLoadingPlanCompletedV1.Order("in-d","root-d","ord-d","FL-D","root-od","RK-D","wh-1","ONT8","ONT8",2,20,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,List.of(item),"100","Customer 100","ONE_CLICK_DROPSHIP",List.of(pallet),"FBA"); var payload=new FirstLegLoadingPlanCompletedV1("plan-d","LP-D","book-d","MSCU6639870","CAB-D","92","Warehouse",List.of(order)); JsonNode node=json.valueToTree(payload); addManifest(node.path("orders").get(0).path("pallets").get(0),2); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); var event=new EventEnvelope("123e4567-e89b-12d3-a456-426614174010",EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED,1,Instant.now(),RegionCode.CN,RegionCode.OVERSEAS,"100","LOADING_PLAN","plan-d",2,1L,"123e4567-e89b-12d3-a456-426614174011","FIRSTLEG_LOADING_COMPLETED:plan-d:100:1",null,node,hash); assertEquals(1,service.consume(event)); ArgumentCaptor<DropshipRequests.Receive> captor=ArgumentCaptor.forClass(DropshipRequests.Receive.class); verify(dropshipOrders).receive(captor.capture()); verify(inventory,never()).registerInTransit(anyString(),anyString(),any()); assertEquals("100",captor.getValue().customerId()); assertEquals("123e4567-e89b-12d3-a456-426614174010:DROPSHIP:in-d",captor.getValue().idempotencyKey()); assertEquals(1,captor.getValue().pallets().size()); assertEquals("PLT-1",captor.getValue().pallets().get(0).palletNo()); assertEquals(List.of("BOX-1","BOX-2"),captor.getValue().pallets().get(0).skuPackages().get(0).cartonMarks()); }
@Test void freezesReceivingWarehouseInInitialInTransitInventory(){
service.consume(event());
ArgumentCaptor<InventoryCommandRequests.InTransit> captured=ArgumentCaptor.forClass(InventoryCommandRequests.InTransit.class);
@@ -160,6 +160,7 @@ class LoadingCompletedConsumerTest {
secondSku.put("sourceInboundDetailId","d-2");
var marks=secondSku.putArray("cartonMarks"); for(int n=6;n<=10;n++)marks.add("RK1#10-"+n);
skus.add(secondSku);
addManifest(order.path("pallets").get(0),10);
service.consume(rehash(original,node));
var captured=ArgumentCaptor.forClass(com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton.class);
verify(cartons,times(10)).insert(captured.capture());
@@ -193,7 +194,7 @@ class LoadingCompletedConsumerTest {
sourceOrder.put("orderType","ONE_CLICK_DROPSHIP").put("destinationType","OVERSEAS");
var sku=json.createObjectNode().put("sourceInboundDetailId","d-1").put("skuCode","SKU-1").put("productName","产品").put("packageCount",5).put("productQuantity",50);
var marks=sku.putArray("cartonMarks"); for(int n=1;n<=5;n++)marks.add("RK1#5-"+n);
var pallet=json.createObjectNode().put("palletNo","DP-260909-0143"); pallet.putArray("skuPackages").add(sku);
var pallet=json.createObjectNode().put("palletNo","DP-260909-0143"); pallet.putArray("skuPackages").add(sku); addManifest(pallet,5);
sourceOrder.putArray("pallets").add(pallet);
if(duplicateSku) ((com.fasterxml.jackson.databind.node.ArrayNode)sourceOrder.path("items")).add(sourceOrder.path("items").get(0).deepCopy());
node.putArray("orders").add(sourceOrder);
@@ -202,5 +203,6 @@ class LoadingCompletedConsumerTest {
}
private EventEnvelope event(){ var item1=new FirstLegLoadingPlanCompletedV1.Item("d-1","SKU-1","产品","Product",5,50,1,new BigDecimal("10"),new BigDecimal("9"),new BigDecimal("12"),new BigDecimal("1.2"),"wh-1","ONT8","FBA ONT8","FBA-1","SHIP-1","MARK-1"); var item2=new FirstLegLoadingPlanCompletedV1.Item("d-2","SKU-2","产品2","Product2",3,30,2,new BigDecimal("8"),new BigDecimal("7"),new BigDecimal("9"),new BigDecimal("0.8"),"wh-2","LAX9","FBA LAX9","FBA-2","SHIP-2","MARK-2"); var p=new FirstLegLoadingPlanCompletedV1("plan-1","LP1","book-1","MSCU6639870","CAB1","ow-1","洛杉矶海外仓",List.of(new FirstLegLoadingPlanCompletedV1.Order("in-1","root-1","ord-1","FL1","root-o1","RK1","wh-1","ONT8","FBA ONT8",5,50,1,new BigDecimal("10"),new BigDecimal("9"),new BigDecimal("1.2"),List.of(item1),"100","Customer 100","FIRST_LEG",List.of()),new FirstLegLoadingPlanCompletedV1.Order("in-2","root-2","ord-2","FL2","root-o2","RK2","wh-2","LAX9","FBA LAX9",3,30,2,new BigDecimal("8"),new BigDecimal("7"),new BigDecimal("0.8"),List.of(item2),"100","Customer 100","FIRST_LEG",List.of()))); JsonNode node=json.valueToTree(p); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); return new EventEnvelope("123e4567-e89b-12d3-a456-426614174000",EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED,1,Instant.now(),RegionCode.CN,RegionCode.OVERSEAS,"100","LOADING_PLAN","plan-1",2,1L,"123e4567-e89b-12d3-a456-426614174001","FIRSTLEG_LOADING_COMPLETED:plan-1:100:1",null,node,hash); }
private JsonNode v2Payload(EventEnvelope source){ com.fasterxml.jackson.databind.node.ObjectNode node=source.payload().deepCopy(); node.set("sourceContainerBookingId",node.remove("containerBookingId")); return node; }
private EventEnvelope eventForPayload(FirstLegLoadingPlanCompletedV1 payload, long version){ JsonNode node=json.valueToTree(payload); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); return new EventEnvelope("123e4567-e89b-12d3-a456-426614174099",EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED,1,Instant.now(),RegionCode.CN,RegionCode.OVERSEAS,"100","LOADING_PLAN",payload.loadingPlanId(),2,version,"123e4567-e89b-12d3-a456-426614174098","FIRSTLEG_LOADING_COMPLETED:"+payload.loadingPlanId()+":100:1",null,node,hash); }
private EventEnvelope eventForPayload(FirstLegLoadingPlanCompletedV1 payload, long version){ JsonNode node=json.valueToTree(payload); for(JsonNode order:node.path("orders")) for(JsonNode pallet:order.path("pallets")) addManifest(pallet,order.path("packageCount").asInt()); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); return new EventEnvelope("123e4567-e89b-12d3-a456-426614174099",EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED,1,Instant.now(),RegionCode.CN,RegionCode.OVERSEAS,"100","LOADING_PLAN",payload.loadingPlanId(),2,version,"123e4567-e89b-12d3-a456-426614174098","FIRSTLEG_LOADING_COMPLETED:"+payload.loadingPlanId()+":100:1",null,node,hash); }
private void addManifest(JsonNode pallet,int packageCount){ var object=(com.fasterxml.jackson.databind.node.ObjectNode)pallet; object.put("manifestVersion",1).put("manifestPackageCount",packageCount).put("manifestFragmentCount",1).put("manifestHash","a".repeat(64)); }
}