Merge origin/dev into test preserving warehouse and PDA workflows
This commit is contained in:
+9
@@ -2,6 +2,7 @@ package com.baiyue.warehouse.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.warehouse.dto.DropshipPalletRequests;
|
||||
import com.baiyue.warehouse.dto.RelabelPalletRequests;
|
||||
import com.baiyue.warehouse.entity.WarehousePalletEntity;
|
||||
import com.baiyue.warehouse.dto.WarehousePalletResolveResponse;
|
||||
import com.baiyue.warehouse.security.InternalServiceTokenGuard;
|
||||
@@ -24,6 +25,14 @@ public class InternalWarehousePalletController {
|
||||
return ApiResponse.success(service.ensureDropshipPallet(request.customerId(), request.warehouseId(), request.businessId()));
|
||||
}
|
||||
|
||||
@PostMapping("/ensure-relabel")
|
||||
public ApiResponse<WarehousePalletResolveResponse> ensureRelabel(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@Valid @RequestBody RelabelPalletRequests.Ensure request) {
|
||||
tokenGuard.verify(token);
|
||||
return ApiResponse.success(service.ensureRelabelPallet(request.warehouseId(), request.businessId()));
|
||||
}
|
||||
|
||||
@GetMapping("/resolve/{palletCode}")
|
||||
public ApiResponse<WarehousePalletResolveResponse> resolve(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
package com.baiyue.warehouse.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.PositiveOrZero;
|
||||
import jakarta.validation.constraints.Size;
|
||||
@@ -12,7 +11,7 @@ public final class PalletTransitionRequests {
|
||||
public record Apply(@Positive long warehouseId,
|
||||
@PositiveOrZero int expectedVersion,
|
||||
@NotBlank @Size(max = 20) String targetStatus,
|
||||
@NotNull @Positive Long targetLocationId,
|
||||
@Positive Long targetLocationId,
|
||||
@NotBlank @Size(max = 128) String operationKey,
|
||||
@Size(max = 500) String reason) { }
|
||||
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ public record PalletTransitionResponse(
|
||||
long warehouseId,
|
||||
String beforeStatus,
|
||||
String afterStatus,
|
||||
long beforeLocationId,
|
||||
long afterLocationId,
|
||||
Long beforeLocationId,
|
||||
Long afterLocationId,
|
||||
int beforeVersion,
|
||||
int afterVersion,
|
||||
String operationStatus,
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.baiyue.warehouse.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public final class RelabelPalletRequests {
|
||||
private RelabelPalletRequests() { }
|
||||
|
||||
public record Ensure(@Positive long warehouseId,
|
||||
@NotBlank @Size(max = 128) String businessId) { }
|
||||
}
|
||||
+3
-3
@@ -4,7 +4,7 @@ public record WarehousePalletResolveResponse(
|
||||
long palletId,
|
||||
String palletCode,
|
||||
long warehouseId,
|
||||
long locationId,
|
||||
Long locationId,
|
||||
String status,
|
||||
String fbaWarehouseCode,
|
||||
java.math.BigDecimal unitPrice,
|
||||
@@ -14,13 +14,13 @@ public record WarehousePalletResolveResponse(
|
||||
boolean reservable,
|
||||
String reservationBlockReason
|
||||
) {
|
||||
public WarehousePalletResolveResponse(long palletId, String palletCode, long warehouseId, long locationId,
|
||||
public WarehousePalletResolveResponse(long palletId, String palletCode, long warehouseId, Long locationId,
|
||||
String status, String fbaWarehouseCode, int version) {
|
||||
this(palletId, palletCode, warehouseId, locationId, status, fbaWarehouseCode,
|
||||
java.math.BigDecimal.ZERO, version, null, null, false, "LOCATION_STATE_UNKNOWN");
|
||||
}
|
||||
|
||||
public WarehousePalletResolveResponse(long palletId, String palletCode, long warehouseId, long locationId,
|
||||
public WarehousePalletResolveResponse(long palletId, String palletCode, long warehouseId, Long locationId,
|
||||
String status, String fbaWarehouseCode,
|
||||
java.math.BigDecimal unitPrice, int version) {
|
||||
this(palletId, palletCode, warehouseId, locationId, status, fbaWarehouseCode,
|
||||
|
||||
+1
-1
@@ -39,6 +39,6 @@ public interface WarehousePalletTransitionMapper extends BaseMapper<WarehousePal
|
||||
"updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{palletId} AND customer_id=0 " +
|
||||
"AND warehouse_id=#{warehouseId} AND version=#{expectedVersion} AND status<>'SCRAPPED' AND deleted=0")
|
||||
int transition(@Param("warehouseId") long warehouseId, @Param("palletId") long palletId,
|
||||
@Param("targetStatus") String targetStatus, @Param("targetLocationId") long targetLocationId,
|
||||
@Param("targetStatus") String targetStatus, @Param("targetLocationId") Long targetLocationId,
|
||||
@Param("expectedVersion") int expectedVersion);
|
||||
}
|
||||
|
||||
+32
-8
@@ -45,16 +45,17 @@ public class WarehousePalletTransitionService {
|
||||
if (version != request.expectedVersion()) {
|
||||
throw new BizException(ResultCode.CONFLICT, "托盘版本已变化,请重新扫码");
|
||||
}
|
||||
if (pallet.getLocationId() == null) {
|
||||
throw new BizException(ResultCode.CONFLICT, "托盘没有有效库位,不能执行库内作业");
|
||||
}
|
||||
transition(pallet, status, request.targetLocationId(), version);
|
||||
// 当前海外尾程按托盘作业,不要求业务端选择库位。未传目标库位时保留托盘现有
|
||||
// 技术库位;历史无库位托盘则只流转托盘状态,不能因此阻断 PDA 作业。
|
||||
Long targetLocationId = request.targetLocationId() == null
|
||||
? pallet.getLocationId() : request.targetLocationId();
|
||||
transition(pallet, status, targetLocationId, version);
|
||||
WarehousePalletOperationEntity operation = new WarehousePalletOperationEntity();
|
||||
operation.setId(IdWorker.getId()); operation.setOperationKey(key); operation.setRequestHash(requestHash);
|
||||
operation.setWarehouseId(request.warehouseId()); operation.setPalletId(palletId);
|
||||
operation.setPalletCode(pallet.getPalletCode()); operation.setBeforeStatus(pallet.getStatus());
|
||||
operation.setAfterStatus(status); operation.setBeforeLocationId(pallet.getLocationId());
|
||||
operation.setAfterLocationId(request.targetLocationId()); operation.setBeforeVersion(version);
|
||||
operation.setAfterLocationId(targetLocationId); operation.setBeforeVersion(version);
|
||||
operation.setAfterVersion(version + 1); operation.setOperationStatus("APPLIED");
|
||||
operation.setReason(trim(request.reason())); operation.setCreatedAt(LocalDateTime.now());
|
||||
operation.setUpdatedAt(operation.getCreatedAt()); mapper.insert(operation);
|
||||
@@ -75,7 +76,7 @@ public class WarehousePalletTransitionService {
|
||||
}
|
||||
WarehousePalletEntity pallet = mapper.lockPublicPallet(request.warehouseId(), palletId);
|
||||
if (pallet == null || !operation.getAfterStatus().equals(pallet.getStatus())
|
||||
|| !Long.valueOf(operation.getAfterLocationId()).equals(pallet.getLocationId())) {
|
||||
|| !java.util.Objects.equals(operation.getAfterLocationId(), pallet.getLocationId())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "托盘已被后续作业修改,自动补偿失败");
|
||||
}
|
||||
transition(pallet, operation.getBeforeStatus(), operation.getBeforeLocationId(),
|
||||
@@ -85,14 +86,18 @@ public class WarehousePalletTransitionService {
|
||||
return response(operation, false);
|
||||
}
|
||||
|
||||
private void transition(WarehousePalletEntity pallet, String targetStatus, long targetLocationId,
|
||||
private void transition(WarehousePalletEntity pallet, String targetStatus, Long targetLocationId,
|
||||
int expectedVersion) {
|
||||
int currentVersion = pallet.getVersion() == null ? 0 : pallet.getVersion();
|
||||
if (currentVersion != expectedVersion || !MUTABLE_STATUSES.contains(pallet.getStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "托盘版本或状态已变化,请重新扫码");
|
||||
}
|
||||
long warehouseId = pallet.getWarehouseId();
|
||||
long sourceLocationId = pallet.getLocationId();
|
||||
Long sourceLocationId = pallet.getLocationId();
|
||||
if (sourceLocationId == null || targetLocationId == null) {
|
||||
transitionWithOptionalLocation(pallet, sourceLocationId, targetLocationId, targetStatus, expectedVersion);
|
||||
return;
|
||||
}
|
||||
WarehouseLocationEntity source;
|
||||
WarehouseLocationEntity target;
|
||||
if (sourceLocationId <= targetLocationId) {
|
||||
@@ -116,6 +121,25 @@ public class WarehousePalletTransitionService {
|
||||
}
|
||||
}
|
||||
|
||||
private void transitionWithOptionalLocation(WarehousePalletEntity pallet, Long sourceLocationId,
|
||||
Long targetLocationId, String targetStatus,
|
||||
int expectedVersion) {
|
||||
long warehouseId = pallet.getWarehouseId();
|
||||
if (sourceLocationId != null) {
|
||||
WarehouseLocationEntity source = requireLocation(warehouseId, sourceLocationId);
|
||||
requireLocationMatchesPallet(source, pallet.getStatus());
|
||||
releaseSourceLocation(warehouseId, source, pallet.getStatus());
|
||||
}
|
||||
if (targetLocationId != null) {
|
||||
WarehouseLocationEntity target = requireLocation(warehouseId, targetLocationId);
|
||||
requireAvailableTarget(target, targetStatus);
|
||||
occupyTargetLocation(warehouseId, target, targetStatus);
|
||||
}
|
||||
if (mapper.transition(warehouseId, pallet.getId(), targetStatus, targetLocationId, expectedVersion) != 1) {
|
||||
throw new BizException(ResultCode.CONFLICT, "托盘版本已变化,请重新扫码");
|
||||
}
|
||||
}
|
||||
|
||||
private WarehouseLocationEntity requireLocation(long warehouseId, long locationId) {
|
||||
WarehouseLocationEntity location = mapper.lockPublicLocation(warehouseId, locationId);
|
||||
if (location == null || "INACTIVE".equals(location.getStatus())) {
|
||||
|
||||
+27
-5
@@ -116,6 +116,30 @@ public class WarehouseResourceService {
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public WarehousePalletResolveResponse ensureRelabelPallet(long warehouseId, String businessId) {
|
||||
String normalizedBusinessId = businessId == null ? null : businessId.trim();
|
||||
WarehousePalletBindingEntity existing = repository.findPalletBinding(PUBLIC_RESOURCE_SCOPE, warehouseId,
|
||||
"RELABEL_PDA_NEW", normalizedBusinessId);
|
||||
if (existing != null) return resolvePublicPallet(existing.getPalletId());
|
||||
Long palletId = createPallet(PUBLIC_RESOURCE_SCOPE, warehouseId,
|
||||
new PalletRequests.SaveRequest(null, "STANDARD", null, java.math.BigDecimal.ZERO, "IDLE", null,
|
||||
"PDA换标拆托新托盘"));
|
||||
WarehousePalletBindingEntity binding = new WarehousePalletBindingEntity();
|
||||
binding.setId(IdWorker.getId()); binding.setCustomerId(PUBLIC_RESOURCE_SCOPE); binding.setWarehouseId(warehouseId);
|
||||
binding.setBusinessType("RELABEL_PDA_NEW"); binding.setBusinessId(normalizedBusinessId); binding.setPalletId(palletId);
|
||||
binding.setCreatedAt(java.time.LocalDateTime.now());
|
||||
try {
|
||||
repository.insertPalletBinding(binding);
|
||||
return resolvePublicPallet(palletId);
|
||||
} catch (org.springframework.dao.DuplicateKeyException duplicate) {
|
||||
WarehousePalletBindingEntity winner = repository.findPalletBinding(PUBLIC_RESOURCE_SCOPE, warehouseId,
|
||||
"RELABEL_PDA_NEW", normalizedBusinessId);
|
||||
if (winner == null) throw duplicate;
|
||||
return resolvePublicPallet(winner.getPalletId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the warehouse-scoped virtual location used only as a pallet/inventory reference.
|
||||
* It is deliberately never made IDLE/OCCUPIED by resource locking.
|
||||
@@ -186,7 +210,6 @@ public class WarehouseResourceService {
|
||||
if (matches.isEmpty()) throw new BizException(ResultCode.NOT_FOUND, "托盘码不存在");
|
||||
if (matches.size() != 1) throw new BizException(ResultCode.CONFLICT, "托盘码在多个仓库重复,无法解析");
|
||||
WarehousePalletEntity pallet = matches.get(0);
|
||||
if (pallet.getLocationId() == null) throw new BizException(ResultCode.CONFLICT, "托盘未绑定库位");
|
||||
if ("SCRAPPED".equals(pallet.getStatus())) throw new BizException(ResultCode.CONFLICT, "托盘已报废");
|
||||
return palletReference(pallet);
|
||||
}
|
||||
@@ -200,7 +223,6 @@ public class WarehouseResourceService {
|
||||
public WarehousePalletResolveResponse resolvePublicPallet(long palletId) {
|
||||
WarehousePalletEntity pallet = repository.findPublicPallet(palletId);
|
||||
if (pallet == null) throw new BizException(ResultCode.NOT_FOUND, "托盘不存在");
|
||||
if (pallet.getLocationId() == null) throw new BizException(ResultCode.CONFLICT, "托盘未绑定库位");
|
||||
return palletReference(pallet);
|
||||
}
|
||||
|
||||
@@ -224,8 +246,8 @@ public class WarehouseResourceService {
|
||||
}
|
||||
|
||||
private WarehousePalletResolveResponse palletReference(WarehousePalletEntity pallet) {
|
||||
WarehouseLocationEntity location = repository.findLocation(PUBLIC_RESOURCE_SCOPE,
|
||||
pallet.getWarehouseId(), pallet.getLocationId());
|
||||
WarehouseLocationEntity location = pallet.getLocationId() == null ? null
|
||||
: repository.findLocation(PUBLIC_RESOURCE_SCOPE, pallet.getWarehouseId(), pallet.getLocationId());
|
||||
String blockReason = reservationBlockReason(pallet, location);
|
||||
return new WarehousePalletResolveResponse(pallet.getId(), pallet.getPalletCode(), pallet.getWarehouseId(),
|
||||
pallet.getLocationId(), pallet.getStatus(), pallet.getFbaWarehouseCode(), pallet.getUnitPrice(),
|
||||
@@ -236,7 +258,7 @@ public class WarehouseResourceService {
|
||||
|
||||
private static String reservationBlockReason(WarehousePalletEntity pallet, WarehouseLocationEntity location) {
|
||||
if (!"IDLE".equals(pallet.getStatus())) return "PALLET_NOT_IDLE";
|
||||
if (location == null) return "LOCATION_NOT_FOUND";
|
||||
if (location == null) return pallet.getLocationId() == null ? "LOCATION_NOT_REQUIRED" : "LOCATION_NOT_FOUND";
|
||||
if ("VIRTUAL".equals(location.getLocationType())) {
|
||||
return "ACTIVE".equals(location.getStatus()) ? null : "VIRTUAL_LOCATION_NOT_ACTIVE";
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ CREATE TABLE IF NOT EXISTS wh_pallet_operation (
|
||||
request_hash VARCHAR(128) NOT NULL COMMENT '状态变更请求摘要', warehouse_id BIGINT NOT NULL COMMENT '实际海外仓ID',
|
||||
pallet_id BIGINT NOT NULL COMMENT '实物托盘ID', pallet_code VARCHAR(128) NOT NULL COMMENT '实物托盘码快照',
|
||||
before_status VARCHAR(20) NOT NULL COMMENT '变更前托盘状态', after_status VARCHAR(20) NOT NULL COMMENT '变更后托盘状态',
|
||||
before_location_id BIGINT NOT NULL COMMENT '变更前库位ID', after_location_id BIGINT NOT NULL COMMENT '变更后库位ID',
|
||||
before_location_id BIGINT NULL COMMENT '变更前库位ID,无库位托盘为空', after_location_id BIGINT NULL COMMENT '变更后库位ID,无库位托盘为空',
|
||||
before_version INT NOT NULL COMMENT '变更前托盘版本', after_version INT NOT NULL COMMENT '变更后托盘版本',
|
||||
operation_status VARCHAR(32) NOT NULL COMMENT '操作状态:APPLIED已应用、COMPENSATED已补偿',
|
||||
reason VARCHAR(500) NULL COMMENT '操作原因', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
@@ -123,6 +123,10 @@ CREATE TABLE IF NOT EXISTS wh_pallet_operation (
|
||||
KEY idx_wh_pallet_operation_pallet (warehouse_id,pallet_id,created_at)
|
||||
) ENGINE=InnoDB COMMENT='实物托盘状态幂等变更与补偿记录';
|
||||
|
||||
ALTER TABLE wh_pallet_operation
|
||||
MODIFY COLUMN before_location_id BIGINT NULL COMMENT '变更前库位ID,无库位托盘为空',
|
||||
MODIFY COLUMN after_location_id BIGINT NULL COMMENT '变更后库位ID,无库位托盘为空';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS wh_pallet_batch (
|
||||
id BIGINT NOT NULL COMMENT '批量托盘预留ID',
|
||||
warehouse_id BIGINT NOT NULL COMMENT '实际海外仓ID',
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Overseas warehouse PDA pallet transition internal API
|
||||
version: 1.0.0
|
||||
paths:
|
||||
/internal/v1/warehouse-pallets/resolve/{palletCode}:
|
||||
get:
|
||||
summary: Resolve a public pallet for PDA operations
|
||||
description: Location fields are nullable because overseas tail-leg operations may work by pallet only.
|
||||
security: [{ internalServiceToken: [] }]
|
||||
parameters:
|
||||
- in: path
|
||||
name: palletCode
|
||||
required: true
|
||||
schema: { type: string }
|
||||
responses:
|
||||
'200':
|
||||
description: Pallet resolution result
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/PalletResolveApiResponse' }
|
||||
/internal/v1/pda-pallet-transitions/{palletId}:
|
||||
post:
|
||||
summary: Apply an idempotent PDA pallet state transition
|
||||
security: [{ internalServiceToken: [] }]
|
||||
parameters:
|
||||
- in: path
|
||||
name: palletId
|
||||
required: true
|
||||
schema: { type: integer, format: int64, minimum: 1 }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/PalletTransitionApply' }
|
||||
responses:
|
||||
'200':
|
||||
description: Transition result
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/PalletTransitionApiResponse' }
|
||||
/internal/v1/pda-pallet-transitions/{palletId}/{operationKey}/compensate:
|
||||
post:
|
||||
summary: Compensate an applied PDA pallet transition
|
||||
security: [{ internalServiceToken: [] }]
|
||||
parameters:
|
||||
- in: path
|
||||
name: palletId
|
||||
required: true
|
||||
schema: { type: integer, format: int64, minimum: 1 }
|
||||
- in: path
|
||||
name: operationKey
|
||||
required: true
|
||||
schema: { type: string, maxLength: 128 }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/PalletTransitionCompensate' }
|
||||
responses:
|
||||
'200':
|
||||
description: Compensation result
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/PalletTransitionApiResponse' }
|
||||
components:
|
||||
securitySchemes:
|
||||
internalServiceToken: { type: apiKey, in: header, name: X-Internal-Service-Token }
|
||||
schemas:
|
||||
PalletTransitionApply:
|
||||
type: object
|
||||
required: [warehouseId, expectedVersion, targetStatus, operationKey]
|
||||
properties:
|
||||
warehouseId: { type: string, pattern: '^[1-9][0-9]*$' }
|
||||
expectedVersion: { type: integer, minimum: 0 }
|
||||
targetStatus: { type: string, enum: [IDLE, OCCUPIED] }
|
||||
targetLocationId:
|
||||
type: string
|
||||
pattern: '^[1-9][0-9]*$'
|
||||
nullable: true
|
||||
description: Optional; omitted for pallet-only operations without location management
|
||||
operationKey: { type: string, maxLength: 128 }
|
||||
reason: { type: string, maxLength: 500, nullable: true }
|
||||
PalletTransitionCompensate:
|
||||
type: object
|
||||
required: [warehouseId]
|
||||
properties:
|
||||
warehouseId: { type: string, pattern: '^[1-9][0-9]*$' }
|
||||
PalletResolveResult:
|
||||
type: object
|
||||
required: [palletId, palletCode, warehouseId, status, unitPrice, version, reservable]
|
||||
properties:
|
||||
palletId: { type: string, pattern: '^[1-9][0-9]*$' }
|
||||
palletCode: { type: string }
|
||||
warehouseId: { type: string, pattern: '^[1-9][0-9]*$' }
|
||||
locationId: { type: string, pattern: '^[1-9][0-9]*$', nullable: true }
|
||||
status: { type: string }
|
||||
fbaWarehouseCode: { type: string, nullable: true }
|
||||
unitPrice: { type: number }
|
||||
version: { type: integer, minimum: 0 }
|
||||
locationType: { type: string, nullable: true }
|
||||
locationStatus: { type: string, nullable: true }
|
||||
reservable: { type: boolean }
|
||||
reservationBlockReason: { type: string, nullable: true }
|
||||
PalletTransitionResult:
|
||||
type: object
|
||||
required: [operationKey, palletId, palletCode, warehouseId, beforeStatus, afterStatus, beforeVersion, afterVersion, operationStatus, replayed]
|
||||
properties:
|
||||
operationKey: { type: string }
|
||||
palletId: { type: string, pattern: '^[1-9][0-9]*$' }
|
||||
palletCode: { type: string }
|
||||
warehouseId: { type: string, pattern: '^[1-9][0-9]*$' }
|
||||
beforeStatus: { type: string }
|
||||
afterStatus: { type: string }
|
||||
beforeLocationId: { type: string, pattern: '^[1-9][0-9]*$', nullable: true }
|
||||
afterLocationId: { type: string, pattern: '^[1-9][0-9]*$', nullable: true }
|
||||
beforeVersion: { type: integer }
|
||||
afterVersion: { type: integer }
|
||||
operationStatus: { type: string }
|
||||
replayed: { type: boolean }
|
||||
PalletTransitionApiResponse:
|
||||
type: object
|
||||
properties:
|
||||
code: { type: string, example: '200' }
|
||||
message: { type: string }
|
||||
data: { $ref: '#/components/schemas/PalletTransitionResult' }
|
||||
PalletResolveApiResponse:
|
||||
type: object
|
||||
properties:
|
||||
code: { type: string, example: '200' }
|
||||
message: { type: string }
|
||||
data: { $ref: '#/components/schemas/PalletResolveResult' }
|
||||
+17
@@ -111,6 +111,23 @@ class WarehousePalletTransitionServiceTest {
|
||||
verify(mapper, never()).transitionLocation(anyLong(), anyLong(), anyString(), anyString());
|
||||
}
|
||||
|
||||
@Test void locationlessPalletCanChangeStatusWithoutSelectingLocation() {
|
||||
WarehousePalletEntity pallet = pallet("P-NO-LOCATION", "IDLE", 2);
|
||||
pallet.setLocationId(null);
|
||||
when(mapper.lockPublicPallet(88L, 10L)).thenReturn(pallet);
|
||||
when(mapper.transition(88L, 10L, "OCCUPIED", null, 2)).thenReturn(1);
|
||||
|
||||
var response = service.apply(10L, new PalletTransitionRequests.Apply(
|
||||
88L, 2, "OCCUPIED", null, "locationless-occupy", "pallet-only operation"));
|
||||
|
||||
assertNull(response.beforeLocationId());
|
||||
assertNull(response.afterLocationId());
|
||||
assertEquals("OCCUPIED", response.afterStatus());
|
||||
verify(mapper, never()).lockPublicLocation(anyLong(), anyLong());
|
||||
verify(mapper, never()).transitionLocation(anyLong(), anyLong(), anyString(), anyString());
|
||||
verify(mapper).transition(88L, 10L, "OCCUPIED", null, 2);
|
||||
}
|
||||
|
||||
@Test void inconsistentIdlePalletAndOccupiedLocationIsRejectedBeforeMutation() {
|
||||
when(mapper.lockPublicPallet(88L, 10L)).thenReturn(pallet("P-1", "IDLE", 4));
|
||||
when(mapper.lockPublicLocation(88L, 11L)).thenReturn(location(11L, "STORAGE", "OCCUPIED"));
|
||||
|
||||
+33
@@ -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.WarehousePalletBindingEntity;
|
||||
import com.baiyue.warehouse.dto.SharedWarehouseReference;
|
||||
import com.baiyue.warehouse.repository.WarehouseResourceRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -97,6 +98,21 @@ class WarehouseResourceServiceTest {
|
||||
assertThat(result.reservationBlockReason()).isEqualTo("LOCATION_NOT_IDLE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesLocationlessPalletForPalletOnlyWorkflow() {
|
||||
WarehousePalletEntity pallet = pallet(40001L, "IDLE", 30001L);
|
||||
pallet.setLocationId(null);
|
||||
when(repository.findPublicPalletsByCode("PLT-NO-LOCATION")).thenReturn(List.of(pallet));
|
||||
pallet.setPalletCode("PLT-NO-LOCATION");
|
||||
|
||||
var result = service.resolvePublicPallet("PLT-NO-LOCATION");
|
||||
|
||||
assertThat(result.locationId()).isNull();
|
||||
assertThat(result.reservable()).isFalse();
|
||||
assertThat(result.reservationBlockReason()).isEqualTo("LOCATION_NOT_REQUIRED");
|
||||
verify(repository, never()).findLocation(0L, WAREHOUSE, 0L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsLocationWithSnowflakeAssignedByMapper() {
|
||||
doAnswer(invocation -> {
|
||||
@@ -378,6 +394,23 @@ class WarehouseResourceServiceTest {
|
||||
verify(repository, never()).updatePallet(org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void relabelPalletCreationReplaysExistingBusinessBinding() {
|
||||
WarehousePalletBindingEntity binding = new WarehousePalletBindingEntity();
|
||||
binding.setPalletId(40001L);
|
||||
when(repository.findPalletBinding(0L, WAREHOUSE, "RELABEL_PDA_NEW", "RL-1:key-1"))
|
||||
.thenReturn(binding);
|
||||
when(repository.findPublicPallet(40001L)).thenReturn(pallet(40001L, "IDLE", 30001L));
|
||||
when(repository.findLocation(0L, WAREHOUSE, 30001L)).thenReturn(location(30001L, "VIRTUAL", "ACTIVE"));
|
||||
|
||||
var result = service.ensureRelabelPallet(WAREHOUSE, " RL-1:key-1 ");
|
||||
|
||||
assertThat(result.palletId()).isEqualTo(40001L);
|
||||
assertThat(result.palletCode()).isEqualTo("PLT-01");
|
||||
verify(repository, never()).insertPallet(org.mockito.ArgumentMatchers.any());
|
||||
verify(repository, never()).insertPalletBinding(org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
private static SharedWarehouseReference warehouse() {
|
||||
return new SharedWarehouseReference(WAREHOUSE, "US-LAX-01", "Los Angeles", "ACTIVE");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user