派车提交
This commit is contained in:
+22
@@ -3,8 +3,11 @@ package com.baiyue.warehouse.controller;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.warehouse.dto.WarehouseResourceLockRequests;
|
||||
import com.baiyue.warehouse.dto.WarehouseResourceLockResponse;
|
||||
import com.baiyue.warehouse.dto.WarehousePalletScrapResponse;
|
||||
import com.baiyue.warehouse.entity.WarehouseLocationEntity;
|
||||
import com.baiyue.warehouse.security.InternalServiceTokenGuard;
|
||||
import com.baiyue.warehouse.service.WarehouseResourceLockService;
|
||||
import com.baiyue.warehouse.service.WarehouseResourceService;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
@@ -18,6 +21,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
@RequestMapping("/internal/v1/warehouse-resource-locks")
|
||||
public class InternalWarehouseResourceLockController {
|
||||
@Resource private WarehouseResourceLockService service;
|
||||
@Resource private WarehouseResourceService resourceService;
|
||||
@Resource private InternalServiceTokenGuard tokenGuard;
|
||||
|
||||
@PostMapping
|
||||
@@ -45,4 +49,22 @@ public class InternalWarehouseResourceLockController {
|
||||
tokenGuard.verify(token);
|
||||
return ApiResponse.success(service.scrap(request.tenantId(), lockId));
|
||||
}
|
||||
|
||||
@PostMapping("/pallets/{palletId}/scrap")
|
||||
public ApiResponse<WarehousePalletScrapResponse> scrapPallet(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@PathVariable long palletId,
|
||||
@Valid @RequestBody WarehouseResourceLockRequests.CloseRequest request) {
|
||||
tokenGuard.verify(token);
|
||||
return ApiResponse.success(service.scrapByPallet(request.tenantId(), palletId));
|
||||
}
|
||||
|
||||
@PostMapping("/default-locations/ensure")
|
||||
public ApiResponse<WarehouseLocationEntity> ensureDefaultLocation(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@Valid @RequestBody WarehouseResourceLockRequests.DefaultLocationRequest request) {
|
||||
tokenGuard.verify(token);
|
||||
return ApiResponse.success(resourceService.ensureDefaultVirtualLocation(
|
||||
request.tenantId(), request.warehouseId()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public final class PalletRequests {
|
||||
}
|
||||
|
||||
public record SaveRequest(
|
||||
@NotBlank @Size(max = 64) String palletCode,
|
||||
@Size(max = 128) String palletCode,
|
||||
@NotBlank @Size(max = 32) String palletType,
|
||||
@DecimalMin(value = "0.0000", inclusive = false) BigDecimal capacityCbm,
|
||||
@NotNull @DecimalMin("0.00") BigDecimal unitPrice,
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.baiyue.warehouse.dto;
|
||||
|
||||
public record WarehousePalletScrapResponse(long palletId, long warehouseId, Long locationId, String status) {
|
||||
}
|
||||
@@ -17,4 +17,9 @@ public final class WarehouseResourceLockRequests {
|
||||
) {}
|
||||
|
||||
public record CloseRequest(@Positive long tenantId) {}
|
||||
|
||||
public record DefaultLocationRequest(
|
||||
@Positive long tenantId,
|
||||
@Positive long warehouseId
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,21 @@ package com.baiyue.warehouse.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.warehouse.entity.WarehouseLocationEntity;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
public interface WarehouseLocationMapper extends BaseMapper<WarehouseLocationEntity> {
|
||||
/**
|
||||
* The unique location-code index makes this safe for concurrent first use of an older warehouse.
|
||||
*/
|
||||
@Insert("""
|
||||
INSERT IGNORE INTO wh_location
|
||||
(id, tenant_id, warehouse_id, zone_code, location_code, location_type,
|
||||
max_load_kg, status, remark, deleted, version)
|
||||
VALUES
|
||||
(#{location.id}, #{location.tenantId}, #{location.warehouseId}, #{location.zoneCode},
|
||||
#{location.locationCode}, #{location.locationType}, #{location.maxLoadKg},
|
||||
#{location.status}, #{location.remark}, 0, 0)
|
||||
""")
|
||||
int insertDefaultVirtualLocationIgnore(@Param("location") WarehouseLocationEntity location);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,11 @@ public interface WarehouseResourceLockMapper extends BaseMapper<WarehouseResourc
|
||||
WarehouseResourceLockEntity findByReservationKey(@Param("tenantId") long tenantId,
|
||||
@Param("reservationKey") String reservationKey);
|
||||
|
||||
@Select("SELECT * FROM wh_resource_lock WHERE tenant_id=#{tenantId} AND pallet_id=#{palletId} " +
|
||||
"AND status='ACTIVE' ORDER BY locked_at DESC LIMIT 1")
|
||||
WarehouseResourceLockEntity findActiveByPallet(@Param("tenantId") long tenantId,
|
||||
@Param("palletId") long palletId);
|
||||
|
||||
@Update("""
|
||||
UPDATE wh_pallet
|
||||
SET status = 'OCCUPIED', location_id = #{locationId}, updated_at = CURRENT_TIMESTAMP(3), version = version + 1
|
||||
@@ -42,6 +47,17 @@ public interface WarehouseResourceLockMapper extends BaseMapper<WarehouseResourc
|
||||
int releasePallet(@Param("tenantId") long tenantId, @Param("warehouseId") long warehouseId,
|
||||
@Param("palletId") long palletId, @Param("locationId") long locationId);
|
||||
|
||||
@Update("""
|
||||
UPDATE wh_pallet
|
||||
SET status = 'IDLE', updated_at = CURRENT_TIMESTAMP(3), version = version + 1
|
||||
WHERE id = #{palletId} AND tenant_id = #{tenantId} AND warehouse_id = #{warehouseId}
|
||||
AND location_id = #{locationId} AND status = 'OCCUPIED' AND deleted = 0
|
||||
""")
|
||||
int releasePalletAtVirtualLocation(@Param("tenantId") long tenantId,
|
||||
@Param("warehouseId") long warehouseId,
|
||||
@Param("palletId") long palletId,
|
||||
@Param("locationId") long locationId);
|
||||
|
||||
@Update("""
|
||||
UPDATE wh_location
|
||||
SET status = 'IDLE', updated_at = CURRENT_TIMESTAMP(3), version = version + 1
|
||||
@@ -60,6 +76,21 @@ public interface WarehouseResourceLockMapper extends BaseMapper<WarehouseResourc
|
||||
int scrapPallet(@Param("tenantId") long tenantId, @Param("warehouseId") long warehouseId,
|
||||
@Param("palletId") long palletId, @Param("locationId") long locationId);
|
||||
|
||||
@Update("""
|
||||
UPDATE wh_pallet
|
||||
SET status = 'SCRAPPED', updated_at = CURRENT_TIMESTAMP(3), version = version + 1
|
||||
WHERE id = #{palletId} AND tenant_id = #{tenantId} AND warehouse_id = #{warehouseId}
|
||||
AND location_id = #{locationId} AND status = 'OCCUPIED' AND deleted = 0
|
||||
""")
|
||||
int scrapPalletAtVirtualLocation(@Param("tenantId") long tenantId,
|
||||
@Param("warehouseId") long warehouseId,
|
||||
@Param("palletId") long palletId,
|
||||
@Param("locationId") long locationId);
|
||||
|
||||
@Update("UPDATE wh_pallet SET status='SCRAPPED',updated_at=CURRENT_TIMESTAMP(3),version=version+1 " +
|
||||
"WHERE tenant_id=#{tenantId} AND id=#{palletId} AND status='OCCUPIED' AND deleted=0")
|
||||
int scrapPalletWithoutLock(@Param("tenantId") long tenantId, @Param("palletId") long palletId);
|
||||
|
||||
@Update("""
|
||||
UPDATE wh_resource_lock
|
||||
SET status = #{status}, released_at = CURRENT_TIMESTAMP(3), updated_at = CURRENT_TIMESTAMP(3)
|
||||
|
||||
@@ -7,9 +7,11 @@ import com.baiyue.warehouse.dto.WarehouseRequests;
|
||||
import com.baiyue.warehouse.entity.ServiceCapabilityEntity;
|
||||
import com.baiyue.warehouse.entity.WarehouseAddressEntity;
|
||||
import com.baiyue.warehouse.entity.WarehouseEntity;
|
||||
import com.baiyue.warehouse.entity.WarehouseLocationEntity;
|
||||
import com.baiyue.warehouse.mapper.ServiceCapabilityMapper;
|
||||
import com.baiyue.warehouse.mapper.WarehouseAddressMapper;
|
||||
import com.baiyue.warehouse.mapper.WarehouseMapper;
|
||||
import com.baiyue.warehouse.mapper.WarehouseLocationMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.util.StringUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -22,6 +24,7 @@ import java.util.List;
|
||||
@Repository
|
||||
public class WarehouseRepository {
|
||||
@Resource private WarehouseMapper warehouses;
|
||||
@Resource private WarehouseLocationMapper locations;
|
||||
@Resource private WarehouseAddressMapper addresses;
|
||||
@Resource private ServiceCapabilityMapper capabilities;
|
||||
|
||||
@@ -29,6 +32,17 @@ public class WarehouseRepository {
|
||||
warehouses.insert(warehouse);
|
||||
}
|
||||
|
||||
public void insertLocation(WarehouseLocationEntity location) {
|
||||
locations.insert(location);
|
||||
}
|
||||
|
||||
public boolean existsLocationCode(long tenantId, long warehouseId, String locationCode) {
|
||||
return locations.selectCount(new LambdaQueryWrapper<WarehouseLocationEntity>()
|
||||
.eq(WarehouseLocationEntity::getTenantId, tenantId)
|
||||
.eq(WarehouseLocationEntity::getWarehouseId, warehouseId)
|
||||
.eq(WarehouseLocationEntity::getLocationCode, locationCode)) > 0;
|
||||
}
|
||||
|
||||
public void saveWarehouse(WarehouseEntity warehouse) {
|
||||
warehouses.updateById(warehouse);
|
||||
}
|
||||
|
||||
+24
@@ -26,6 +26,16 @@ public class WarehouseResourceLockRepository {
|
||||
.eq(WarehouseResourceLockEntity::getId, lockId));
|
||||
}
|
||||
|
||||
public WarehouseResourceLockEntity findActiveByPallet(long tenantId, long palletId) {
|
||||
return locks.findActiveByPallet(tenantId, palletId);
|
||||
}
|
||||
|
||||
public WarehousePalletEntity findPallet(long tenantId, long palletId) {
|
||||
return pallets.selectOne(new LambdaQueryWrapper<WarehousePalletEntity>()
|
||||
.eq(WarehousePalletEntity::getTenantId, tenantId)
|
||||
.eq(WarehousePalletEntity::getId, palletId));
|
||||
}
|
||||
|
||||
public WarehousePalletEntity findPallet(long tenantId, long warehouseId, long palletId) {
|
||||
return pallets.selectOne(new LambdaQueryWrapper<WarehousePalletEntity>()
|
||||
.eq(WarehousePalletEntity::getTenantId, tenantId)
|
||||
@@ -54,10 +64,24 @@ public class WarehouseResourceLockRepository {
|
||||
return locks.releasePallet(lock.getTenantId(), lock.getWarehouseId(), lock.getPalletId(), lock.getLocationId());
|
||||
}
|
||||
|
||||
public int releasePalletAtVirtualLocation(WarehouseResourceLockEntity lock) {
|
||||
return locks.releasePalletAtVirtualLocation(lock.getTenantId(), lock.getWarehouseId(),
|
||||
lock.getPalletId(), lock.getLocationId());
|
||||
}
|
||||
|
||||
public int scrapPallet(WarehouseResourceLockEntity lock) {
|
||||
return locks.scrapPallet(lock.getTenantId(), lock.getWarehouseId(), lock.getPalletId(), lock.getLocationId());
|
||||
}
|
||||
|
||||
public int scrapPalletWithoutLock(long tenantId, long palletId) {
|
||||
return locks.scrapPalletWithoutLock(tenantId, palletId);
|
||||
}
|
||||
|
||||
public int scrapPalletAtVirtualLocation(WarehouseResourceLockEntity lock) {
|
||||
return locks.scrapPalletAtVirtualLocation(lock.getTenantId(), lock.getWarehouseId(),
|
||||
lock.getPalletId(), lock.getLocationId());
|
||||
}
|
||||
|
||||
public int releaseLocation(WarehouseResourceLockEntity lock) {
|
||||
return locks.releaseLocation(lock.getTenantId(), lock.getWarehouseId(), lock.getLocationId());
|
||||
}
|
||||
|
||||
+27
@@ -27,10 +27,37 @@ public class WarehouseResourceRepository {
|
||||
.eq(WarehouseEntity::getId, warehouseId)) > 0;
|
||||
}
|
||||
|
||||
public WarehouseEntity findWarehouse(long tenantId, long warehouseId) {
|
||||
return warehouses.selectOne(new LambdaQueryWrapper<WarehouseEntity>()
|
||||
.eq(WarehouseEntity::getTenantId, tenantId)
|
||||
.eq(WarehouseEntity::getId, warehouseId));
|
||||
}
|
||||
|
||||
public WarehouseLocationEntity findDefaultVirtualLocation(long tenantId, long warehouseId) {
|
||||
return locations.selectOne(new LambdaQueryWrapper<WarehouseLocationEntity>()
|
||||
.eq(WarehouseLocationEntity::getTenantId, tenantId)
|
||||
.eq(WarehouseLocationEntity::getWarehouseId, warehouseId)
|
||||
.eq(WarehouseLocationEntity::getLocationType, "VIRTUAL")
|
||||
.eq(WarehouseLocationEntity::getStatus, "ACTIVE")
|
||||
.orderByAsc(WarehouseLocationEntity::getCreatedAt)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
public WarehouseLocationEntity findLocationByCode(long tenantId, long warehouseId, String locationCode) {
|
||||
return locations.selectOne(new LambdaQueryWrapper<WarehouseLocationEntity>()
|
||||
.eq(WarehouseLocationEntity::getTenantId, tenantId)
|
||||
.eq(WarehouseLocationEntity::getWarehouseId, warehouseId)
|
||||
.eq(WarehouseLocationEntity::getLocationCode, locationCode));
|
||||
}
|
||||
|
||||
public void insertLocation(WarehouseLocationEntity entity) {
|
||||
locations.insert(entity);
|
||||
}
|
||||
|
||||
public int insertDefaultVirtualLocationIgnore(WarehouseLocationEntity entity) {
|
||||
return locations.insertDefaultVirtualLocationIgnore(entity);
|
||||
}
|
||||
|
||||
public void updateLocation(WarehouseLocationEntity entity) {
|
||||
locations.updateById(entity);
|
||||
}
|
||||
|
||||
@@ -10,11 +10,13 @@ import com.baiyue.warehouse.dto.WarehouseRequests;
|
||||
import com.baiyue.warehouse.entity.ServiceCapabilityEntity;
|
||||
import com.baiyue.warehouse.entity.WarehouseAddressEntity;
|
||||
import com.baiyue.warehouse.entity.WarehouseEntity;
|
||||
import com.baiyue.warehouse.entity.WarehouseLocationEntity;
|
||||
import com.baiyue.warehouse.repository.WarehouseRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 仓库应用服务,负责仓库主数据、地址和服务能力的租户级用例编排、事务控制与业务校验。
|
||||
@@ -37,10 +39,28 @@ public class WarehouseApplicationService {
|
||||
ensureCodeUnique(request.tenantId(), request.warehouseCode(), null);
|
||||
WarehouseEntity warehouse = toEntity(request);
|
||||
repository.insertWarehouse(warehouse);
|
||||
ensureDefaultVirtualLocation(warehouse);
|
||||
insertChildren(warehouse.getTenantId(), warehouse.getId(), request.address(), request.capabilities());
|
||||
return warehouse.getId();
|
||||
}
|
||||
|
||||
private void ensureDefaultVirtualLocation(WarehouseEntity warehouse) {
|
||||
String locationCode = "LOC-" + warehouse.getWarehouseCode() + "-DEFAULT";
|
||||
if (repository.existsLocationCode(warehouse.getTenantId(), warehouse.getId(), locationCode)) {
|
||||
return;
|
||||
}
|
||||
WarehouseLocationEntity location = new WarehouseLocationEntity();
|
||||
location.setTenantId(warehouse.getTenantId());
|
||||
location.setWarehouseId(warehouse.getId());
|
||||
location.setZoneCode("DEFAULT");
|
||||
location.setLocationCode(locationCode);
|
||||
location.setLocationType("VIRTUAL");
|
||||
location.setMaxLoadKg(new BigDecimal("999999999.000"));
|
||||
location.setStatus("ACTIVE");
|
||||
location.setRemark("System default virtual location");
|
||||
repository.insertLocation(location);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(
|
||||
event = "WAREHOUSE_UPDATE",
|
||||
|
||||
+41
-4
@@ -4,6 +4,7 @@ import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.warehouse.dto.WarehouseResourceLockRequests;
|
||||
import com.baiyue.warehouse.dto.WarehouseResourceLockResponse;
|
||||
import com.baiyue.warehouse.dto.WarehousePalletScrapResponse;
|
||||
import com.baiyue.warehouse.entity.WarehouseLocationEntity;
|
||||
import com.baiyue.warehouse.entity.WarehousePalletEntity;
|
||||
import com.baiyue.warehouse.entity.WarehouseResourceLockEntity;
|
||||
@@ -33,7 +34,9 @@ public class WarehouseResourceLockService {
|
||||
if (concurrent != null) return idempotentResult(concurrent, request);
|
||||
throw new BizException(ResultCode.NOT_FOUND, "Pallet or location does not belong to the tenant warehouse");
|
||||
}
|
||||
if (!"IDLE".equals(pallet.getStatus()) || !"IDLE".equals(location.getStatus())) {
|
||||
boolean virtualLocation = isVirtual(location);
|
||||
boolean locationAvailable = virtualLocation ? "ACTIVE".equals(location.getStatus()) : "IDLE".equals(location.getStatus());
|
||||
if (!"IDLE".equals(pallet.getStatus()) || !locationAvailable) {
|
||||
WarehouseResourceLockEntity concurrent =
|
||||
repository.findByReservationKey(request.tenantId(), request.reservationKey());
|
||||
if (concurrent != null) return idempotentResult(concurrent, request);
|
||||
@@ -42,7 +45,7 @@ public class WarehouseResourceLockService {
|
||||
if (repository.occupyPallet(request.tenantId(), request.warehouseId(), request.palletId(), request.locationId()) != 1) {
|
||||
return concurrentIdempotentOrConflict(request);
|
||||
}
|
||||
if (repository.occupyLocation(request.tenantId(), request.warehouseId(), request.locationId()) != 1) {
|
||||
if (!virtualLocation && repository.occupyLocation(request.tenantId(), request.warehouseId(), request.locationId()) != 1) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Location is no longer IDLE");
|
||||
}
|
||||
|
||||
@@ -65,7 +68,9 @@ public class WarehouseResourceLockService {
|
||||
WarehouseResourceLockEntity lock = requireLock(tenantId, lockId);
|
||||
if ("RELEASED".equals(lock.getStatus())) return WarehouseResourceLockResponse.from(lock);
|
||||
requireActive(lock);
|
||||
if (repository.releasePallet(lock) != 1 || repository.releaseLocation(lock) != 1
|
||||
boolean virtualLocation = isVirtualLockLocation(lock);
|
||||
int palletResult = virtualLocation ? repository.releasePalletAtVirtualLocation(lock) : repository.releasePallet(lock);
|
||||
if (palletResult != 1 || (!virtualLocation && repository.releaseLocation(lock) != 1)
|
||||
|| repository.close(lock, "RELEASED") != 1) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Resource lock state changed concurrently");
|
||||
}
|
||||
@@ -79,7 +84,9 @@ public class WarehouseResourceLockService {
|
||||
WarehouseResourceLockEntity lock = requireLock(tenantId, lockId);
|
||||
if ("SCRAPPED".equals(lock.getStatus())) return WarehouseResourceLockResponse.from(lock);
|
||||
requireActive(lock);
|
||||
if (repository.scrapPallet(lock) != 1 || repository.releaseLocation(lock) != 1
|
||||
boolean virtualLocation = isVirtualLockLocation(lock);
|
||||
int palletResult = virtualLocation ? repository.scrapPalletAtVirtualLocation(lock) : repository.scrapPallet(lock);
|
||||
if (palletResult != 1 || (!virtualLocation && repository.releaseLocation(lock) != 1)
|
||||
|| repository.close(lock, "SCRAPPED") != 1) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Resource lock state changed concurrently");
|
||||
}
|
||||
@@ -88,6 +95,25 @@ public class WarehouseResourceLockService {
|
||||
return WarehouseResourceLockResponse.from(lock);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public WarehousePalletScrapResponse scrapByPallet(long tenantId, long palletId) {
|
||||
WarehousePalletEntity pallet = repository.findPallet(tenantId, palletId);
|
||||
if (pallet == null) throw new BizException(ResultCode.NOT_FOUND, "Pallet not found");
|
||||
if ("SCRAPPED".equals(pallet.getStatus())) {
|
||||
return new WarehousePalletScrapResponse(palletId, pallet.getWarehouseId(), pallet.getLocationId(), "SCRAPPED");
|
||||
}
|
||||
if (!"OCCUPIED".equals(pallet.getStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Only an OCCUPIED pallet can be scrapped after shipment");
|
||||
}
|
||||
WarehouseResourceLockEntity lock = repository.findActiveByPallet(tenantId, palletId);
|
||||
if (lock != null) {
|
||||
scrap(tenantId, lock.getId());
|
||||
} else if (repository.scrapPalletWithoutLock(tenantId, palletId) != 1) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Pallet state changed concurrently");
|
||||
}
|
||||
return new WarehousePalletScrapResponse(palletId, pallet.getWarehouseId(), pallet.getLocationId(), "SCRAPPED");
|
||||
}
|
||||
|
||||
private WarehouseResourceLockResponse concurrentIdempotentOrConflict(
|
||||
WarehouseResourceLockRequests.ReserveRequest request) {
|
||||
WarehouseResourceLockEntity existing = repository.findByReservationKey(request.tenantId(), request.reservationKey());
|
||||
@@ -118,4 +144,15 @@ public class WarehouseResourceLockService {
|
||||
throw new BizException(ResultCode.CONFLICT, "Resource lock is not ACTIVE");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private boolean isVirtualLockLocation(WarehouseResourceLockEntity lock) {
|
||||
WarehouseLocationEntity location = repository.findLocation(lock.getTenantId(), lock.getWarehouseId(), lock.getLocationId());
|
||||
if (location == null) throw new BizException(ResultCode.NOT_FOUND, "Resource lock location not found");
|
||||
return isVirtual(location);
|
||||
}
|
||||
|
||||
private static boolean isVirtual(WarehouseLocationEntity location) {
|
||||
return "VIRTUAL".equals(location.getLocationType());
|
||||
}
|
||||
}
|
||||
|
||||
+106
-17
@@ -7,12 +7,17 @@ import com.baiyue.warehouse.dto.LocationRequests;
|
||||
import com.baiyue.warehouse.dto.PalletRequests;
|
||||
import com.baiyue.warehouse.entity.WarehouseLocationEntity;
|
||||
import com.baiyue.warehouse.entity.WarehousePalletEntity;
|
||||
import com.baiyue.warehouse.entity.WarehouseEntity;
|
||||
import com.baiyue.warehouse.repository.WarehouseResourceRepository;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import java.util.Set;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@Service
|
||||
public class WarehouseResourceService {
|
||||
@@ -33,6 +38,11 @@ public class WarehouseResourceService {
|
||||
@Transactional
|
||||
public void updateLocation(long tenantId, long warehouseId, long id, LocationRequests.SaveRequest request) {
|
||||
WarehouseLocationEntity existing = requireLocation(tenantId, warehouseId, id);
|
||||
if ("VIRTUAL".equals(existing.getLocationType())
|
||||
&& (!"VIRTUAL".equals(request.locationType()) || !"ACTIVE".equals(request.status())
|
||||
|| !existing.getLocationCode().equals(request.locationCode()))) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Default virtual location must remain VIRTUAL and ACTIVE");
|
||||
}
|
||||
validateLocation(request, tenantId, warehouseId, id);
|
||||
WarehouseLocationEntity entity = locationEntity(tenantId, warehouseId, request);
|
||||
entity.setId(existing.getId());
|
||||
@@ -52,7 +62,10 @@ public class WarehouseResourceService {
|
||||
|
||||
@Transactional
|
||||
public void deleteLocation(long tenantId, long warehouseId, long id) {
|
||||
requireLocation(tenantId, warehouseId, id);
|
||||
WarehouseLocationEntity location = requireLocation(tenantId, warehouseId, id);
|
||||
if ("VIRTUAL".equals(location.getLocationType())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Default virtual location cannot be deleted");
|
||||
}
|
||||
if (repository.locationHasPallets(tenantId, warehouseId, id)) {
|
||||
throw new BizException(ResultCode.CONFLICT, "库位仍绑定托盘,不能删除");
|
||||
}
|
||||
@@ -61,18 +74,69 @@ public class WarehouseResourceService {
|
||||
|
||||
@Transactional
|
||||
public Long createPallet(long tenantId, long warehouseId, PalletRequests.SaveRequest request) {
|
||||
requireWarehouse(tenantId, warehouseId);
|
||||
validatePallet(request, tenantId, warehouseId, null);
|
||||
WarehousePalletEntity entity = palletEntity(tenantId, warehouseId, request);
|
||||
WarehouseEntity warehouse = requireWarehouseEntity(tenantId, warehouseId);
|
||||
validatePalletStatus(request.status());
|
||||
Long palletId = IdWorker.getId();
|
||||
String palletCode = StringUtils.hasText(request.palletCode()) ? request.palletCode().trim()
|
||||
: generatedPalletCode(warehouse.getWarehouseCode(), palletId);
|
||||
Long locationId = request.locationId();
|
||||
if (locationId == null) {
|
||||
locationId = ensureDefaultVirtualLocation(tenantId, warehouseId).getId();
|
||||
}
|
||||
validatePallet(request, palletCode, locationId, tenantId, warehouseId, null);
|
||||
WarehousePalletEntity entity = palletEntity(tenantId, warehouseId, request, palletCode, locationId);
|
||||
entity.setId(palletId);
|
||||
repository.insertPallet(entity);
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the warehouse-scoped virtual location used only as a pallet/inventory reference.
|
||||
* It is deliberately never made IDLE/OCCUPIED by resource locking.
|
||||
*/
|
||||
@Transactional
|
||||
public WarehouseLocationEntity ensureDefaultVirtualLocation(long tenantId, long warehouseId) {
|
||||
WarehouseEntity warehouse = requireWarehouseEntity(tenantId, warehouseId);
|
||||
String locationCode = defaultLocationCode(warehouse.getWarehouseCode());
|
||||
WarehouseLocationEntity existing = repository.findLocationByCode(tenantId, warehouseId, locationCode);
|
||||
if (existing != null) {
|
||||
if (!"VIRTUAL".equals(existing.getLocationType()) || !"ACTIVE".equals(existing.getStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT,
|
||||
"Default virtual location code is occupied by a non-active virtual location");
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
|
||||
WarehouseLocationEntity location = new WarehouseLocationEntity();
|
||||
location.setId(IdWorker.getId());
|
||||
location.setTenantId(tenantId);
|
||||
location.setWarehouseId(warehouseId);
|
||||
location.setZoneCode("DEFAULT");
|
||||
location.setLocationCode(locationCode);
|
||||
location.setLocationType("VIRTUAL");
|
||||
location.setMaxLoadKg(new java.math.BigDecimal("999999999.000"));
|
||||
location.setStatus("ACTIVE");
|
||||
location.setRemark("System default virtual location");
|
||||
repository.insertDefaultVirtualLocationIgnore(location);
|
||||
|
||||
WarehouseLocationEntity resolved = repository.findLocationByCode(tenantId, warehouseId, locationCode);
|
||||
if (resolved == null) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Default virtual location could not be created");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void updatePallet(long tenantId, long warehouseId, long id, PalletRequests.SaveRequest request) {
|
||||
WarehousePalletEntity existing = requirePallet(tenantId, warehouseId, id);
|
||||
validatePallet(request, tenantId, warehouseId, id);
|
||||
WarehousePalletEntity entity = palletEntity(tenantId, warehouseId, request);
|
||||
if ("SCRAPPED".equals(existing.getStatus()) && !"SCRAPPED".equals(request.status())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "SCRAPPED pallet cannot be reused");
|
||||
}
|
||||
String palletCode = StringUtils.hasText(request.palletCode()) ? request.palletCode().trim()
|
||||
: existing.getPalletCode();
|
||||
Long locationId = request.locationId() == null ? existing.getLocationId() : request.locationId();
|
||||
validatePallet(request, palletCode, locationId, tenantId, warehouseId, id);
|
||||
WarehousePalletEntity entity = palletEntity(tenantId, warehouseId, request, palletCode, locationId);
|
||||
entity.setId(existing.getId());
|
||||
repository.updatePallet(entity);
|
||||
}
|
||||
@@ -94,6 +158,9 @@ public class WarehouseResourceService {
|
||||
if ("OCCUPIED".equals(pallet.getStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "占用中的托盘不能删除");
|
||||
}
|
||||
if ("SCRAPPED".equals(pallet.getStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Scrapped pallet must be retained and cannot be reused");
|
||||
}
|
||||
repository.deletePallet(id);
|
||||
}
|
||||
|
||||
@@ -103,6 +170,14 @@ public class WarehouseResourceService {
|
||||
}
|
||||
}
|
||||
|
||||
private WarehouseEntity requireWarehouseEntity(long tenantId, long warehouseId) {
|
||||
WarehouseEntity warehouse = repository.findWarehouse(tenantId, warehouseId);
|
||||
if (warehouse == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "Tenant warehouse not found");
|
||||
}
|
||||
return warehouse;
|
||||
}
|
||||
|
||||
private WarehouseLocationEntity requireLocation(long tenantId, long warehouseId, long id) {
|
||||
requireWarehouse(tenantId, warehouseId);
|
||||
WarehouseLocationEntity entity = repository.findLocation(tenantId, warehouseId, id);
|
||||
@@ -127,16 +202,20 @@ public class WarehouseResourceService {
|
||||
}
|
||||
}
|
||||
|
||||
private void validatePallet(PalletRequests.SaveRequest request, long tenantId,
|
||||
long warehouseId, Long excludedId) {
|
||||
if (!PALLET_STATUSES.contains(request.status())) {
|
||||
throw new BizException("托盘状态仅支持 IDLE、OCCUPIED、SCRAPPED");
|
||||
}
|
||||
if (repository.palletCodeExists(tenantId, warehouseId, request.palletCode(), excludedId)) {
|
||||
private void validatePallet(PalletRequests.SaveRequest request, String palletCode, Long locationId,
|
||||
long tenantId, long warehouseId, Long excludedId) {
|
||||
validatePalletStatus(request.status());
|
||||
if (repository.palletCodeExists(tenantId, warehouseId, palletCode, excludedId)) {
|
||||
throw new BizException(ResultCode.CONFLICT, "当前仓库下托盘编码已存在");
|
||||
}
|
||||
if (request.locationId() != null) {
|
||||
requireLocation(tenantId, warehouseId, request.locationId());
|
||||
if (locationId != null) {
|
||||
requireLocation(tenantId, warehouseId, locationId);
|
||||
}
|
||||
}
|
||||
|
||||
private void validatePalletStatus(String status) {
|
||||
if (!PALLET_STATUSES.contains(status)) {
|
||||
throw new BizException("托盘状态仅支持 IDLE、OCCUPIED、SCRAPPED");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,12 +234,13 @@ public class WarehouseResourceService {
|
||||
}
|
||||
|
||||
private WarehousePalletEntity palletEntity(long tenantId, long warehouseId,
|
||||
PalletRequests.SaveRequest request) {
|
||||
PalletRequests.SaveRequest request,
|
||||
String palletCode, Long locationId) {
|
||||
WarehousePalletEntity entity = new WarehousePalletEntity();
|
||||
entity.setTenantId(tenantId);
|
||||
entity.setWarehouseId(warehouseId);
|
||||
entity.setLocationId(request.locationId());
|
||||
entity.setPalletCode(request.palletCode());
|
||||
entity.setLocationId(locationId);
|
||||
entity.setPalletCode(palletCode);
|
||||
entity.setPalletType(request.palletType());
|
||||
entity.setCapacityCbm(request.capacityCbm());
|
||||
entity.setUnitPrice(request.unitPrice());
|
||||
@@ -168,4 +248,13 @@ public class WarehouseResourceService {
|
||||
entity.setRemark(request.remark());
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static String generatedPalletCode(String warehouseCode, long palletId) {
|
||||
return "PLT-" + warehouseCode + "-" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE)
|
||||
+ "-" + Long.toUnsignedString(palletId);
|
||||
}
|
||||
|
||||
private static String defaultLocationCode(String warehouseCode) {
|
||||
return "LOC-" + warehouseCode + "-DEFAULT";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS wh_pallet (
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '所属仓库ID',
|
||||
location_id BIGINT UNSIGNED DEFAULT NULL COMMENT '同仓库内绑定的库位ID,可为空',
|
||||
pallet_code VARCHAR(64) NOT NULL COMMENT '仓库内唯一托盘编码',
|
||||
pallet_code VARCHAR(128) NOT NULL COMMENT '仓库内唯一托盘编码',
|
||||
pallet_type VARCHAR(32) NOT NULL COMMENT '托盘类型',
|
||||
capacity_cbm DECIMAL(18,4) NOT NULL DEFAULT 2.0000 COMMENT '托盘容量(立方米)',
|
||||
unit_price DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '托盘单价',
|
||||
@@ -159,13 +159,25 @@ ALTER TABLE wh_location
|
||||
MODIFY COLUMN deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
|
||||
MODIFY COLUMN version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号';
|
||||
|
||||
-- Every warehouse has one non-occupiable virtual location used for inventory history.
|
||||
INSERT INTO wh_location (id, tenant_id, warehouse_id, zone_code, location_code, location_type,
|
||||
max_load_kg, status, remark)
|
||||
SELECT UUID_SHORT(), w.tenant_id, w.id, 'DEFAULT', CONCAT('LOC-', w.warehouse_code, '-DEFAULT'),
|
||||
'VIRTUAL', 999999999.000, 'ACTIVE', 'System default virtual location'
|
||||
FROM wh_warehouse w
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM wh_location l
|
||||
WHERE l.tenant_id = w.tenant_id AND l.warehouse_id = w.id
|
||||
AND l.location_code = CONCAT('LOC-', w.warehouse_code, '-DEFAULT') AND l.deleted = 0
|
||||
);
|
||||
|
||||
ALTER TABLE wh_pallet COMMENT='仓库托盘表:按租户和仓库维护托盘、容量、单价及状态';
|
||||
ALTER TABLE wh_pallet
|
||||
MODIFY COLUMN id BIGINT UNSIGNED NOT NULL COMMENT '托盘ID(雪花ID)',
|
||||
MODIFY COLUMN tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
MODIFY COLUMN warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '所属仓库ID',
|
||||
MODIFY COLUMN location_id BIGINT UNSIGNED NULL COMMENT '同仓库内绑定的库位ID,可为空',
|
||||
MODIFY COLUMN pallet_code VARCHAR(64) NOT NULL COMMENT '仓库内唯一托盘编码',
|
||||
MODIFY COLUMN pallet_code VARCHAR(128) NOT NULL COMMENT '仓库内唯一托盘编码',
|
||||
MODIFY COLUMN pallet_type VARCHAR(32) NOT NULL COMMENT '托盘类型',
|
||||
MODIFY COLUMN capacity_cbm DECIMAL(18,4) NOT NULL DEFAULT 2.0000 COMMENT '托盘容量(立方米)',
|
||||
MODIFY COLUMN unit_price DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '托盘单价',
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.baiyue.warehouse.service;
|
||||
|
||||
import com.baiyue.warehouse.dto.WarehouseRequests;
|
||||
import com.baiyue.warehouse.entity.WarehouseEntity;
|
||||
import com.baiyue.warehouse.entity.WarehouseLocationEntity;
|
||||
import com.baiyue.warehouse.repository.WarehouseRepository;
|
||||
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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class WarehouseApplicationServiceTest {
|
||||
@Mock WarehouseRepository repository;
|
||||
private WarehouseApplicationService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new WarehouseApplicationService();
|
||||
ReflectionTestUtils.setField(service, "repository", repository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void warehouseCreationAlsoCreatesActiveDefaultVirtualLocation() {
|
||||
doAnswer(call -> {
|
||||
((WarehouseEntity) call.getArgument(0)).setId(20001L);
|
||||
return null;
|
||||
}).when(repository).insertWarehouse(any());
|
||||
|
||||
service.create(request());
|
||||
|
||||
ArgumentCaptor<WarehouseLocationEntity> location = ArgumentCaptor.forClass(WarehouseLocationEntity.class);
|
||||
verify(repository).insertLocation(location.capture());
|
||||
assertThat(location.getValue().getTenantId()).isEqualTo(10001L);
|
||||
assertThat(location.getValue().getWarehouseId()).isEqualTo(20001L);
|
||||
assertThat(location.getValue().getLocationCode()).isEqualTo("LOC-US-LAX-01-DEFAULT");
|
||||
assertThat(location.getValue().getLocationType()).isEqualTo("VIRTUAL");
|
||||
assertThat(location.getValue().getStatus()).isEqualTo("ACTIVE");
|
||||
}
|
||||
|
||||
private static WarehouseRequests.SaveRequest request() {
|
||||
WarehouseRequests.AddressRequest address = new WarehouseRequests.AddressRequest(
|
||||
"WAREHOUSE", "US", "CA", "Los Angeles", null, "90001",
|
||||
"1 Logistics Ave", null, null, null);
|
||||
return new WarehouseRequests.SaveRequest(10001L, "US-LAX-01", "LAX Warehouse",
|
||||
"OVERSEAS", "US", "America/Los_Angeles", null, null, null,
|
||||
"ACTIVE", null, address, List.of());
|
||||
}
|
||||
}
|
||||
+69
@@ -122,6 +122,7 @@ class WarehouseResourceLockServiceTest {
|
||||
void releaseRestoresBothResourcesAndClosesLock() {
|
||||
WarehouseResourceLockEntity lock = activeLock();
|
||||
when(repository.find(TENANT, lock.getId())).thenReturn(lock);
|
||||
when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(location("OCCUPIED"));
|
||||
when(repository.releasePallet(lock)).thenReturn(1);
|
||||
when(repository.releaseLocation(lock)).thenReturn(1);
|
||||
when(repository.close(lock, "RELEASED")).thenReturn(1);
|
||||
@@ -137,6 +138,7 @@ class WarehouseResourceLockServiceTest {
|
||||
void scrapDiscardsPalletAndReleasesLocation() {
|
||||
WarehouseResourceLockEntity lock = activeLock();
|
||||
when(repository.find(TENANT, lock.getId())).thenReturn(lock);
|
||||
when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(location("OCCUPIED"));
|
||||
when(repository.scrapPallet(lock)).thenReturn(1);
|
||||
when(repository.releaseLocation(lock)).thenReturn(1);
|
||||
when(repository.close(lock, "SCRAPPED")).thenReturn(1);
|
||||
@@ -161,6 +163,73 @@ class WarehouseResourceLockServiceTest {
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void virtualLocationIsNeverOccupiedOrReleased() {
|
||||
WarehouseLocationEntity virtual = location("ACTIVE");
|
||||
virtual.setLocationType("VIRTUAL");
|
||||
when(repository.findPallet(TENANT, WAREHOUSE, PALLET)).thenReturn(pallet("IDLE"));
|
||||
when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(virtual);
|
||||
when(repository.occupyPallet(TENANT, WAREHOUSE, PALLET, LOCATION)).thenReturn(1);
|
||||
doAnswer(call -> {
|
||||
((WarehouseResourceLockEntity) call.getArgument(0)).setId(502L);
|
||||
return null;
|
||||
}).when(repository).insert(any());
|
||||
|
||||
service.reserve(request("virtual-reserve"));
|
||||
|
||||
verify(repository, never()).occupyLocation(TENANT, WAREHOUSE, LOCATION);
|
||||
}
|
||||
|
||||
@Test
|
||||
void scrappingAtVirtualLocationKeepsLocationUntouched() {
|
||||
WarehouseResourceLockEntity lock = activeLock();
|
||||
WarehouseLocationEntity virtual = location("ACTIVE");
|
||||
virtual.setLocationType("VIRTUAL");
|
||||
when(repository.find(TENANT, lock.getId())).thenReturn(lock);
|
||||
when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(virtual);
|
||||
when(repository.scrapPalletAtVirtualLocation(lock)).thenReturn(1);
|
||||
when(repository.close(lock, "SCRAPPED")).thenReturn(1);
|
||||
|
||||
var result = service.scrap(TENANT, lock.getId());
|
||||
|
||||
assertThat(result.status()).isEqualTo("SCRAPPED");
|
||||
verify(repository).scrapPalletAtVirtualLocation(lock);
|
||||
verify(repository, never()).releaseLocation(lock);
|
||||
}
|
||||
|
||||
@Test
|
||||
void scrapByPalletClosesItsActiveLockAndKeepsLocationReference() {
|
||||
WarehousePalletEntity pallet = pallet("OCCUPIED");
|
||||
pallet.setWarehouseId(WAREHOUSE);
|
||||
pallet.setLocationId(LOCATION);
|
||||
WarehouseResourceLockEntity lock = activeLock();
|
||||
when(repository.findPallet(TENANT, PALLET)).thenReturn(pallet);
|
||||
when(repository.findActiveByPallet(TENANT, PALLET)).thenReturn(lock);
|
||||
when(repository.find(TENANT, lock.getId())).thenReturn(lock);
|
||||
WarehouseLocationEntity virtual = location("ACTIVE");
|
||||
virtual.setLocationType("VIRTUAL");
|
||||
when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(virtual);
|
||||
when(repository.scrapPalletAtVirtualLocation(lock)).thenReturn(1);
|
||||
when(repository.close(lock, "SCRAPPED")).thenReturn(1);
|
||||
|
||||
var result = service.scrapByPallet(TENANT, PALLET);
|
||||
|
||||
assertThat(result.status()).isEqualTo("SCRAPPED");
|
||||
assertThat(result.locationId()).isEqualTo(LOCATION);
|
||||
verify(repository).scrapPalletAtVirtualLocation(lock);
|
||||
}
|
||||
|
||||
@Test
|
||||
void scrapByPalletIsIdempotentWhenAlreadyScrapped() {
|
||||
WarehousePalletEntity pallet = pallet("SCRAPPED");
|
||||
pallet.setWarehouseId(WAREHOUSE);
|
||||
pallet.setLocationId(LOCATION);
|
||||
when(repository.findPallet(TENANT, PALLET)).thenReturn(pallet);
|
||||
|
||||
assertThat(service.scrapByPallet(TENANT, PALLET).status()).isEqualTo("SCRAPPED");
|
||||
verify(repository, never()).findActiveByPallet(TENANT, PALLET);
|
||||
}
|
||||
|
||||
private static WarehouseResourceLockRequests.ReserveRequest request(String key) {
|
||||
return new WarehouseResourceLockRequests.ReserveRequest(TENANT, WAREHOUSE, PALLET, LOCATION,
|
||||
"INBOUND_PLAN_PALLET", "plan-pallet-1", key);
|
||||
|
||||
+97
-1
@@ -7,6 +7,7 @@ import com.baiyue.warehouse.dto.LocationRequests;
|
||||
import com.baiyue.warehouse.dto.PalletRequests;
|
||||
import com.baiyue.warehouse.entity.WarehouseLocationEntity;
|
||||
import com.baiyue.warehouse.entity.WarehousePalletEntity;
|
||||
import com.baiyue.warehouse.entity.WarehouseEntity;
|
||||
import com.baiyue.warehouse.repository.WarehouseResourceRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -89,6 +90,7 @@ class WarehouseResourceServiceTest {
|
||||
@Test
|
||||
void palletDefaultsToTwoCubicMetresAndBindsOwnedLocation() {
|
||||
when(repository.warehouseOwned(TENANT, WAREHOUSE)).thenReturn(true);
|
||||
when(repository.findWarehouse(TENANT, WAREHOUSE)).thenReturn(warehouse());
|
||||
WarehouseLocationEntity location = new WarehouseLocationEntity();
|
||||
location.setId(30001L);
|
||||
when(repository.findLocation(TENANT, WAREHOUSE, 30001L)).thenReturn(location);
|
||||
@@ -111,6 +113,7 @@ class WarehouseResourceServiceTest {
|
||||
@Test
|
||||
void rejectsLocationFromAnotherWarehouseWhenBindingPallet() {
|
||||
when(repository.warehouseOwned(TENANT, WAREHOUSE)).thenReturn(true);
|
||||
when(repository.findWarehouse(TENANT, WAREHOUSE)).thenReturn(warehouse());
|
||||
when(repository.findLocation(TENANT, WAREHOUSE, 30002L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> service.createPallet(TENANT, WAREHOUSE,
|
||||
@@ -147,8 +150,21 @@ class WarehouseResourceServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnknownPalletStatus() {
|
||||
void preventsDeletingScrappedPalletSoItsIdentityCannotBeReused() {
|
||||
when(repository.warehouseOwned(TENANT, WAREHOUSE)).thenReturn(true);
|
||||
WarehousePalletEntity pallet = new WarehousePalletEntity();
|
||||
pallet.setStatus("SCRAPPED");
|
||||
when(repository.findPallet(TENANT, WAREHOUSE, 40003L)).thenReturn(pallet);
|
||||
|
||||
assertThatThrownBy(() -> service.deletePallet(TENANT, WAREHOUSE, 40003L))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("cannot be reused");
|
||||
verify(repository, never()).deletePallet(40003L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnknownPalletStatus() {
|
||||
when(repository.findWarehouse(TENANT, WAREHOUSE)).thenReturn(warehouse());
|
||||
|
||||
assertThatThrownBy(() -> service.createPallet(TENANT, WAREHOUSE,
|
||||
new PalletRequests.SaveRequest("PLT-01", "STANDARD", null,
|
||||
@@ -161,4 +177,84 @@ class WarehouseResourceServiceTest {
|
||||
return new LocationRequests.SaveRequest("A", code, "STORAGE",
|
||||
new BigDecimal("1000.000"), "ACTIVE", null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatesRestartSafePalletCodeAndUsesDefaultVirtualLocation() {
|
||||
when(repository.findWarehouse(TENANT, WAREHOUSE)).thenReturn(warehouse());
|
||||
WarehouseLocationEntity location = new WarehouseLocationEntity();
|
||||
location.setId(30003L);
|
||||
location.setLocationType("VIRTUAL");
|
||||
location.setStatus("ACTIVE");
|
||||
when(repository.findLocationByCode(TENANT, WAREHOUSE, "LOC-US-LAX-01-DEFAULT"))
|
||||
.thenReturn(location);
|
||||
when(repository.warehouseOwned(TENANT, WAREHOUSE)).thenReturn(true);
|
||||
when(repository.findLocation(TENANT, WAREHOUSE, 30003L)).thenReturn(location);
|
||||
|
||||
service.createPallet(TENANT, WAREHOUSE,
|
||||
new PalletRequests.SaveRequest(null, "STANDARD", null, BigDecimal.ONE, "IDLE", null, null));
|
||||
|
||||
ArgumentCaptor<WarehousePalletEntity> captor = ArgumentCaptor.forClass(WarehousePalletEntity.class);
|
||||
verify(repository).insertPallet(captor.capture());
|
||||
assertThat(captor.getValue().getPalletCode()).matches("PLT-US-LAX-01-[0-9]{8}-[0-9]+");
|
||||
assertThat(captor.getValue().getLocationId()).isEqualTo(30003L);
|
||||
assertThat(captor.getValue().getId()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultVirtualLocationIsInsertedIdempotentlyForAnExistingWarehouse() {
|
||||
when(repository.findWarehouse(TENANT, WAREHOUSE)).thenReturn(warehouse());
|
||||
WarehouseLocationEntity resolved = new WarehouseLocationEntity();
|
||||
resolved.setId(30004L);
|
||||
resolved.setLocationType("VIRTUAL");
|
||||
resolved.setStatus("ACTIVE");
|
||||
when(repository.findLocationByCode(TENANT, WAREHOUSE, "LOC-US-LAX-01-DEFAULT"))
|
||||
.thenReturn(null, resolved);
|
||||
|
||||
WarehouseLocationEntity result = service.ensureDefaultVirtualLocation(TENANT, WAREHOUSE);
|
||||
|
||||
assertThat(result.getId()).isEqualTo(30004L);
|
||||
ArgumentCaptor<WarehouseLocationEntity> location = ArgumentCaptor.forClass(WarehouseLocationEntity.class);
|
||||
verify(repository).insertDefaultVirtualLocationIgnore(location.capture());
|
||||
assertThat(location.getValue().getLocationCode()).isEqualTo("LOC-US-LAX-01-DEFAULT");
|
||||
assertThat(location.getValue().getLocationType()).isEqualTo("VIRTUAL");
|
||||
assertThat(location.getValue().getStatus()).isEqualTo("ACTIVE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsAConflictingDefaultLocationCode() {
|
||||
when(repository.findWarehouse(TENANT, WAREHOUSE)).thenReturn(warehouse());
|
||||
WarehouseLocationEntity conflict = new WarehouseLocationEntity();
|
||||
conflict.setLocationType("STORAGE");
|
||||
conflict.setStatus("ACTIVE");
|
||||
when(repository.findLocationByCode(TENANT, WAREHOUSE, "LOC-US-LAX-01-DEFAULT"))
|
||||
.thenReturn(conflict);
|
||||
|
||||
assertThatThrownBy(() -> service.ensureDefaultVirtualLocation(TENANT, WAREHOUSE))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("Default virtual location code");
|
||||
verify(repository, never()).insertDefaultVirtualLocationIgnore(org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void scrappedPalletCannotBeReactivated() {
|
||||
when(repository.warehouseOwned(TENANT, WAREHOUSE)).thenReturn(true);
|
||||
WarehousePalletEntity existing = new WarehousePalletEntity();
|
||||
existing.setId(40002L);
|
||||
existing.setPalletCode("PLT-OLD");
|
||||
existing.setStatus("SCRAPPED");
|
||||
when(repository.findPallet(TENANT, WAREHOUSE, 40002L)).thenReturn(existing);
|
||||
|
||||
assertThatThrownBy(() -> service.updatePallet(TENANT, WAREHOUSE, 40002L,
|
||||
new PalletRequests.SaveRequest(null, "STANDARD", null, BigDecimal.ONE, "IDLE", null, null)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("cannot be reused");
|
||||
}
|
||||
|
||||
private static WarehouseEntity warehouse() {
|
||||
WarehouseEntity warehouse = new WarehouseEntity();
|
||||
warehouse.setId(WAREHOUSE);
|
||||
warehouse.setTenantId(TENANT);
|
||||
warehouse.setWarehouseCode("US-LAX-01");
|
||||
return warehouse;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user