feat(warehouse-overseas): reserve inbound pallet batches
This commit is contained in:
+5
-1
@@ -29,7 +29,7 @@ class SchemaChineseCommentContractTest {
|
||||
Path.of("..", "scripts", "db", "incremental.sql"), StandardCharsets.UTF_8);
|
||||
|
||||
List<TableDefinition> tables = parseAndAssert(baseline, "仓储基线 SQL");
|
||||
assertEquals(5, tables.size(), "海外仓资源表数量变化时必须同步表索引和备注迁移");
|
||||
assertEquals(7, tables.size(), "海外仓资源表数量变化时必须同步表索引和备注迁移");
|
||||
assertFalse(baseline.contains("CREATE TABLE IF NOT EXISTS wh_warehouse ("));
|
||||
assertFalse(baseline.contains("CREATE TABLE IF NOT EXISTS wh_warehouse_address ("));
|
||||
assertFalse(baseline.contains("CREATE TABLE IF NOT EXISTS wh_service_capability ("));
|
||||
@@ -39,6 +39,10 @@ class SchemaChineseCommentContractTest {
|
||||
assertTrue(marker >= 0, "统一增量 SQL 缺少 025 中文备注迁移");
|
||||
String migration = incremental.substring(marker);
|
||||
for (TableDefinition table : tables) {
|
||||
if (Pattern.compile("(?is)CREATE\\s+TABLE\\s+IF\\s+NOT\\s+EXISTS\\s+" + table.name() + "\\b")
|
||||
.matcher(migration).find()) {
|
||||
continue;
|
||||
}
|
||||
assertTrue(migration.contains("table_name='" + table.name() + "'"),
|
||||
"025 备注迁移缺少表: " + table.name());
|
||||
for (String column : table.columns()) {
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.baiyue.warehouse.schema;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class WarehousePalletBatchContractTest {
|
||||
|
||||
@Test
|
||||
void baselineAndUnifiedIncrementalContainBatchTablesAndCommandHashes() throws Exception {
|
||||
String baseline = read(Path.of("src", "main", "resources", "db", "warehouse.sql"));
|
||||
String incremental = read(Path.of("..", "scripts", "db", "incremental.sql"));
|
||||
String unified = read(Path.of("..", "deploy", "sql", "海外区域统一增量-MySQL8.sql"));
|
||||
for (String sql : new String[] {baseline, incremental, unified}) {
|
||||
assertThat(sql).contains("CREATE TABLE IF NOT EXISTS wh_pallet_batch (")
|
||||
.contains("CREATE TABLE IF NOT EXISTS wh_pallet_batch_item (")
|
||||
.contains("confirm_command_hash CHAR(64)")
|
||||
.contains("compensate_command_hash CHAR(64)")
|
||||
.contains("business_key VARCHAR(100)")
|
||||
.contains("pallet_code VARCHAR(64)");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void internalOpenApiFreezesEnvelopeAndLengthContract() throws Exception {
|
||||
String yaml = read(Path.of("src", "main", "resources", "openapi",
|
||||
"warehouse-pallet-batch-internal-v1.yaml"));
|
||||
assertThat(yaml).contains("/internal/v1/warehouse-pallet-batches/ensure-reserved:")
|
||||
.contains("/internal/v1/warehouse-pallet-batches/confirm:")
|
||||
.contains("/internal/v1/warehouse-pallet-batches/compensate:")
|
||||
.contains("业务冲突仍为HTTP 200且响应体code=A0409")
|
||||
.contains("businessKey: { type: string, minLength: 1, maxLength: 100 }")
|
||||
.contains("palletCode: { type: string, maxLength: 64 }")
|
||||
.doesNotContain("'409':");
|
||||
}
|
||||
|
||||
private static String read(Path path) throws Exception {
|
||||
return Files.readString(path, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
+381
@@ -0,0 +1,381 @@
|
||||
package com.baiyue.warehouse.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.warehouse.dto.PalletRequests;
|
||||
import com.baiyue.warehouse.dto.WarehousePalletBatchRequests;
|
||||
import com.baiyue.warehouse.dto.WarehouseResourceLockRequests;
|
||||
import com.baiyue.warehouse.dto.WarehouseResourceLockResponse;
|
||||
import com.baiyue.warehouse.entity.WarehousePalletBatchEntity;
|
||||
import com.baiyue.warehouse.entity.WarehousePalletBatchItemEntity;
|
||||
import com.baiyue.warehouse.entity.WarehousePalletBindingEntity;
|
||||
import com.baiyue.warehouse.entity.WarehousePalletEntity;
|
||||
import com.baiyue.warehouse.entity.WarehouseResourceLockEntity;
|
||||
import com.baiyue.warehouse.mapper.WarehousePalletBatchItemMapper;
|
||||
import com.baiyue.warehouse.mapper.WarehousePalletBatchMapper;
|
||||
import com.baiyue.warehouse.repository.WarehouseResourceLockRepository;
|
||||
import com.baiyue.warehouse.repository.WarehouseResourceRepository;
|
||||
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 java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class WarehousePalletBatchServiceTest {
|
||||
private static final long WAREHOUSE = 201L;
|
||||
private static final long CUSTOMER_A = 101L;
|
||||
private static final long CUSTOMER_B = 102L;
|
||||
|
||||
@Mock WarehousePalletBatchMapper batches;
|
||||
@Mock WarehousePalletBatchItemMapper items;
|
||||
@Mock WarehouseResourceService resources;
|
||||
@Mock WarehouseResourceLockService locks;
|
||||
@Mock WarehouseResourceLockRepository lockRecords;
|
||||
@Mock WarehouseResourceRepository repository;
|
||||
private WarehousePalletBatchService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new WarehousePalletBatchService();
|
||||
ReflectionTestUtils.setField(service, "batches", batches);
|
||||
ReflectionTestUtils.setField(service, "items", items);
|
||||
ReflectionTestUtils.setField(service, "resources", resources);
|
||||
ReflectionTestUtils.setField(service, "locks", locks);
|
||||
ReflectionTestUtils.setField(service, "lockRecords", lockRecords);
|
||||
ReflectionTestUtils.setField(service, "repository", repository);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
|
||||
}
|
||||
|
||||
@Test
|
||||
void reservesPublicPalletsButIsolatesEachCrossCustomerLock() {
|
||||
when(batches.insertIgnore(any())).thenReturn(1);
|
||||
stubNewResources();
|
||||
|
||||
var response = service.ensureReserved(request());
|
||||
|
||||
assertThat(response.status()).isEqualTo("RESERVED");
|
||||
assertThat(response.items()).extracting(item -> item.customerId()).containsExactly(CUSTOMER_A, CUSTOMER_B);
|
||||
ArgumentCaptor<Long> scope = ArgumentCaptor.forClass(Long.class);
|
||||
verify(resources, times(2)).createPallet(scope.capture(), anyLong(), any(PalletRequests.SaveRequest.class));
|
||||
assertThat(scope.getAllValues()).containsOnly(WarehouseResourceService.PUBLIC_RESOURCE_SCOPE);
|
||||
ArgumentCaptor<WarehouseResourceLockRequests.ReserveRequest> reservations =
|
||||
ArgumentCaptor.forClass(WarehouseResourceLockRequests.ReserveRequest.class);
|
||||
verify(locks, times(2)).reserve(reservations.capture());
|
||||
assertThat(reservations.getAllValues()).extracting(value -> value.customerId())
|
||||
.containsExactly(CUSTOMER_A, CUSTOMER_B);
|
||||
assertThat(reservations.getAllValues()).extracting(value -> value.businessId())
|
||||
.containsExactly("container:plan-a:1", "container:plan-b:2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void concurrentSameRequestReplaysWinningBatchWithoutCreatingResources() {
|
||||
AtomicReference<WarehousePalletBatchEntity> inserted = new AtomicReference<>();
|
||||
when(batches.lockByKey(WAREHOUSE, "container-1")).thenAnswer(call -> inserted.get());
|
||||
when(batches.insertIgnore(any())).thenAnswer(call -> {
|
||||
WarehousePalletBatchEntity losingInsert = call.getArgument(0);
|
||||
WarehousePalletBatchEntity winner = batch(losingInsert.getId(), "RESERVED", 1);
|
||||
winner.setRequestHash(losingInsert.getRequestHash());
|
||||
inserted.set(winner);
|
||||
return 0;
|
||||
});
|
||||
when(items.lockByBatch(anyLong())).thenReturn(List.of(item(1L, CUSTOMER_A, 301L, 401L, 501L, 1, "RESERVED")));
|
||||
|
||||
var response = service.ensureReserved(request());
|
||||
|
||||
assertThat(response.replayed()).isTrue();
|
||||
assertThat(response.batchId()).isEqualTo(inserted.get().getId());
|
||||
verify(resources, never()).createPallet(anyLong(), anyLong(), any());
|
||||
verify(locks, never()).reserve(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameBatchKeyWithDifferentPayloadIsRejectedAsA0409() {
|
||||
WarehousePalletBatchEntity existing = batch(11L, "RESERVED", 1);
|
||||
existing.setRequestHash("different");
|
||||
when(batches.lockByKey(WAREHOUSE, "container-1")).thenReturn(existing);
|
||||
|
||||
assertThatThrownBy(() -> service.ensureReserved(request()))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("different pallet request");
|
||||
verify(resources, never()).createPallet(anyLong(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void existingBusinessBindingIsRejectedBeforeAnyPalletIsCreated() {
|
||||
when(batches.insertIgnore(any())).thenReturn(1);
|
||||
when(repository.lockPalletBinding(CUSTOMER_A, WAREHOUSE, "INBOUND_PLAN_AUTO", "container:plan-a:1"))
|
||||
.thenReturn(new WarehousePalletBindingEntity());
|
||||
|
||||
assertThatThrownBy(() -> service.ensureReserved(request()))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("already bound");
|
||||
verify(resources, never()).createPallet(anyLong(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmValidatesAndBindsEveryItemInItsOwnCustomerScope() {
|
||||
WarehousePalletBatchEntity batch = batch(11L, "RESERVED", 1);
|
||||
List<WarehousePalletBatchItemEntity> batchItems = List.of(
|
||||
item(21L, CUSTOMER_A, 301L, 401L, 501L, 1, "RESERVED"),
|
||||
item(22L, CUSTOMER_B, 302L, 402L, 502L, 2, "RESERVED"));
|
||||
when(batches.lockByKey(WAREHOUSE, "container-1")).thenReturn(batch);
|
||||
when(items.lockByBatch(11L)).thenReturn(batchItems);
|
||||
when(lockRecords.find(CUSTOMER_A, 501L)).thenReturn(lock(CUSTOMER_A, batchItems.get(0)));
|
||||
when(lockRecords.find(CUSTOMER_B, 502L)).thenReturn(lock(CUSTOMER_B, batchItems.get(1)));
|
||||
when(lockRecords.close(any(), org.mockito.ArgumentMatchers.eq("CONFIRMED"))).thenReturn(1);
|
||||
|
||||
var response = service.confirm(new WarehousePalletBatchRequests.Confirm(WAREHOUSE, "container-1", "confirm-1"));
|
||||
|
||||
assertThat(response.status()).isEqualTo("CONFIRMED");
|
||||
assertThat(response.items()).extracting(value -> value.status()).containsOnly("CONFIRMED");
|
||||
verify(repository).lockPalletBinding(CUSTOMER_A, WAREHOUSE, "INBOUND_PLAN_AUTO", "business-1");
|
||||
verify(repository).lockPalletBinding(CUSTOMER_B, WAREHOUSE, "INBOUND_PLAN_AUTO", "business-2");
|
||||
ArgumentCaptor<WarehousePalletBindingEntity> bindings = ArgumentCaptor.forClass(WarehousePalletBindingEntity.class);
|
||||
verify(repository, times(2)).insertPalletBinding(bindings.capture());
|
||||
assertThat(bindings.getAllValues()).extracting(WarehousePalletBindingEntity::getCustomerId)
|
||||
.containsExactly(CUSTOMER_A, CUSTOMER_B);
|
||||
}
|
||||
|
||||
@Test
|
||||
void repeatedConfirmIsIdempotentAndDoesNotBindAgain() {
|
||||
WarehousePalletBatchEntity batch = batch(11L, "RESERVED", 1);
|
||||
WarehousePalletBatchItemEntity item = item(21L, CUSTOMER_A, 301L, 401L, 501L, 1, "RESERVED");
|
||||
when(batches.lockByKey(WAREHOUSE, "container-1")).thenReturn(batch);
|
||||
when(items.lockByBatch(11L)).thenReturn(List.of(item));
|
||||
when(lockRecords.find(CUSTOMER_A, 501L)).thenReturn(lock(CUSTOMER_A, item));
|
||||
when(lockRecords.close(any(), org.mockito.ArgumentMatchers.eq("CONFIRMED"))).thenReturn(1);
|
||||
var command = new WarehousePalletBatchRequests.Confirm(WAREHOUSE, "container-1", "confirm-1");
|
||||
|
||||
service.confirm(command);
|
||||
var response = service.confirm(command);
|
||||
|
||||
assertThat(response.replayed()).isTrue();
|
||||
verify(lockRecords, times(1)).find(CUSTOMER_A, 501L);
|
||||
verify(repository, times(1)).insertPalletBinding(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void terminalConfirmRejectsDifferentCommandPayload() {
|
||||
WarehousePalletBatchEntity batch = batch(11L, "RESERVED", 1);
|
||||
WarehousePalletBatchItemEntity item = item(21L, CUSTOMER_A, 301L, 401L, 501L, 1, "RESERVED");
|
||||
when(batches.lockByKey(WAREHOUSE, "container-1")).thenReturn(batch);
|
||||
when(items.lockByBatch(11L)).thenReturn(List.of(item));
|
||||
when(lockRecords.find(CUSTOMER_A, 501L)).thenReturn(lock(CUSTOMER_A, item));
|
||||
when(lockRecords.close(any(), org.mockito.ArgumentMatchers.eq("CONFIRMED"))).thenReturn(1);
|
||||
service.confirm(new WarehousePalletBatchRequests.Confirm(WAREHOUSE, "container-1", "confirm-1"));
|
||||
|
||||
assertThatThrownBy(() -> service.confirm(
|
||||
new WarehousePalletBatchRequests.Confirm(WAREHOUSE, "container-1", "confirm-2")))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("different payload");
|
||||
}
|
||||
|
||||
@Test
|
||||
void compensationReleasesOnlyThisBatchUsingEachCustomerScope() {
|
||||
WarehousePalletBatchEntity batch = batch(11L, "RESERVED", 1);
|
||||
List<WarehousePalletBatchItemEntity> batchItems = List.of(
|
||||
item(21L, CUSTOMER_A, 301L, 401L, 501L, 1, "RESERVED"),
|
||||
item(22L, CUSTOMER_B, 302L, 402L, 502L, 2, "RESERVED"));
|
||||
when(batches.lockByKey(WAREHOUSE, "container-1")).thenReturn(batch);
|
||||
when(items.lockByBatch(11L)).thenReturn(batchItems);
|
||||
|
||||
var response = service.compensate(
|
||||
new WarehousePalletBatchRequests.Compensate(WAREHOUSE, "container-1", "compensate-1", "lastleg failed"));
|
||||
|
||||
assertThat(response.status()).isEqualTo("COMPENSATED");
|
||||
verify(locks).release(CUSTOMER_A, 501L);
|
||||
verify(locks).release(CUSTOMER_B, 502L);
|
||||
assertThat(batchItems).extracting(WarehousePalletBatchItemEntity::getStatus).containsOnly("RELEASED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void compensationRejectsConfirmedOrAlreadyBoundResources() {
|
||||
WarehousePalletBatchEntity confirmed = batch(11L, "CONFIRMED", 1);
|
||||
when(batches.lockByKey(WAREHOUSE, "container-1")).thenReturn(confirmed);
|
||||
when(items.lockByBatch(11L)).thenReturn(List.of());
|
||||
var command = new WarehousePalletBatchRequests.Compensate(
|
||||
WAREHOUSE, "container-1", "compensate-1", "lastleg failed");
|
||||
|
||||
assertThatThrownBy(() -> service.compensate(command)).isInstanceOf(BizException.class);
|
||||
verify(locks, never()).release(anyLong(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void compensationRejectsBindingToThisBatchPallet() {
|
||||
WarehousePalletBatchEntity batch = batch(11L, "RESERVED", 1);
|
||||
WarehousePalletBatchItemEntity item = item(21L, CUSTOMER_A, 301L, 401L, 501L, 1, "RESERVED");
|
||||
WarehousePalletBindingEntity binding = new WarehousePalletBindingEntity();
|
||||
binding.setPalletId(301L);
|
||||
when(batches.lockByKey(WAREHOUSE, "container-1")).thenReturn(batch);
|
||||
when(items.lockByBatch(11L)).thenReturn(List.of(item));
|
||||
when(repository.lockPalletBinding(CUSTOMER_A, WAREHOUSE, "INBOUND_PLAN_AUTO", "business-1"))
|
||||
.thenReturn(binding);
|
||||
|
||||
assertThatThrownBy(() -> service.compensate(new WarehousePalletBatchRequests.Compensate(
|
||||
WAREHOUSE, "container-1", "compensate-1", "lastleg failed")))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("cannot be compensated");
|
||||
verify(locks, never()).release(anyLong(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void compensationAllowsUnrelatedOldBindingToDifferentPallet() {
|
||||
WarehousePalletBatchEntity batch = batch(11L, "RESERVED", 1);
|
||||
WarehousePalletBatchItemEntity item = item(21L, CUSTOMER_A, 301L, 401L, 501L, 1, "RESERVED");
|
||||
WarehousePalletBindingEntity oldBinding = new WarehousePalletBindingEntity();
|
||||
oldBinding.setPalletId(999L);
|
||||
when(batches.lockByKey(WAREHOUSE, "container-1")).thenReturn(batch);
|
||||
when(items.lockByBatch(11L)).thenReturn(List.of(item));
|
||||
when(repository.lockPalletBinding(CUSTOMER_A, WAREHOUSE, "INBOUND_PLAN_AUTO", "business-1"))
|
||||
.thenReturn(oldBinding);
|
||||
|
||||
service.compensate(new WarehousePalletBatchRequests.Compensate(
|
||||
WAREHOUSE, "container-1", "compensate-1", "lastleg failed"));
|
||||
|
||||
verify(locks).release(CUSTOMER_A, 501L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void terminalCompensationOnlyReplaysIdenticalCommandAndReason() {
|
||||
WarehousePalletBatchEntity batch = batch(11L, "RESERVED", 1);
|
||||
WarehousePalletBatchItemEntity item = item(21L, CUSTOMER_A, 301L, 401L, 501L, 1, "RESERVED");
|
||||
when(batches.lockByKey(WAREHOUSE, "container-1")).thenReturn(batch);
|
||||
when(items.lockByBatch(11L)).thenReturn(List.of(item));
|
||||
var command = new WarehousePalletBatchRequests.Compensate(
|
||||
WAREHOUSE, "container-1", "compensate-1", "lastleg failed");
|
||||
|
||||
service.compensate(command);
|
||||
assertThat(service.compensate(command).replayed()).isTrue();
|
||||
assertThatThrownBy(() -> service.compensate(new WarehousePalletBatchRequests.Compensate(
|
||||
WAREHOUSE, "container-1", "compensate-1", "different reason")))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("different payload");
|
||||
verify(locks, times(1)).release(CUSTOMER_A, 501L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameRequestAfterCompensationReusesPalletAndCreatesNextAttemptLocks() {
|
||||
WarehousePalletBatchEntity batch = batch(11L, "COMPENSATED", 1);
|
||||
List<WarehousePalletBatchItemEntity> batchItems = List.of(
|
||||
item(21L, CUSTOMER_A, 301L, 401L, 501L, 1, "RELEASED"),
|
||||
item(22L, CUSTOMER_B, 302L, 402L, 502L, 2, "RELEASED"));
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
when(batches.lockByKey(WAREHOUSE, "container-1")).thenAnswer(call -> {
|
||||
if (batch.getRequestHash() == null) {
|
||||
return null;
|
||||
}
|
||||
return batch;
|
||||
});
|
||||
when(batches.insertIgnore(any())).thenAnswer(call -> {
|
||||
WarehousePalletBatchEntity candidate = call.getArgument(0);
|
||||
batch.setRequestHash(candidate.getRequestHash());
|
||||
return 0;
|
||||
});
|
||||
when(items.lockByBatch(11L)).thenReturn(batchItems);
|
||||
when(resources.pallet(0L, WAREHOUSE, 301L)).thenReturn(pallet(301L, 401L, "PAL-301"));
|
||||
when(resources.pallet(0L, WAREHOUSE, 302L)).thenReturn(pallet(302L, 402L, "PAL-302"));
|
||||
when(locks.reserve(any())).thenAnswer(call -> lockResponse(
|
||||
((WarehouseResourceLockRequests.ReserveRequest) call.getArgument(0)).customerId(),
|
||||
601L + calls.getAndIncrement()));
|
||||
|
||||
var response = service.ensureReserved(request());
|
||||
|
||||
assertThat(response.attemptNo()).isEqualTo(2);
|
||||
assertThat(response.items()).extracting(value -> value.palletId()).containsExactly(301L, 302L);
|
||||
verify(resources, never()).createPallet(anyLong(), anyLong(), any());
|
||||
ArgumentCaptor<WarehouseResourceLockRequests.ReserveRequest> reservations =
|
||||
ArgumentCaptor.forClass(WarehouseResourceLockRequests.ReserveRequest.class);
|
||||
verify(locks, times(2)).reserve(reservations.capture());
|
||||
assertThat(reservations.getAllValues()).extracting(value -> value.reservationKey())
|
||||
.allMatch(key -> key.contains(":2:"));
|
||||
}
|
||||
|
||||
private void stubNewResources() {
|
||||
AtomicInteger created = new AtomicInteger();
|
||||
when(resources.createPallet(anyLong(), anyLong(), any())).thenAnswer(call -> 301L + created.getAndIncrement());
|
||||
when(resources.pallet(0L, WAREHOUSE, 301L)).thenReturn(pallet(301L, 401L, "PAL-301"));
|
||||
when(resources.pallet(0L, WAREHOUSE, 302L)).thenReturn(pallet(302L, 402L, "PAL-302"));
|
||||
when(locks.reserve(any())).thenAnswer(call -> {
|
||||
WarehouseResourceLockRequests.ReserveRequest value = call.getArgument(0);
|
||||
return lockResponse(value.customerId(), 500L + value.palletId() - 300L);
|
||||
});
|
||||
}
|
||||
|
||||
private static WarehousePalletBatchRequests.EnsureReserved request() {
|
||||
return new WarehousePalletBatchRequests.EnsureReserved(WAREHOUSE, "container-1", List.of(
|
||||
new WarehousePalletBatchRequests.Item(CUSTOMER_B, "container:plan-b:2", "PLAN-B", 2, "STANDARD", "ONT8"),
|
||||
new WarehousePalletBatchRequests.Item(CUSTOMER_A, "container:plan-a:1", "PLAN-A", 1, "STANDARD", "LAX9")));
|
||||
}
|
||||
|
||||
private static WarehousePalletBatchEntity batch(long id, String status, int attempt) {
|
||||
WarehousePalletBatchEntity entity = new WarehousePalletBatchEntity();
|
||||
entity.setId(id);
|
||||
entity.setWarehouseId(WAREHOUSE);
|
||||
entity.setBatchKey("container-1");
|
||||
entity.setStatus(status);
|
||||
entity.setAttemptNo(attempt);
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static WarehousePalletBatchItemEntity item(long id, long customerId, long palletId,
|
||||
long locationId, long lockId, int sequence, String status) {
|
||||
WarehousePalletBatchItemEntity entity = new WarehousePalletBatchItemEntity();
|
||||
entity.setId(id);
|
||||
entity.setBatchId(11L);
|
||||
entity.setCustomerId(customerId);
|
||||
entity.setWarehouseId(WAREHOUSE);
|
||||
entity.setBusinessKey("business-" + sequence);
|
||||
entity.setInboundPlanNo("PLAN-" + sequence);
|
||||
entity.setSequenceNo(sequence);
|
||||
entity.setPalletId(palletId);
|
||||
entity.setPalletCode("PAL-" + palletId);
|
||||
entity.setLocationId(locationId);
|
||||
entity.setResourceLockId(lockId);
|
||||
entity.setStatus(status);
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static WarehousePalletEntity pallet(long id, long locationId, String code) {
|
||||
WarehousePalletEntity entity = new WarehousePalletEntity();
|
||||
entity.setId(id);
|
||||
entity.setWarehouseId(WAREHOUSE);
|
||||
entity.setLocationId(locationId);
|
||||
entity.setPalletCode(code);
|
||||
entity.setStatus("OCCUPIED");
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static WarehouseResourceLockEntity lock(long customerId, WarehousePalletBatchItemEntity item) {
|
||||
WarehouseResourceLockEntity entity = new WarehouseResourceLockEntity();
|
||||
entity.setId(item.getResourceLockId());
|
||||
entity.setCustomerId(customerId);
|
||||
entity.setWarehouseId(WAREHOUSE);
|
||||
entity.setPalletId(item.getPalletId());
|
||||
entity.setLocationId(item.getLocationId());
|
||||
entity.setBusinessType("INBOUND_PLAN_AUTO");
|
||||
entity.setBusinessId(item.getBusinessKey());
|
||||
entity.setStatus("ACTIVE");
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static WarehouseResourceLockResponse lockResponse(long customerId, long lockId) {
|
||||
return new WarehouseResourceLockResponse(lockId, customerId, WAREHOUSE, 0L, 0L,
|
||||
"INBOUND_PLAN_AUTO", "business", "reservation", "ACTIVE", null, null);
|
||||
}
|
||||
}
|
||||
+19
@@ -251,6 +251,25 @@ class WarehouseResourceServiceTest {
|
||||
assertThat(captor.getValue().getId()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatedPalletCodeRemainsWithinLastlegSnapshotLimitForLongWarehouseCode() {
|
||||
String longCode = "US-LONG-WAREHOUSE-CODE-THAT-EXCEEDS-THIRTY-CHARACTERS";
|
||||
when(warehouseDirectory.required(WAREHOUSE))
|
||||
.thenReturn(new SharedWarehouseReference(WAREHOUSE, longCode, "Long warehouse", "ACTIVE"));
|
||||
WarehouseLocationEntity location = new WarehouseLocationEntity();
|
||||
location.setId(30009L);
|
||||
location.setLocationType("STORAGE");
|
||||
location.setStatus("IDLE");
|
||||
when(repository.findLocation(TENANT, WAREHOUSE, 30009L)).thenReturn(location);
|
||||
|
||||
service.createPallet(TENANT, WAREHOUSE,
|
||||
new PalletRequests.SaveRequest(null, "STANDARD", null, BigDecimal.ONE, "IDLE", 30009L, null));
|
||||
|
||||
ArgumentCaptor<WarehousePalletEntity> captor = ArgumentCaptor.forClass(WarehousePalletEntity.class);
|
||||
verify(repository).insertPallet(captor.capture());
|
||||
assertThat(captor.getValue().getPalletCode()).hasSizeLessThanOrEqualTo(64);
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultVirtualLocationIsInsertedIdempotentlyForAnExistingWarehouse() {
|
||||
WarehouseLocationEntity resolved = new WarehouseLocationEntity();
|
||||
|
||||
Reference in New Issue
Block a user