Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test
This commit is contained in:
@@ -109,6 +109,16 @@ public interface LocationMapper extends BaseMapper<LocationEntity> {
|
||||
@Param("warehouseId") long warehouseId,
|
||||
@Param("limit") int limit);
|
||||
|
||||
@Select("""
|
||||
SELECT * FROM wh_location
|
||||
WHERE id = #{id} AND tenant_id = #{tenantId} AND warehouse_id = #{warehouseId}
|
||||
AND deleted = 0
|
||||
FOR UPDATE
|
||||
""")
|
||||
LocationEntity lockOwned(@Param("tenantId") long tenantId,
|
||||
@Param("warehouseId") long warehouseId,
|
||||
@Param("id") long id);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO wh_location (
|
||||
id, tenant_id, warehouse_id, zone_code, zone_name, location_code, location_name,
|
||||
|
||||
@@ -61,6 +61,16 @@ public interface LocationReservationMapper extends BaseMapper<LocationReservatio
|
||||
int release(@Param("tenantId") long tenantId, @Param("id") long id,
|
||||
@Param("version") int version, @Param("reason") String reason);
|
||||
|
||||
@Update("""
|
||||
UPDATE wh_location_reservation
|
||||
SET reservation_status = 'RELEASED', released_at = CURRENT_TIMESTAMP(3),
|
||||
release_reason = #{reason}, updated_at = CURRENT_TIMESTAMP(3), version = version + 1
|
||||
WHERE id = #{id} AND tenant_id = #{tenantId}
|
||||
AND reservation_status = 'CONFIRMED' AND version = #{version}
|
||||
""")
|
||||
int releaseConfirmed(@Param("tenantId") long tenantId, @Param("id") long id,
|
||||
@Param("version") int version, @Param("reason") String reason);
|
||||
|
||||
@Update("""
|
||||
UPDATE wh_location_reservation
|
||||
SET business_id = #{targetBusinessId}, inbound_code = #{targetInboundCode},
|
||||
|
||||
@@ -44,6 +44,10 @@ public class LocationRepository {
|
||||
.eq(LocationEntity::getId, id));
|
||||
}
|
||||
|
||||
public LocationEntity lockOwned(long tenantId, long warehouseId, long id) {
|
||||
return locations.lockOwned(tenantId, warehouseId, id);
|
||||
}
|
||||
|
||||
public PageResult<LocationEntity> page(Long tenantId, PageQuery query) {
|
||||
Page<LocationEntity> result = locations.selectLocationPage(new Page<>(query.page(), query.size()),
|
||||
tenantId, query.warehouseId(), query.zoneCode(), query.locationCode(), query.locationType(), query.status());
|
||||
@@ -94,6 +98,10 @@ public class LocationRepository {
|
||||
|| occupancies.countActiveByLocation(tenantId, locationId) > 0;
|
||||
}
|
||||
|
||||
public int countActiveOccupancies(long tenantId, long locationId) {
|
||||
return occupancies.countActiveByLocation(tenantId, locationId);
|
||||
}
|
||||
|
||||
public List<LocationReservationEntity> findReservationsByKeys(long tenantId, String sourceService,
|
||||
Collection<String> keys) {
|
||||
if (keys.isEmpty()) {
|
||||
@@ -129,6 +137,10 @@ public class LocationRepository {
|
||||
return reservations.release(entity.getTenantId(), entity.getId(), entity.getVersion(), reason);
|
||||
}
|
||||
|
||||
public int releaseConfirmedReservation(LocationReservationEntity entity, String reason) {
|
||||
return reservations.releaseConfirmed(entity.getTenantId(), entity.getId(), entity.getVersion(), reason);
|
||||
}
|
||||
|
||||
public int transferConfirmedReservation(LocationReservationEntity entity, String sourceService,
|
||||
long sourceBusinessId, long targetBusinessId,
|
||||
String targetInboundCode) {
|
||||
|
||||
@@ -24,6 +24,8 @@ import com.baiyue.warehouse.entity.LocationReservationEntity;
|
||||
import com.baiyue.warehouse.entity.WarehouseEntity;
|
||||
import com.baiyue.warehouse.repository.LocationRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -42,6 +44,7 @@ import java.util.LinkedHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -51,6 +54,8 @@ import java.util.Set;
|
||||
@Service
|
||||
@Validated
|
||||
public class LocationResourceService {
|
||||
private static final Logger log = LoggerFactory.getLogger(LocationResourceService.class);
|
||||
|
||||
@Resource
|
||||
private LocationRepository repository;
|
||||
@Resource
|
||||
@@ -165,13 +170,8 @@ public class LocationResourceService {
|
||||
if (!"OCCUPIED".equals(occupancy.getOccupancyStatus())) {
|
||||
continue;
|
||||
}
|
||||
LocationEntity location = repository.findOwned(command.tenantId(), occupancy.getWarehouseId(), occupancy.getLocationId());
|
||||
if (location == null || repository.releaseOccupancy(occupancy, command.reason()) != 1
|
||||
|| (!Boolean.TRUE.equals(occupancy.getSharedOverflow())
|
||||
&& repository.changeStatus(command.tenantId(), occupancy.getWarehouseId(), occupancy.getLocationId(),
|
||||
"OCCUPIED", "AVAILABLE", location.getVersion(), command.operatorId()) != 1)) {
|
||||
throw new BizException(ResultCode.CONFLICT, "释放占用时库位状态已被并发修改");
|
||||
}
|
||||
releaseOccupiedResource(occupancy, command.reason(), command.operatorId(),
|
||||
"释放占用时库位状态已被并发修改");
|
||||
released.add(active(occupancy, "RELEASED", null));
|
||||
}
|
||||
for (LocationReservationEntity reservation : repository.findReservationsByBusiness(
|
||||
@@ -237,15 +237,8 @@ public class LocationResourceService {
|
||||
List<SelectiveReleaseResult> released = new ArrayList<>(selectedIndexes.size());
|
||||
for (int index : selectedIndexes) {
|
||||
LocationOccupancyEntity occupancy = active.get(index);
|
||||
LocationEntity location = repository.findOwned(command.tenantId(), occupancy.getWarehouseId(),
|
||||
occupancy.getLocationId());
|
||||
if (location == null || repository.releaseOccupancy(occupancy, command.reason()) != 1
|
||||
|| (!Boolean.TRUE.equals(occupancy.getSharedOverflow())
|
||||
&& repository.changeStatus(command.tenantId(), occupancy.getWarehouseId(), occupancy.getLocationId(),
|
||||
"OCCUPIED", "AVAILABLE", location.getVersion(), command.operatorId()) != 1)) {
|
||||
throw new BizException(ResultCode.CONFLICT,
|
||||
"Location occupancy changed concurrently while releasing selected locations");
|
||||
}
|
||||
releaseOccupiedResource(occupancy, command.reason(), command.operatorId(),
|
||||
"Location occupancy changed concurrently while releasing selected locations");
|
||||
released.add(new SelectiveReleaseResult(occupancy.getReservationId(), occupancy.getId(), index + 1,
|
||||
occupancy.getWarehouseId(), occupancy.getPalletId(), occupancy.getLocationId(),
|
||||
occupancy.getLocationCode(), packages.get(index)));
|
||||
@@ -289,15 +282,8 @@ public class LocationResourceService {
|
||||
throw new BizException(ResultCode.CONFLICT,
|
||||
"Occupied location has no valid allocated package count and cannot be released exactly");
|
||||
}
|
||||
LocationEntity location = repository.findOwned(command.tenantId(), occupancy.getWarehouseId(),
|
||||
occupancy.getLocationId());
|
||||
if (location == null || repository.releaseOccupancy(occupancy, command.reason()) != 1
|
||||
|| (!Boolean.TRUE.equals(occupancy.getSharedOverflow())
|
||||
&& repository.changeStatus(command.tenantId(), occupancy.getWarehouseId(), occupancy.getLocationId(),
|
||||
"OCCUPIED", "AVAILABLE", location.getVersion(), command.operatorId()) != 1)) {
|
||||
throw new BizException(ResultCode.CONFLICT,
|
||||
"Location occupancy changed concurrently while releasing selected locations");
|
||||
}
|
||||
releaseOccupiedResource(occupancy, command.reason(), command.operatorId(),
|
||||
"Location occupancy changed concurrently while releasing selected locations");
|
||||
released.add(new ExactSelectiveReleaseResult(occupancy.getReservationId(), occupancy.getId(), index + 1,
|
||||
occupancy.getWarehouseId(), occupancy.getPalletId(), occupancy.getLocationId(),
|
||||
occupancy.getLocationCode(), allocatedPackageCount));
|
||||
@@ -322,19 +308,94 @@ public class LocationResourceService {
|
||||
LocationOccupancyEntity occupancy = active.get(index);
|
||||
if (!requested.contains(occupancy.getReservationId())) continue;
|
||||
Integer allocated = occupancy.getAllocatedPackageCount();
|
||||
LocationEntity location = repository.findOwned(command.tenantId(), occupancy.getWarehouseId(), occupancy.getLocationId());
|
||||
if (allocated == null || allocated < 0 || location == null || repository.releaseOccupancy(occupancy, command.reason()) != 1
|
||||
|| (!Boolean.TRUE.equals(occupancy.getSharedOverflow()) && repository.changeStatus(command.tenantId(),
|
||||
occupancy.getWarehouseId(), occupancy.getLocationId(), "OCCUPIED", "AVAILABLE", location.getVersion(),
|
||||
command.operatorId()) != 1)) {
|
||||
if (allocated == null || allocated < 0) {
|
||||
throw new BizException(ResultCode.CONFLICT, "释放库位资源时发生并发变更");
|
||||
}
|
||||
releaseOccupiedResource(occupancy, command.reason(), command.operatorId(), "释放库位资源时发生并发变更");
|
||||
result.add(new ExactSelectiveReleaseResult(occupancy.getReservationId(), occupancy.getId(), index + 1,
|
||||
occupancy.getWarehouseId(), occupancy.getPalletId(), occupancy.getLocationId(), occupancy.getLocationCode(), allocated));
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Occupancy, confirmed reservation and the materialized location status form one resource fact.
|
||||
* Lock the location before changing either fact so the final availability decision is serialized.
|
||||
*/
|
||||
private void releaseOccupiedResource(LocationOccupancyEntity occupancy, String reason, long operatorId,
|
||||
String conflictMessage) {
|
||||
LocationEntity location = repository.lockOwned(occupancy.getTenantId(), occupancy.getWarehouseId(),
|
||||
occupancy.getLocationId());
|
||||
LocationReservationEntity reservation = repository.lockReservation(
|
||||
occupancy.getTenantId(), occupancy.getReservationId());
|
||||
if (!matchesConfirmedResource(occupancy, reservation) || location == null) {
|
||||
warnReleaseConflict(occupancy, location, reservation, 0, 0, -1, "RESOURCE_FACT_MISMATCH");
|
||||
throw new BizException(ResultCode.CONFLICT, conflictMessage);
|
||||
}
|
||||
|
||||
int occupancyAffected = repository.releaseOccupancy(occupancy, reason);
|
||||
if (occupancyAffected != 1) {
|
||||
warnReleaseConflict(occupancy, location, reservation, occupancyAffected, 0, -1,
|
||||
"OCCUPANCY_UPDATE_MISSED");
|
||||
throw new BizException(ResultCode.CONFLICT, conflictMessage);
|
||||
}
|
||||
int reservationAffected = repository.releaseConfirmedReservation(reservation, reason);
|
||||
if (reservationAffected != 1) {
|
||||
warnReleaseConflict(occupancy, location, reservation, occupancyAffected, reservationAffected, -1,
|
||||
"RESERVATION_UPDATE_MISSED");
|
||||
throw new BizException(ResultCode.CONFLICT, conflictMessage);
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(location.getSharedOverflow())) {
|
||||
return;
|
||||
}
|
||||
int remainingActive = repository.countActiveOccupancies(occupancy.getTenantId(), occupancy.getLocationId());
|
||||
if (remainingActive > 0) {
|
||||
warnReleaseConflict(occupancy, location, reservation, occupancyAffected, reservationAffected,
|
||||
remainingActive, "NON_SHARED_LOCATION_STILL_OCCUPIED");
|
||||
throw new BizException(ResultCode.CONFLICT, conflictMessage);
|
||||
}
|
||||
if ("AVAILABLE".equals(location.getStatus())) {
|
||||
log.warn("warehouse_location_release_converged reservationId={} occupancyId={} locationId={} "
|
||||
+ "expectedLocationStatus=OCCUPIED actualLocationStatus={} occupancyAffected={} "
|
||||
+ "reservationAffected={} remainingActiveOccupancies={}",
|
||||
occupancy.getReservationId(), occupancy.getId(), occupancy.getLocationId(), location.getStatus(),
|
||||
occupancyAffected, reservationAffected, remainingActive);
|
||||
return;
|
||||
}
|
||||
if (!"OCCUPIED".equals(location.getStatus())
|
||||
|| repository.changeStatus(occupancy.getTenantId(), occupancy.getWarehouseId(), occupancy.getLocationId(),
|
||||
"OCCUPIED", "AVAILABLE", location.getVersion(), operatorId) != 1) {
|
||||
warnReleaseConflict(occupancy, location, reservation, occupancyAffected, reservationAffected,
|
||||
remainingActive, "LOCATION_STATUS_UPDATE_MISSED");
|
||||
throw new BizException(ResultCode.CONFLICT, conflictMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean matchesConfirmedResource(LocationOccupancyEntity occupancy,
|
||||
LocationReservationEntity reservation) {
|
||||
return reservation != null
|
||||
&& "CONFIRMED".equals(reservation.getReservationStatus())
|
||||
&& Objects.equals(reservation.getWarehouseId(), occupancy.getWarehouseId())
|
||||
&& Objects.equals(reservation.getBusinessId(), occupancy.getBusinessId())
|
||||
&& Objects.equals(reservation.getLocationId(), occupancy.getLocationId())
|
||||
&& Objects.equals(reservation.getPalletId(), occupancy.getPalletId())
|
||||
&& Objects.equals(reservation.getSourceService(), occupancy.getSourceService());
|
||||
}
|
||||
|
||||
private static void warnReleaseConflict(LocationOccupancyEntity occupancy, LocationEntity location,
|
||||
LocationReservationEntity reservation, int occupancyAffected,
|
||||
int reservationAffected, int remainingActive, String cause) {
|
||||
log.warn("warehouse_location_release_conflict cause={} reservationId={} occupancyId={} locationId={} "
|
||||
+ "expectedReservationStatus=CONFIRMED actualReservationStatus={} "
|
||||
+ "expectedOccupancyStatus=OCCUPIED actualOccupancyStatus={} "
|
||||
+ "expectedLocationStatus=OCCUPIED actualLocationStatus={} occupancyAffected={} "
|
||||
+ "reservationAffected={} remainingActiveOccupancies={}",
|
||||
cause, occupancy.getReservationId(), occupancy.getId(), occupancy.getLocationId(),
|
||||
reservation == null ? null : reservation.getReservationStatus(), occupancy.getOccupancyStatus(),
|
||||
location == null ? null : location.getStatus(), occupancyAffected, reservationAffected, remainingActive);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将未拣货的已占用库位从主入库单转给二级子入库单。
|
||||
* 转移只改变业务归属和剩余箱数,不释放库位;托盘仅作为关联快照。
|
||||
|
||||
@@ -85,9 +85,7 @@ ON DUPLICATE KEY UPDATE
|
||||
location_type=VALUES(location_type),
|
||||
max_weight_kg=VALUES(max_weight_kg),
|
||||
shared_overflow=VALUES(shared_overflow),
|
||||
status='AVAILABLE',
|
||||
remark=VALUES(remark),
|
||||
deleted=0;
|
||||
remark=VALUES(remark);
|
||||
|
||||
INSERT INTO wh_location(
|
||||
id,tenant_id,warehouse_id,zone_code,zone_name,location_code,location_name,
|
||||
@@ -106,9 +104,7 @@ ON DUPLICATE KEY UPDATE
|
||||
location_type=VALUES(location_type),
|
||||
max_weight_kg=VALUES(max_weight_kg),
|
||||
shared_overflow=VALUES(shared_overflow),
|
||||
status='AVAILABLE',
|
||||
remark=VALUES(remark),
|
||||
deleted=0;
|
||||
remark=VALUES(remark);
|
||||
|
||||
DROP PROCEDURE IF EXISTS assert_yiwu_location_seed;
|
||||
DELIMITER //
|
||||
|
||||
+8
@@ -27,6 +27,9 @@ class InitialWarehouseResourceSeedSqlContractTest {
|
||||
}
|
||||
|
||||
private static void assertSeed(String sql, String yiwuId, String dongguanId, String idPrefix) {
|
||||
int locationSeedStart = sql.indexOf("WHEN zones.zone_index = 0 AND slots.slot_number = 1 THEN 201");
|
||||
int locationSeedEnd = sql.indexOf("DROP PROCEDURE IF EXISTS assert_yiwu_location_seed", locationSeedStart);
|
||||
String locationSeed = sql.substring(locationSeedStart, locationSeedEnd);
|
||||
assertThat(sql)
|
||||
.contains("0," + yiwuId + ",'TP-001','木托盘'")
|
||||
.contains("0," + dongguanId + ",'TP-002','塑料托盘'")
|
||||
@@ -52,5 +55,10 @@ class InitialWarehouseResourceSeedSqlContractTest {
|
||||
.contains("'AVAILABLE'")
|
||||
.doesNotContain("INSERT INTO wh_location_reservation")
|
||||
.doesNotContain("INSERT INTO wh_location_occupancy");
|
||||
assertThat(locationSeed)
|
||||
.doesNotContain("status='AVAILABLE'")
|
||||
.doesNotContain("status = 'AVAILABLE'")
|
||||
.doesNotContain("deleted=0")
|
||||
.doesNotContain("deleted = 0");
|
||||
}
|
||||
}
|
||||
|
||||
+99
-20
@@ -257,9 +257,7 @@ class LocationResourceServiceTest {
|
||||
item.setBusinessId(901L);
|
||||
});
|
||||
when(repository.lockOccupiedByBusiness(0L, "firstleg-service", 901L)).thenReturn(active);
|
||||
when(repository.findOwned(0L, 88L, 10L)).thenReturn(occupiedLocation(10L, "A2"));
|
||||
when(repository.releaseOccupancy(active.get(1), "PDA partial pick")).thenReturn(1);
|
||||
when(repository.changeStatus(0L, 88L, 10L, "OCCUPIED", "AVAILABLE", 0, 7L)).thenReturn(1);
|
||||
stubSuccessfulOccupiedRelease(active.get(1), occupiedLocation(10L, "A2"), "PDA partial pick", 7L);
|
||||
|
||||
var result = service.releaseSelected(new SelectiveReleaseCommand(0L, "firstleg-service", 901L, 7L,
|
||||
"PDA partial pick", 10, List.of(10L), null));
|
||||
@@ -280,9 +278,7 @@ class LocationResourceServiceTest {
|
||||
LocationOccupancyEntity second = occupancy(202L, 102L, 10L, "A2", 502L);
|
||||
second.setAllocatedPackageCount(7);
|
||||
when(repository.lockOccupiedByBusiness(10001L, "FIRSTLEG", 301L)).thenReturn(List.of(first, second));
|
||||
when(repository.findOwned(10001L, 88L, 10L)).thenReturn(occupiedLocation(10L, "A2"));
|
||||
when(repository.releaseOccupancy(second, "PDA full pick")).thenReturn(1);
|
||||
when(repository.changeStatus(10001L, 88L, 10L, "OCCUPIED", "AVAILABLE", 0, 7L)).thenReturn(1);
|
||||
stubSuccessfulOccupiedRelease(second, occupiedLocation(10L, "A2"), "PDA full pick", 7L);
|
||||
|
||||
var result = service.releaseSelectedExactly(new com.baiyue.warehouse.dto.LocationDtos.ExactSelectiveReleaseCommand(
|
||||
10001L, "FIRSTLEG", 301L, 7L, "PDA full pick", List.of(10L)));
|
||||
@@ -299,9 +295,7 @@ class LocationResourceServiceTest {
|
||||
LocationOccupancyEntity occupancy = occupancy(201L, 101L, 9L, "A1", 501L);
|
||||
occupancy.setAllocatedPackageCount(0);
|
||||
when(repository.lockOccupiedByBusiness(10001L, "FIRSTLEG", 301L)).thenReturn(List.of(occupancy));
|
||||
when(repository.findOwned(10001L, 88L, 9L)).thenReturn(occupiedLocation(9L, "A1"));
|
||||
when(repository.releaseOccupancy(occupancy, "PDA full pick")).thenReturn(1);
|
||||
when(repository.changeStatus(10001L, 88L, 9L, "OCCUPIED", "AVAILABLE", 0, 7L)).thenReturn(1);
|
||||
stubSuccessfulOccupiedRelease(occupancy, occupiedLocation(9L, "A1"), "PDA full pick", 7L);
|
||||
|
||||
var result = service.releaseSelectedExactly(new com.baiyue.warehouse.dto.LocationDtos.ExactSelectiveReleaseCommand(
|
||||
10001L, "FIRSTLEG", 301L, 7L, "PDA full pick", List.of(9L)));
|
||||
@@ -323,11 +317,8 @@ class LocationResourceServiceTest {
|
||||
});
|
||||
when(repository.lockOccupiedByBusiness(0L, "firstleg-service", 901L)).thenReturn(active);
|
||||
for (LocationOccupancyEntity item : List.of(allocationOne, allocationTwo)) {
|
||||
when(repository.findOwned(0L, 88L, item.getLocationId()))
|
||||
.thenReturn(occupiedLocation(item.getLocationId(), item.getLocationCode()));
|
||||
when(repository.releaseOccupancy(item, "PDA default pick")).thenReturn(1);
|
||||
when(repository.changeStatus(0L, 88L, item.getLocationId(), "OCCUPIED", "AVAILABLE", 0, 7L))
|
||||
.thenReturn(1);
|
||||
stubSuccessfulOccupiedRelease(item, occupiedLocation(item.getLocationId(), item.getLocationCode()),
|
||||
"PDA default pick", 7L);
|
||||
}
|
||||
|
||||
var result = service.releaseSelected(new SelectiveReleaseCommand(0L, "firstleg-service", 901L, 7L,
|
||||
@@ -348,11 +339,8 @@ class LocationResourceServiceTest {
|
||||
when(repository.lockOccupiedByBusiness(10001L, "FIRSTLEG", 301L)).thenReturn(active);
|
||||
for (int index = 0; index < 2; index++) {
|
||||
LocationOccupancyEntity item = active.get(index);
|
||||
when(repository.findOwned(10001L, 88L, item.getLocationId()))
|
||||
.thenReturn(occupiedLocation(item.getLocationId(), item.getLocationCode()));
|
||||
when(repository.releaseOccupancy(item, "PDA default pick")).thenReturn(1);
|
||||
when(repository.changeStatus(10001L, 88L, item.getLocationId(), "OCCUPIED", "AVAILABLE", 0, 7L))
|
||||
.thenReturn(1);
|
||||
stubSuccessfulOccupiedRelease(item, occupiedLocation(item.getLocationId(), item.getLocationCode()),
|
||||
"PDA default pick", 7L);
|
||||
}
|
||||
|
||||
var result = service.releaseSelected(new SelectiveReleaseCommand(10001L, "FIRSTLEG", 301L, 7L,
|
||||
@@ -379,7 +367,8 @@ class LocationResourceServiceTest {
|
||||
void shouldFailOnConcurrentOccupancyUpdate() {
|
||||
LocationOccupancyEntity active = occupancy(201L, 101L, 9L, "A1", 501L);
|
||||
when(repository.lockOccupiedByBusiness(10001L, "FIRSTLEG", 301L)).thenReturn(List.of(active));
|
||||
when(repository.findOwned(10001L, 88L, 9L)).thenReturn(occupiedLocation(9L, "A1"));
|
||||
when(repository.lockOwned(10001L, 88L, 9L)).thenReturn(occupiedLocation(9L, "A1"));
|
||||
when(repository.lockReservation(10001L, 101L)).thenReturn(confirmedReservationFor(active));
|
||||
when(repository.releaseOccupancy(active, "PDA pick")).thenReturn(0);
|
||||
|
||||
assertThatThrownBy(() -> service.releaseSelected(new SelectiveReleaseCommand(
|
||||
@@ -389,6 +378,64 @@ class LocationResourceServiceTest {
|
||||
any(String.class), any(String.class), anyInt(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldConvergeDriftedAvailableLocationWhenLastOccupancyIsReleasedByReservation() {
|
||||
LocationOccupancyEntity active = occupancy(201L, 101L, 9L, "A1", 501L);
|
||||
active.setAllocatedPackageCount(6);
|
||||
LocationEntity drifted = location(9L, "A1");
|
||||
drifted.setVersion(2);
|
||||
when(repository.lockOccupiedByBusiness(10001L, "FIRSTLEG", 301L)).thenReturn(List.of(active));
|
||||
stubSuccessfulOccupiedRelease(active, drifted, "PDA complete", 7L);
|
||||
|
||||
var result = service.releaseSelectedReservations(new com.baiyue.warehouse.dto.LocationDtos.ReservationReleaseCommand(
|
||||
10001L, "FIRSTLEG", 301L, 7L, "PDA complete", List.of(101L)));
|
||||
|
||||
assertThat(result).singleElement().satisfies(item -> {
|
||||
assertThat(item.reservationId()).isEqualTo(101L);
|
||||
assertThat(item.locationId()).isEqualTo(9L);
|
||||
});
|
||||
verify(repository).releaseConfirmedReservation(any(LocationReservationEntity.class), org.mockito.Mockito.eq("PDA complete"));
|
||||
verify(repository, never()).changeStatus(anyLong(), anyLong(), anyLong(),
|
||||
any(String.class), any(String.class), anyInt(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectAvailableNonSharedLocationWhenAnotherOccupancyRemains() {
|
||||
LocationOccupancyEntity active = occupancy(201L, 101L, 9L, "A1", 501L);
|
||||
when(repository.lockOccupiedByBusiness(10001L, "FIRSTLEG", 301L)).thenReturn(List.of(active));
|
||||
when(repository.lockOwned(10001L, 88L, 9L)).thenReturn(location(9L, "A1"));
|
||||
LocationReservationEntity reservation = confirmedReservationFor(active);
|
||||
when(repository.lockReservation(10001L, 101L)).thenReturn(reservation);
|
||||
when(repository.releaseOccupancy(active, "PDA complete")).thenReturn(1);
|
||||
when(repository.releaseConfirmedReservation(reservation, "PDA complete")).thenReturn(1);
|
||||
when(repository.countActiveOccupancies(10001L, 9L)).thenReturn(1);
|
||||
|
||||
assertThatThrownBy(() -> service.releaseSelectedReservations(
|
||||
new com.baiyue.warehouse.dto.LocationDtos.ReservationReleaseCommand(
|
||||
10001L, "FIRSTLEG", 301L, 7L, "PDA complete", List.of(101L))))
|
||||
.isInstanceOf(com.baiyue.common.core.exception.BizException.class);
|
||||
verify(repository, never()).changeStatus(anyLong(), anyLong(), anyLong(),
|
||||
any(String.class), any(String.class), anyInt(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReleaseOneSharedOverflowAllocationWithoutOpeningOrMergingTheOther() {
|
||||
LocationOccupancyEntity active = occupancy(201L, 101L, 99L, "A0", 501L);
|
||||
active.setSharedOverflow(true);
|
||||
active.setAllocatedPackageCount(4);
|
||||
LocationEntity shared = overflow();
|
||||
when(repository.lockOccupiedByBusiness(10001L, "FIRSTLEG", 301L)).thenReturn(List.of(active));
|
||||
stubSuccessfulOccupiedRelease(active, shared, "PDA complete", 7L);
|
||||
|
||||
assertThat(service.releaseSelectedReservations(
|
||||
new com.baiyue.warehouse.dto.LocationDtos.ReservationReleaseCommand(
|
||||
10001L, "FIRSTLEG", 301L, 7L, "PDA complete", List.of(101L))))
|
||||
.singleElement().extracting(item -> item.reservationId()).isEqualTo(101L);
|
||||
verify(repository, never()).countActiveOccupancies(anyLong(), anyLong());
|
||||
verify(repository, never()).changeStatus(anyLong(), anyLong(), anyLong(),
|
||||
any(String.class), any(String.class), anyInt(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldTransferConfirmedOccupiedResourcesToChildInbound() {
|
||||
LocationOccupancyEntity occupancy = occupancy(201L, 101L, 9L, "A1", 501L);
|
||||
@@ -514,4 +561,36 @@ class LocationResourceServiceTest {
|
||||
reservation.setVersion(0);
|
||||
return reservation;
|
||||
}
|
||||
|
||||
private void stubSuccessfulOccupiedRelease(LocationOccupancyEntity occupancy, LocationEntity location,
|
||||
String reason, long operatorId) {
|
||||
location.setTenantId(occupancy.getTenantId());
|
||||
LocationReservationEntity reservation = confirmedReservationFor(occupancy);
|
||||
when(repository.lockOwned(occupancy.getTenantId(), occupancy.getWarehouseId(), occupancy.getLocationId()))
|
||||
.thenReturn(location);
|
||||
when(repository.lockReservation(occupancy.getTenantId(), occupancy.getReservationId())).thenReturn(reservation);
|
||||
when(repository.releaseOccupancy(occupancy, reason)).thenReturn(1);
|
||||
when(repository.releaseConfirmedReservation(reservation, reason)).thenReturn(1);
|
||||
if (!Boolean.TRUE.equals(location.getSharedOverflow())) {
|
||||
when(repository.countActiveOccupancies(occupancy.getTenantId(), occupancy.getLocationId())).thenReturn(0);
|
||||
if ("OCCUPIED".equals(location.getStatus())) {
|
||||
when(repository.changeStatus(occupancy.getTenantId(), occupancy.getWarehouseId(), occupancy.getLocationId(),
|
||||
"OCCUPIED", "AVAILABLE", location.getVersion(), operatorId)).thenReturn(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static LocationReservationEntity confirmedReservationFor(LocationOccupancyEntity occupancy) {
|
||||
LocationReservationEntity reservation = reservation("CONFIRMED");
|
||||
reservation.setId(occupancy.getReservationId());
|
||||
reservation.setTenantId(occupancy.getTenantId());
|
||||
reservation.setWarehouseId(occupancy.getWarehouseId());
|
||||
reservation.setSourceService(occupancy.getSourceService());
|
||||
reservation.setBusinessId(occupancy.getBusinessId());
|
||||
reservation.setPalletId(occupancy.getPalletId());
|
||||
reservation.setLocationId(occupancy.getLocationId());
|
||||
reservation.setLocationCode(occupancy.getLocationCode());
|
||||
reservation.setSharedOverflow(occupancy.getSharedOverflow());
|
||||
return reservation;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user